From bd51ac38f0e9f122b2bd4ef5bdde12db638c1733 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Tue, 18 Apr 2017 17:09:33 -0400 Subject: [PATCH] initial commit --- .gitignore | 7 + js/add-array.js | 110 + js/add-three.js | 119 + js/ext-clip.js | 6986 +++++++++++++++++ js/ext-fsave.js | 270 + js/ext-n3d.js | 1 + js/ext-pixi.js | 1 + js/ext-tween.js | 1 + js/geo-bounds.js | 141 + js/geo-debug.js | 221 + js/geo-line.js | 114 + js/geo-point.js | 645 ++ js/geo-polygon.js | 1192 +++ js/geo-polygons.js | 710 ++ js/geo-render.js | 99 + js/geo-slope.js | 134 + js/geo.js | 394 + js/kiri-codec.js | 223 + js/kiri-db.js | 164 + js/kiri-driver-cam.js | 1573 ++++ js/kiri-driver-fdm.js | 462 ++ js/kiri-driver-laser.js | 354 + js/kiri-layer.js | 165 + js/kiri-pack.js | 64 + js/kiri-print.js | 765 ++ js/kiri-serial.js | 608 ++ js/kiri-slice.js | 1202 +++ js/kiri-slicer.js | 781 ++ js/kiri-widget.js | 647 ++ js/kiri-work.js | 317 + js/kiri.js | 3291 ++++++++ js/license.js | 7 + js/meta.js | 2136 +++++ js/moto-ajax.js | 77 + js/moto-ctrl.js | 566 ++ js/moto-db.js | 211 + js/moto-kv.js | 56 + js/moto-load-obj.js | 239 + js/moto-load-stl.js | 250 + js/moto-space.js | 622 ++ js/moto-ui.js | 323 + js/web-server.js | 1006 +++ license.md | 8 + notes.md | 112 + package.json | 31 + readme.md | 21 + web/kiri/favicon-mobile.png | Bin 0 -> 6142 bytes web/kiri/favicon.ico | Bin 0 -> 1150 bytes web/kiri/filter/CAM/Any.Generic.Grbl | 22 + web/kiri/filter/CAM/Any.Generic.LinuxCNC | 24 + web/kiri/filter/CAM/Any.Generic.TinyG | 1 + web/kiri/filter/CAM/Carbide3D.Nomad.883Pro | 22 + web/kiri/filter/CAM/Carbide3D.Shapeoko.3 | 22 + web/kiri/filter/CAM/Carbide3D.Shapeoko.XL | 22 + web/kiri/filter/CAM/Carbide3D.Shapeoko.XXL | 22 + web/kiri/filter/CAM/Inventables.Carvey.Any | 24 + web/kiri/filter/CAM/Inventables.XCarve.1000mm | 22 + web/kiri/filter/CAM/Inventables.XCarve.500mm | 22 + web/kiri/filter/CAM/Sienci.Mill.One | 22 + web/kiri/filter/FDM/Any.Generic.Marlin | 34 + web/kiri/filter/FDM/Any.Generic.Sailfish | 1 + web/kiri/filter/FDM/Atom.v2 | 26 + web/kiri/filter/FDM/DeltaMaker.2T | 30 + web/kiri/filter/FDM/DeltaMaker.Original | 30 + web/kiri/filter/FDM/Leapfrog.CreatrHS | 36 + web/kiri/filter/FDM/MakerGear.M2 | 38 + web/kiri/filter/FDM/Makerbot.Replicator2 | 44 + web/kiri/filter/FDM/TypeA.Series1 | 37 + web/kiri/filter/FDM/TypeA.Series1Pro | 40 + web/kiri/filter/FDM/Ultimaker.Ultimaker2 | 36 + web/kiri/filter/FDM/Wanhao.Duplicator_4s | 36 + web/kiri/filter/FDM/Wanhao.Duplicator_5s | 36 + web/kiri/filter/FDM/Wanhao.Duplicator_6 | 36 + web/kiri/filter/FDM/Wanhao.Duplicator_i3 | 36 + web/kiri/help-kiri.html | 27 + web/kiri/index.html | 251 + web/kiri/output-gcode.html | 47 + web/kiri/output-laser.html | 18 + web/kiri/style-atom3dp.css | 31 + web/kiri/style.css | 578 ++ web/meta/favicon-mobile.png | Bin 0 -> 6035 bytes web/meta/favicon.ico | Bin 0 -> 1150 bytes web/meta/index.html | 26 + web/meta/style.css | 56 + web/moto/style.css | 277 + web/obj/cube.stl | Bin 0 -> 16084 bytes web/obj/meta-corner-1.stl | Bin 0 -> 584 bytes web/obj/meta-corner-2.stl | Bin 0 -> 484 bytes web/obj/meta-corner-3.stl | Bin 0 -> 384 bytes web/obj/meta-corner-4.stl | Bin 0 -> 284 bytes 90 files changed, 29458 insertions(+) create mode 100644 .gitignore create mode 100644 js/add-array.js create mode 100644 js/add-three.js create mode 100755 js/ext-clip.js create mode 100644 js/ext-fsave.js create mode 120000 js/ext-n3d.js create mode 120000 js/ext-pixi.js create mode 120000 js/ext-tween.js create mode 100644 js/geo-bounds.js create mode 100644 js/geo-debug.js create mode 100644 js/geo-line.js create mode 100644 js/geo-point.js create mode 100644 js/geo-polygon.js create mode 100644 js/geo-polygons.js create mode 100644 js/geo-render.js create mode 100644 js/geo-slope.js create mode 100644 js/geo.js create mode 100644 js/kiri-codec.js create mode 100644 js/kiri-db.js create mode 100644 js/kiri-driver-cam.js create mode 100644 js/kiri-driver-fdm.js create mode 100644 js/kiri-driver-laser.js create mode 100644 js/kiri-layer.js create mode 100644 js/kiri-pack.js create mode 100644 js/kiri-print.js create mode 100644 js/kiri-serial.js create mode 100644 js/kiri-slice.js create mode 100644 js/kiri-slicer.js create mode 100644 js/kiri-widget.js create mode 100644 js/kiri-work.js create mode 100644 js/kiri.js create mode 100644 js/license.js create mode 100644 js/meta.js create mode 100644 js/moto-ajax.js create mode 100644 js/moto-ctrl.js create mode 100644 js/moto-db.js create mode 100644 js/moto-kv.js create mode 100644 js/moto-load-obj.js create mode 100644 js/moto-load-stl.js create mode 100644 js/moto-space.js create mode 100644 js/moto-ui.js create mode 100644 js/web-server.js create mode 100644 license.md create mode 100644 notes.md create mode 100644 package.json create mode 100644 readme.md create mode 100644 web/kiri/favicon-mobile.png create mode 100644 web/kiri/favicon.ico create mode 100644 web/kiri/filter/CAM/Any.Generic.Grbl create mode 100644 web/kiri/filter/CAM/Any.Generic.LinuxCNC create mode 120000 web/kiri/filter/CAM/Any.Generic.TinyG create mode 100644 web/kiri/filter/CAM/Carbide3D.Nomad.883Pro create mode 100644 web/kiri/filter/CAM/Carbide3D.Shapeoko.3 create mode 100644 web/kiri/filter/CAM/Carbide3D.Shapeoko.XL create mode 100644 web/kiri/filter/CAM/Carbide3D.Shapeoko.XXL create mode 100644 web/kiri/filter/CAM/Inventables.Carvey.Any create mode 100644 web/kiri/filter/CAM/Inventables.XCarve.1000mm create mode 100644 web/kiri/filter/CAM/Inventables.XCarve.500mm create mode 100644 web/kiri/filter/CAM/Sienci.Mill.One create mode 100644 web/kiri/filter/FDM/Any.Generic.Marlin create mode 120000 web/kiri/filter/FDM/Any.Generic.Sailfish create mode 100644 web/kiri/filter/FDM/Atom.v2 create mode 100644 web/kiri/filter/FDM/DeltaMaker.2T create mode 100644 web/kiri/filter/FDM/DeltaMaker.Original create mode 100644 web/kiri/filter/FDM/Leapfrog.CreatrHS create mode 100644 web/kiri/filter/FDM/MakerGear.M2 create mode 100644 web/kiri/filter/FDM/Makerbot.Replicator2 create mode 100644 web/kiri/filter/FDM/TypeA.Series1 create mode 100644 web/kiri/filter/FDM/TypeA.Series1Pro create mode 100644 web/kiri/filter/FDM/Ultimaker.Ultimaker2 create mode 100644 web/kiri/filter/FDM/Wanhao.Duplicator_4s create mode 100644 web/kiri/filter/FDM/Wanhao.Duplicator_5s create mode 100644 web/kiri/filter/FDM/Wanhao.Duplicator_6 create mode 100644 web/kiri/filter/FDM/Wanhao.Duplicator_i3 create mode 100644 web/kiri/help-kiri.html create mode 100644 web/kiri/index.html create mode 100644 web/kiri/output-gcode.html create mode 100644 web/kiri/output-laser.html create mode 100644 web/kiri/style-atom3dp.css create mode 100644 web/kiri/style.css create mode 100644 web/meta/favicon-mobile.png create mode 100644 web/meta/favicon.ico create mode 100644 web/meta/index.html create mode 100644 web/meta/style.css create mode 100644 web/moto/style.css create mode 100644 web/obj/cube.stl create mode 100644 web/obj/meta-corner-1.stl create mode 100644 web/obj/meta-corner-2.stl create mode 100644 web/obj/meta-corner-3.stl create mode 100644 web/obj/meta-corner-4.stl diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..61b170bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +node_modules +persist +cache +mod +*.log +obj/*.stl diff --git a/js/add-array.js b/js/add-array.js new file mode 100644 index 00000000..69c35952 --- /dev/null +++ b/js/add-array.js @@ -0,0 +1,110 @@ +"use strict"; + +(function() { + var AP = Array.prototype; + + /** ****************************************************************** + * array prototype helpers + ******************************************************************* */ + + AP.peek = function() { + return this[this.length-1]; + }; + + /** + * allow chaining of push() calls + * + * @param v + * @returns {Array} + */ + AP.append = function(v) { + this.push(v); + return this; + }; + + /** + * append all array elements to this array + * + * @param {Array} arr + * @returns {Array} + */ + AP.appendAll = function(arr) { + if (arr && arr.length > 0) this.push.apply(this,arr); + return this; + }; + + /** + * shallow cloning with clone(arg) call on each new element + * + * @param {Array} [arg] + * @returns {Array} + */ + AP.clone = function(arg) { + var na = this.slice(), + ln = na.length, + i = 0; + while (i < ln) na[i] = na[i++].clone(arg); + return na; + }; + + /** + * remove and return element from array, if present + * + * @param val + * @returns {*} + */ + AP.remove = function(val) { + var idx = this.indexOf(val); + if (idx >= 0) return this.splice(idx,1); + return null; + }; + + /** + * return last array element if array length > 0 + * + * @returns {*} + */ + AP.last = function() { + if (this.length === 0) return null; + return this[this.length-1]; + }; + + AP.contains = function(val) { + return this.indexOf(val) >= 0; + }; + + AP.toFloat32 = function() { + var i = 0, f32 = new Float32Array(this.length); + while (i < this.length) { + f32[i] = this[i++]; + } + return f32; + }; + + AP.forEachPair = function(fn, incr) { + var scope = this, + idx = 0, + inc = incr || 2, + len = scope.length; + while (idx < len) { + fn(scope[idx], scope[(idx+1)%len], idx); + idx += inc; + } + }; + + AP.show = function(fn,f) { + f = f || function(v) { console.log(v) }; + this.forEach(function(av) { + f(av[fn]()); + }) + }; + + /** ****************************************************************** + * string prototype helpers + ******************************************************************* */ + + String.prototype.reverse = function() { + return this.split('').reverse().join(''); + }; + +})(); diff --git a/js/add-three.js b/js/add-three.js new file mode 100644 index 00000000..bc1d8c1b --- /dev/null +++ b/js/add-three.js @@ -0,0 +1,119 @@ +"use strict"; + +(function() { + + var MP = THREE.Mesh.prototype, + GP = THREE.Geometry.prototype, + BP = THREE.BufferGeometry.prototype; + + MP.getBoundingBox = function(update) { + return this.geometry.getBoundingBox(update); + }; + + MP.center = function() { + this.geometry.center(); + return this; + }; + + MP.mirrorX = function() { + return this.mirror(0); + }; + + MP.mirrorY = function() { + return this.mirror(1); + }; + + MP.mirrorZ = function() { + return this.mirror(2); + }; + + MP.mirror = function(start) { + var i, + geo = this.geometry, + at = geo.attributes, + pa = at.position.array, + nm = at.normal.array; + for (i = start || 0 ; i < pa.length; i += 3) { + pa[i] = -pa[i]; + nm[i] = -nm[i]; + } + geo.computeVertexNormals(); + return this; + }; + + GP.center = function(x,y,z) { + var box = this.getBoundingBox(), + mid = box.dim.clone().multiplyScalar(0.5), + dif = mid.clone().add(box.min), + pos = this.attributes.position, + arr = pos.array, + maxx = Math.max(mid.x, mid.y, mid.z), + i = 0; + if (x) dif.x -= (maxx - mid.x) * x; + if (y) dif.y -= (maxx - mid.y) * y; + if (z) dif.z -= (maxx - mid.z) * z; + while (i < arr.length) { + arr[i++] -= dif.x; + arr[i++] -= dif.y; + arr[i++] -= dif.z; + } + pos.needsUpdate = true; + // force update of (obsolete) bounding box + this.getBoundingBox(true); + return this; + }; + + GP.getBoundingBox = function(update) { + //if (update) this.boundingBox = null; + if (update || !this.boundingBox) this.computeBoundingBox(); + this.boundingBox.dim = this.boundingBox.max.clone().sub(this.boundingBox.min); + return this.boundingBox; + }; + + GP.unitScale = function(unit) { + var bbox = this.getBoundingBox(), + scale = unit || 1; + this.applyMatrix( + new THREE.Matrix3().identity().multiplyScalar(scale / + Math.max( + bbox.max.x - bbox.min.x, + bbox.max.y - bbox.min.y, + bbox.max.z - bbox.min.z + ) + ) + ); + // force update of (obsolete) bounding box + this.getBoundingBox(true); + return this; + }; + + BP.center = GP.center; + BP.getBoundingBox = GP.getBoundingBox; + BP.unitScale = GP.unitScale; + + BP.fixNormals = function() { + this.computeVertexNormals(); + return this; + }; + + THREE.Geometry.fromVertices = function(vertices) { + var geometry = new THREE.BufferGeometry(); + geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3)); + return geometry; + }; + + THREE.Object3D.prototype.newGroup = function() { + var group = new THREE.Group(); + this.add(group); + return group; + }; + + THREE.Object3D.prototype.removeAll = function() { + this.children.slice().forEach(function (c) { + c.parent = undefined; + c.dispatchEvent( { type: 'removed' } ); + }); + this.children = []; + }; + +})(); diff --git a/js/ext-clip.js b/js/ext-clip.js new file mode 100755 index 00000000..347f6605 --- /dev/null +++ b/js/ext-clip.js @@ -0,0 +1,6986 @@ +// rev 452 +/******************************************************************************** + * * + * Author : Angus Johnson * + * Version : 6.1.3a * + * Date : 22 January 2014 * + * Website : http://www.angusj.com * + * Copyright : Angus Johnson 2010-2014 * + * * + * License: * + * Use, modification & distribution is subject to Boost Software License Ver 1. * + * http://www.boost.org/LICENSE_1_0.txt * + * * + * Attributions: * + * The code in this library is an extension of Bala Vatti's clipping algorithm: * + * "A generic solution to polygon clipping" * + * Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. * + * http://portal.acm.org/citation.cfm?id=129906 * + * * + * Computer graphics and geometric modeling: implementation and algorithms * + * By Max K. Agoston * + * Springer; 1 edition (January 4, 2005) * + * http://books.google.com/books?q=vatti+clipping+agoston * + * * + * See also: * + * "Polygon Offsetting by Computing Winding Numbers" * + * Paper no. DETC2005-85513 pp. 565-575 * + * ASME 2005 International Design Engineering Technical Conferences * + * and Computers and Information in Engineering Conference (IDETC/CIE2005) * + * September 24-28, 2005 , Long Beach, California, USA * + * http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf * + * * + *******************************************************************************/ +/******************************************************************************* + * * + * Author : Timo * + * Version : 6.1.3.2 * + * Date : 1 February 2014 * + * * + * This is a translation of the C# Clipper library to Javascript. * + * Int128 struct of C# is implemented using JSBN of Tom Wu. * + * Because Javascript lacks support for 64-bit integers, the space * + * is a little more restricted than in C# version. * + * * + * C# version has support for coordinate space: * + * +-4611686018427387903 ( sqrt(2^127 -1)/2 ) * + * while Javascript version has support for space: * + * +-4503599627370495 ( sqrt(2^106 -1)/2 ) * + * * + * Tom Wu's JSBN proved to be the fastest big integer library: * + * http://jsperf.com/big-integer-library-test * + * * + * This class can be made simpler when (if ever) 64-bit integer support comes. * + * * + *******************************************************************************/ +/******************************************************************************* + * * + * Basic JavaScript BN library - subset useful for RSA encryption. * + * http://www-cs-students.stanford.edu/~tjw/jsbn/ * + * Copyright (c) 2005 Tom Wu * + * All Rights Reserved. * + * See "LICENSE" for details: * + * http://www-cs-students.stanford.edu/~tjw/jsbn/LICENSE * + * * + *******************************************************************************/ +(function () +{ + "use strict"; + // disable modules when conflicts with others + var use_modules = false; + //use_int32: When enabled 32bit ints are used instead of 64bit ints. This + //improve performance but coordinate values are limited to the range +/- 46340 + var use_int32 = false; + //use_xyz: adds a Z member to IntPoint. Adds a minor cost to performance. + var use_xyz = false; + //UseLines: Enables line clipping. Adds a very minor cost to performance. + var use_lines = true; + //use_deprecated: Enables support for the obsolete OffsetPaths() function + //which has been replace with the ClipperOffset class. + var use_deprecated = false; + + var ClipperLib = {}; + var isNode = false; + if (use_modules && typeof module !== 'undefined' && module.exports) + { + module.exports = ClipperLib; + isNode = true; + } + else + { + self.ClipperLib = ClipperLib; + } + var navigator_appName; + if (!isNode) + { + var nav = navigator.userAgent.toString().toLowerCase(); + navigator_appName = navigator.appName; + } + else + { + var nav = "chrome"; // Node.js uses Chrome's V8 engine + navigator_appName = "Netscape"; // Firefox, Chrome and Safari returns "Netscape", so Node.js should also + } + // Browser test to speedup performance critical functions + var browser = {}; + if (nav.indexOf("chrome") != -1 && nav.indexOf("chromium") == -1) browser.chrome = 1; + else browser.chrome = 0; + if (nav.indexOf("chromium") != -1) browser.chromium = 1; + else browser.chromium = 0; + if (nav.indexOf("safari") != -1 && nav.indexOf("chrome") == -1 && nav.indexOf("chromium") == -1) browser.safari = 1; + else browser.safari = 0; + if (nav.indexOf("firefox") != -1) browser.firefox = 1; + else browser.firefox = 0; + if (nav.indexOf("firefox/17") != -1) browser.firefox17 = 1; + else browser.firefox17 = 0; + if (nav.indexOf("firefox/15") != -1) browser.firefox15 = 1; + else browser.firefox15 = 0; + if (nav.indexOf("firefox/3") != -1) browser.firefox3 = 1; + else browser.firefox3 = 0; + if (nav.indexOf("opera") != -1) browser.opera = 1; + else browser.opera = 0; + if (nav.indexOf("msie 10") != -1) browser.msie10 = 1; + else browser.msie10 = 0; + if (nav.indexOf("msie 9") != -1) browser.msie9 = 1; + else browser.msie9 = 0; + if (nav.indexOf("msie 8") != -1) browser.msie8 = 1; + else browser.msie8 = 0; + if (nav.indexOf("msie 7") != -1) browser.msie7 = 1; + else browser.msie7 = 0; + if (nav.indexOf("msie ") != -1) browser.msie = 1; + else browser.msie = 0; + ClipperLib.biginteger_used = null; + // Copyright (c) 2005 Tom Wu + // All Rights Reserved. + // See "LICENSE" for details. + // Basic JavaScript BN library - subset useful for RSA encryption. + // Bits per digit + var dbits; + // JavaScript engine analysis + var canary = 0xdeadbeefcafe; + var j_lm = ((canary & 0xffffff) == 0xefcafe); + // (public) Constructor + function BigInteger(a, b, c) + { + // This test variable can be removed, + // but at least for performance tests it is useful piece of knowledge + // This is the only ClipperLib related variable in BigInteger library + ClipperLib.biginteger_used = 1; + if (a != null) + if ("number" == typeof a && "undefined" == typeof (b)) this.fromInt(a); // faster conversion + else if ("number" == typeof a) this.fromNumber(a, b, c); + else if (b == null && "string" != typeof a) this.fromString(a, 256); + else this.fromString(a, b); + } + // return new, unset BigInteger + function nbi() + { + return new BigInteger(null); + } + // am: Compute w_j += (x*this_i), propagate carries, + // c is initial carry, returns final carry. + // c < 3*dvalue, x < 2*dvalue, this_i < dvalue + // We need to select the fastest one that works in this environment. + // am1: use a single mult and divide to get the high bits, + // max digit bits should be 26 because + // max internal value = 2*dvalue^2-2*dvalue (< 2^53) + function am1(i, x, w, j, c, n) + { + while (--n >= 0) + { + var v = x * this[i++] + w[j] + c; + c = Math.floor(v / 0x4000000); + w[j++] = v & 0x3ffffff; + } + return c; + } + // am2 avoids a big mult-and-extract completely. + // Max digit bits should be <= 30 because we do bitwise ops + // on values up to 2*hdvalue^2-hdvalue-1 (< 2^31) + function am2(i, x, w, j, c, n) + { + var xl = x & 0x7fff, + xh = x >> 15; + while (--n >= 0) + { + var l = this[i] & 0x7fff; + var h = this[i++] >> 15; + var m = xh * l + h * xl; + l = xl * l + ((m & 0x7fff) << 15) + w[j] + (c & 0x3fffffff); + c = (l >>> 30) + (m >>> 15) + xh * h + (c >>> 30); + w[j++] = l & 0x3fffffff; + } + return c; + } + // Alternately, set max digit bits to 28 since some + // browsers slow down when dealing with 32-bit numbers. + function am3(i, x, w, j, c, n) + { + var xl = x & 0x3fff, + xh = x >> 14; + while (--n >= 0) + { + var l = this[i] & 0x3fff; + var h = this[i++] >> 14; + var m = xh * l + h * xl; + l = xl * l + ((m & 0x3fff) << 14) + w[j] + c; + c = (l >> 28) + (m >> 14) + xh * h; + w[j++] = l & 0xfffffff; + } + return c; + } + if (j_lm && (navigator_appName == "Microsoft Internet Explorer")) + { + BigInteger.prototype.am = am2; + dbits = 30; + } + else if (j_lm && (navigator_appName != "Netscape")) + { + BigInteger.prototype.am = am1; + dbits = 26; + } + else + { // Mozilla/Netscape seems to prefer am3 + BigInteger.prototype.am = am3; + dbits = 28; + } + BigInteger.prototype.DB = dbits; + BigInteger.prototype.DM = ((1 << dbits) - 1); + BigInteger.prototype.DV = (1 << dbits); + var BI_FP = 52; + BigInteger.prototype.FV = Math.pow(2, BI_FP); + BigInteger.prototype.F1 = BI_FP - dbits; + BigInteger.prototype.F2 = 2 * dbits - BI_FP; + // Digit conversions + var BI_RM = "0123456789abcdefghijklmnopqrstuvwxyz"; + var BI_RC = new Array(); + var rr, vv; + rr = "0".charCodeAt(0); + for (vv = 0; vv <= 9; ++vv) BI_RC[rr++] = vv; + rr = "a".charCodeAt(0); + for (vv = 10; vv < 36; ++vv) BI_RC[rr++] = vv; + rr = "A".charCodeAt(0); + for (vv = 10; vv < 36; ++vv) BI_RC[rr++] = vv; + + function int2char(n) + { + return BI_RM.charAt(n); + } + + function intAt(s, i) + { + var c = BI_RC[s.charCodeAt(i)]; + return (c == null) ? -1 : c; + } + // (protected) copy this to r + function bnpCopyTo(r) + { + for (var i = this.t - 1; i >= 0; --i) r[i] = this[i]; + r.t = this.t; + r.s = this.s; + } + // (protected) set from integer value x, -DV <= x < DV + function bnpFromInt(x) + { + this.t = 1; + this.s = (x < 0) ? -1 : 0; + if (x > 0) this[0] = x; + else if (x < -1) this[0] = x + this.DV; + else this.t = 0; + } + // return bigint initialized to value + function nbv(i) + { + var r = nbi(); + r.fromInt(i); + return r; + } + // (protected) set from string and radix + function bnpFromString(s, b) + { + var k; + if (b == 16) k = 4; + else if (b == 8) k = 3; + else if (b == 256) k = 8; // byte array + else if (b == 2) k = 1; + else if (b == 32) k = 5; + else if (b == 4) k = 2; + else + { + this.fromRadix(s, b); + return; + } + this.t = 0; + this.s = 0; + var i = s.length, + mi = false, + sh = 0; + while (--i >= 0) + { + var x = (k == 8) ? s[i] & 0xff : intAt(s, i); + if (x < 0) + { + if (s.charAt(i) == "-") mi = true; + continue; + } + mi = false; + if (sh == 0) + this[this.t++] = x; + else if (sh + k > this.DB) + { + this[this.t - 1] |= (x & ((1 << (this.DB - sh)) - 1)) << sh; + this[this.t++] = (x >> (this.DB - sh)); + } + else + this[this.t - 1] |= x << sh; + sh += k; + if (sh >= this.DB) sh -= this.DB; + } + if (k == 8 && (s[0] & 0x80) != 0) + { + this.s = -1; + if (sh > 0) this[this.t - 1] |= ((1 << (this.DB - sh)) - 1) << sh; + } + this.clamp(); + if (mi) BigInteger.ZERO.subTo(this, this); + } + // (protected) clamp off excess high words + function bnpClamp() + { + var c = this.s & this.DM; + while (this.t > 0 && this[this.t - 1] == c)--this.t; + } + // (public) return string representation in given radix + function bnToString(b) + { + if (this.s < 0) return "-" + this.negate().toString(b); + var k; + if (b == 16) k = 4; + else if (b == 8) k = 3; + else if (b == 2) k = 1; + else if (b == 32) k = 5; + else if (b == 4) k = 2; + else return this.toRadix(b); + var km = (1 << k) - 1, + d, m = false, + r = "", + i = this.t; + var p = this.DB - (i * this.DB) % k; + if (i-- > 0) + { + if (p < this.DB && (d = this[i] >> p) > 0) + { + m = true; + r = int2char(d); + } + while (i >= 0) + { + if (p < k) + { + d = (this[i] & ((1 << p) - 1)) << (k - p); + d |= this[--i] >> (p += this.DB - k); + } + else + { + d = (this[i] >> (p -= k)) & km; + if (p <= 0) + { + p += this.DB; + --i; + } + } + if (d > 0) m = true; + if (m) r += int2char(d); + } + } + return m ? r : "0"; + } + // (public) -this + function bnNegate() + { + var r = nbi(); + BigInteger.ZERO.subTo(this, r); + return r; + } + // (public) |this| + function bnAbs() + { + return (this.s < 0) ? this.negate() : this; + } + // (public) return + if this > a, - if this < a, 0 if equal + function bnCompareTo(a) + { + var r = this.s - a.s; + if (r != 0) return r; + var i = this.t; + r = i - a.t; + if (r != 0) return (this.s < 0) ? -r : r; + while (--i >= 0) + if ((r = this[i] - a[i]) != 0) return r; + return 0; + } + // returns bit length of the integer x + function nbits(x) + { + var r = 1, + t; + if ((t = x >>> 16) != 0) + { + x = t; + r += 16; + } + if ((t = x >> 8) != 0) + { + x = t; + r += 8; + } + if ((t = x >> 4) != 0) + { + x = t; + r += 4; + } + if ((t = x >> 2) != 0) + { + x = t; + r += 2; + } + if ((t = x >> 1) != 0) + { + x = t; + r += 1; + } + return r; + } + // (public) return the number of bits in "this" + function bnBitLength() + { + if (this.t <= 0) return 0; + return this.DB * (this.t - 1) + nbits(this[this.t - 1] ^ (this.s & this.DM)); + } + // (protected) r = this << n*DB + function bnpDLShiftTo(n, r) + { + var i; + for (i = this.t - 1; i >= 0; --i) r[i + n] = this[i]; + for (i = n - 1; i >= 0; --i) r[i] = 0; + r.t = this.t + n; + r.s = this.s; + } + // (protected) r = this >> n*DB + function bnpDRShiftTo(n, r) + { + for (var i = n; i < this.t; ++i) r[i - n] = this[i]; + r.t = Math.max(this.t - n, 0); + r.s = this.s; + } + // (protected) r = this << n + function bnpLShiftTo(n, r) + { + var bs = n % this.DB; + var cbs = this.DB - bs; + var bm = (1 << cbs) - 1; + var ds = Math.floor(n / this.DB), + c = (this.s << bs) & this.DM, + i; + for (i = this.t - 1; i >= 0; --i) + { + r[i + ds + 1] = (this[i] >> cbs) | c; + c = (this[i] & bm) << bs; + } + for (i = ds - 1; i >= 0; --i) r[i] = 0; + r[ds] = c; + r.t = this.t + ds + 1; + r.s = this.s; + r.clamp(); + } + // (protected) r = this >> n + function bnpRShiftTo(n, r) + { + r.s = this.s; + var ds = Math.floor(n / this.DB); + if (ds >= this.t) + { + r.t = 0; + return; + } + var bs = n % this.DB; + var cbs = this.DB - bs; + var bm = (1 << bs) - 1; + r[0] = this[ds] >> bs; + for (var i = ds + 1; i < this.t; ++i) + { + r[i - ds - 1] |= (this[i] & bm) << cbs; + r[i - ds] = this[i] >> bs; + } + if (bs > 0) r[this.t - ds - 1] |= (this.s & bm) << cbs; + r.t = this.t - ds; + r.clamp(); + } + // (protected) r = this - a + function bnpSubTo(a, r) + { + var i = 0, + c = 0, + m = Math.min(a.t, this.t); + while (i < m) + { + c += this[i] - a[i]; + r[i++] = c & this.DM; + c >>= this.DB; + } + if (a.t < this.t) + { + c -= a.s; + while (i < this.t) + { + c += this[i]; + r[i++] = c & this.DM; + c >>= this.DB; + } + c += this.s; + } + else + { + c += this.s; + while (i < a.t) + { + c -= a[i]; + r[i++] = c & this.DM; + c >>= this.DB; + } + c -= a.s; + } + r.s = (c < 0) ? -1 : 0; + if (c < -1) r[i++] = this.DV + c; + else if (c > 0) r[i++] = c; + r.t = i; + r.clamp(); + } + // (protected) r = this * a, r != this,a (HAC 14.12) + // "this" should be the larger one if appropriate. + function bnpMultiplyTo(a, r) + { + var x = this.abs(), + y = a.abs(); + var i = x.t; + r.t = i + y.t; + while (--i >= 0) r[i] = 0; + for (i = 0; i < y.t; ++i) r[i + x.t] = x.am(0, y[i], r, i, 0, x.t); + r.s = 0; + r.clamp(); + if (this.s != a.s) BigInteger.ZERO.subTo(r, r); + } + // (protected) r = this^2, r != this (HAC 14.16) + function bnpSquareTo(r) + { + var x = this.abs(); + var i = r.t = 2 * x.t; + while (--i >= 0) r[i] = 0; + for (i = 0; i < x.t - 1; ++i) + { + var c = x.am(i, x[i], r, 2 * i, 0, 1); + if ((r[i + x.t] += x.am(i + 1, 2 * x[i], r, 2 * i + 1, c, x.t - i - 1)) >= x.DV) + { + r[i + x.t] -= x.DV; + r[i + x.t + 1] = 1; + } + } + if (r.t > 0) r[r.t - 1] += x.am(i, x[i], r, 2 * i, 0, 1); + r.s = 0; + r.clamp(); + } + // (protected) divide this by m, quotient and remainder to q, r (HAC 14.20) + // r != q, this != m. q or r may be null. + function bnpDivRemTo(m, q, r) + { + var pm = m.abs(); + if (pm.t <= 0) return; + var pt = this.abs(); + if (pt.t < pm.t) + { + if (q != null) q.fromInt(0); + if (r != null) this.copyTo(r); + return; + } + if (r == null) r = nbi(); + var y = nbi(), + ts = this.s, + ms = m.s; + var nsh = this.DB - nbits(pm[pm.t - 1]); // normalize modulus + if (nsh > 0) + { + pm.lShiftTo(nsh, y); + pt.lShiftTo(nsh, r); + } + else + { + pm.copyTo(y); + pt.copyTo(r); + } + var ys = y.t; + var y0 = y[ys - 1]; + if (y0 == 0) return; + var yt = y0 * (1 << this.F1) + ((ys > 1) ? y[ys - 2] >> this.F2 : 0); + var d1 = this.FV / yt, + d2 = (1 << this.F1) / yt, + e = 1 << this.F2; + var i = r.t, + j = i - ys, + t = (q == null) ? nbi() : q; + y.dlShiftTo(j, t); + if (r.compareTo(t) >= 0) + { + r[r.t++] = 1; + r.subTo(t, r); + } + BigInteger.ONE.dlShiftTo(ys, t); + t.subTo(y, y); // "negative" y so we can replace sub with am later + while (y.t < ys) y[y.t++] = 0; + while (--j >= 0) + { + // Estimate quotient digit + var qd = (r[--i] == y0) ? this.DM : Math.floor(r[i] * d1 + (r[i - 1] + e) * d2); + if ((r[i] += y.am(0, qd, r, j, 0, ys)) < qd) + { // Try it out + y.dlShiftTo(j, t); + r.subTo(t, r); + while (r[i] < --qd) r.subTo(t, r); + } + } + if (q != null) + { + r.drShiftTo(ys, q); + if (ts != ms) BigInteger.ZERO.subTo(q, q); + } + r.t = ys; + r.clamp(); + if (nsh > 0) r.rShiftTo(nsh, r); // Denormalize remainder + if (ts < 0) BigInteger.ZERO.subTo(r, r); + } + // (public) this mod a + function bnMod(a) + { + var r = nbi(); + this.abs().divRemTo(a, null, r); + if (this.s < 0 && r.compareTo(BigInteger.ZERO) > 0) a.subTo(r, r); + return r; + } + // Modular reduction using "classic" algorithm + function Classic(m) + { + this.m = m; + } + + function cConvert(x) + { + if (x.s < 0 || x.compareTo(this.m) >= 0) return x.mod(this.m); + else return x; + } + + function cRevert(x) + { + return x; + } + + function cReduce(x) + { + x.divRemTo(this.m, null, x); + } + + function cMulTo(x, y, r) + { + x.multiplyTo(y, r); + this.reduce(r); + } + + function cSqrTo(x, r) + { + x.squareTo(r); + this.reduce(r); + } + Classic.prototype.convert = cConvert; + Classic.prototype.revert = cRevert; + Classic.prototype.reduce = cReduce; + Classic.prototype.mulTo = cMulTo; + Classic.prototype.sqrTo = cSqrTo; + // (protected) return "-1/this % 2^DB"; useful for Mont. reduction + // justification: + // xy == 1 (mod m) + // xy = 1+km + // xy(2-xy) = (1+km)(1-km) + // x[y(2-xy)] = 1-k^2m^2 + // x[y(2-xy)] == 1 (mod m^2) + // if y is 1/x mod m, then y(2-xy) is 1/x mod m^2 + // should reduce x and y(2-xy) by m^2 at each step to keep size bounded. + // JS multiply "overflows" differently from C/C++, so care is needed here. + function bnpInvDigit() + { + if (this.t < 1) return 0; + var x = this[0]; + if ((x & 1) == 0) return 0; + var y = x & 3; // y == 1/x mod 2^2 + y = (y * (2 - (x & 0xf) * y)) & 0xf; // y == 1/x mod 2^4 + y = (y * (2 - (x & 0xff) * y)) & 0xff; // y == 1/x mod 2^8 + y = (y * (2 - (((x & 0xffff) * y) & 0xffff))) & 0xffff; // y == 1/x mod 2^16 + // last step - calculate inverse mod DV directly; + // assumes 16 < DB <= 32 and assumes ability to handle 48-bit ints + y = (y * (2 - x * y % this.DV)) % this.DV; // y == 1/x mod 2^dbits + // we really want the negative inverse, and -DV < y < DV + return (y > 0) ? this.DV - y : -y; + } + // Montgomery reduction + function Montgomery(m) + { + this.m = m; + this.mp = m.invDigit(); + this.mpl = this.mp & 0x7fff; + this.mph = this.mp >> 15; + this.um = (1 << (m.DB - 15)) - 1; + this.mt2 = 2 * m.t; + } + // xR mod m + function montConvert(x) + { + var r = nbi(); + x.abs().dlShiftTo(this.m.t, r); + r.divRemTo(this.m, null, r); + if (x.s < 0 && r.compareTo(BigInteger.ZERO) > 0) this.m.subTo(r, r); + return r; + } + // x/R mod m + function montRevert(x) + { + var r = nbi(); + x.copyTo(r); + this.reduce(r); + return r; + } + // x = x/R mod m (HAC 14.32) + function montReduce(x) + { + while (x.t <= this.mt2) // pad x so am has enough room later + x[x.t++] = 0; + for (var i = 0; i < this.m.t; ++i) + { + // faster way of calculating u0 = x[i]*mp mod DV + var j = x[i] & 0x7fff; + var u0 = (j * this.mpl + (((j * this.mph + (x[i] >> 15) * this.mpl) & this.um) << 15)) & x.DM; + // use am to combine the multiply-shift-add into one call + j = i + this.m.t; + x[j] += this.m.am(0, u0, x, i, 0, this.m.t); + // propagate carry + while (x[j] >= x.DV) + { + x[j] -= x.DV; + x[++j]++; + } + } + x.clamp(); + x.drShiftTo(this.m.t, x); + if (x.compareTo(this.m) >= 0) x.subTo(this.m, x); + } + // r = "x^2/R mod m"; x != r + function montSqrTo(x, r) + { + x.squareTo(r); + this.reduce(r); + } + // r = "xy/R mod m"; x,y != r + function montMulTo(x, y, r) + { + x.multiplyTo(y, r); + this.reduce(r); + } + Montgomery.prototype.convert = montConvert; + Montgomery.prototype.revert = montRevert; + Montgomery.prototype.reduce = montReduce; + Montgomery.prototype.mulTo = montMulTo; + Montgomery.prototype.sqrTo = montSqrTo; + // (protected) true iff this is even + function bnpIsEven() + { + return ((this.t > 0) ? (this[0] & 1) : this.s) == 0; + } + // (protected) this^e, e < 2^32, doing sqr and mul with "r" (HAC 14.79) + function bnpExp(e, z) + { + if (e > 0xffffffff || e < 1) return BigInteger.ONE; + var r = nbi(), + r2 = nbi(), + g = z.convert(this), + i = nbits(e) - 1; + g.copyTo(r); + while (--i >= 0) + { + z.sqrTo(r, r2); + if ((e & (1 << i)) > 0) z.mulTo(r2, g, r); + else + { + var t = r; + r = r2; + r2 = t; + } + } + return z.revert(r); + } + // (public) this^e % m, 0 <= e < 2^32 + function bnModPowInt(e, m) + { + var z; + if (e < 256 || m.isEven()) z = new Classic(m); + else z = new Montgomery(m); + return this.exp(e, z); + } + // protected + BigInteger.prototype.copyTo = bnpCopyTo; + BigInteger.prototype.fromInt = bnpFromInt; + BigInteger.prototype.fromString = bnpFromString; + BigInteger.prototype.clamp = bnpClamp; + BigInteger.prototype.dlShiftTo = bnpDLShiftTo; + BigInteger.prototype.drShiftTo = bnpDRShiftTo; + BigInteger.prototype.lShiftTo = bnpLShiftTo; + BigInteger.prototype.rShiftTo = bnpRShiftTo; + BigInteger.prototype.subTo = bnpSubTo; + BigInteger.prototype.multiplyTo = bnpMultiplyTo; + BigInteger.prototype.squareTo = bnpSquareTo; + BigInteger.prototype.divRemTo = bnpDivRemTo; + BigInteger.prototype.invDigit = bnpInvDigit; + BigInteger.prototype.isEven = bnpIsEven; + BigInteger.prototype.exp = bnpExp; + // public + BigInteger.prototype.toString = bnToString; + BigInteger.prototype.negate = bnNegate; + BigInteger.prototype.abs = bnAbs; + BigInteger.prototype.compareTo = bnCompareTo; + BigInteger.prototype.bitLength = bnBitLength; + BigInteger.prototype.mod = bnMod; + BigInteger.prototype.modPowInt = bnModPowInt; + // "constants" + BigInteger.ZERO = nbv(0); + BigInteger.ONE = nbv(1); + // Copyright (c) 2005-2009 Tom Wu + // All Rights Reserved. + // See "LICENSE" for details. + // Extended JavaScript BN functions, required for RSA private ops. + // Version 1.1: new BigInteger("0", 10) returns "proper" zero + // Version 1.2: square() API, isProbablePrime fix + // (public) + function bnClone() + { + var r = nbi(); + this.copyTo(r); + return r; + } + // (public) return value as integer + function bnIntValue() + { + if (this.s < 0) + { + if (this.t == 1) return this[0] - this.DV; + else if (this.t == 0) return -1; + } + else if (this.t == 1) return this[0]; + else if (this.t == 0) return 0; + // assumes 16 < DB < 32 + return ((this[1] & ((1 << (32 - this.DB)) - 1)) << this.DB) | this[0]; + } + // (public) return value as byte + function bnByteValue() + { + return (this.t == 0) ? this.s : (this[0] << 24) >> 24; + } + // (public) return value as short (assumes DB>=16) + function bnShortValue() + { + return (this.t == 0) ? this.s : (this[0] << 16) >> 16; + } + // (protected) return x s.t. r^x < DV + function bnpChunkSize(r) + { + return Math.floor(Math.LN2 * this.DB / Math.log(r)); + } + // (public) 0 if this == 0, 1 if this > 0 + function bnSigNum() + { + if (this.s < 0) return -1; + else if (this.t <= 0 || (this.t == 1 && this[0] <= 0)) return 0; + else return 1; + } + // (protected) convert to radix string + function bnpToRadix(b) + { + if (b == null) b = 10; + if (this.signum() == 0 || b < 2 || b > 36) return "0"; + var cs = this.chunkSize(b); + var a = Math.pow(b, cs); + var d = nbv(a), + y = nbi(), + z = nbi(), + r = ""; + this.divRemTo(d, y, z); + while (y.signum() > 0) + { + r = (a + z.intValue()).toString(b).substr(1) + r; + y.divRemTo(d, y, z); + } + return z.intValue().toString(b) + r; + } + // (protected) convert from radix string + function bnpFromRadix(s, b) + { + this.fromInt(0); + if (b == null) b = 10; + var cs = this.chunkSize(b); + var d = Math.pow(b, cs), + mi = false, + j = 0, + w = 0; + for (var i = 0; i < s.length; ++i) + { + var x = intAt(s, i); + if (x < 0) + { + if (s.charAt(i) == "-" && this.signum() == 0) mi = true; + continue; + } + w = b * w + x; + if (++j >= cs) + { + this.dMultiply(d); + this.dAddOffset(w, 0); + j = 0; + w = 0; + } + } + if (j > 0) + { + this.dMultiply(Math.pow(b, j)); + this.dAddOffset(w, 0); + } + if (mi) BigInteger.ZERO.subTo(this, this); + } + // (protected) alternate constructor + function bnpFromNumber(a, b, c) + { + if ("number" == typeof b) + { + // new BigInteger(int,int,RNG) + if (a < 2) this.fromInt(1); + else + { + this.fromNumber(a, c); + if (!this.testBit(a - 1)) // force MSB set + this.bitwiseTo(BigInteger.ONE.shiftLeft(a - 1), op_or, this); + if (this.isEven()) this.dAddOffset(1, 0); // force odd + while (!this.isProbablePrime(b)) + { + this.dAddOffset(2, 0); + if (this.bitLength() > a) this.subTo(BigInteger.ONE.shiftLeft(a - 1), this); + } + } + } + else + { + // new BigInteger(int,RNG) + var x = new Array(), + t = a & 7; + x.length = (a >> 3) + 1; + b.nextBytes(x); + if (t > 0) x[0] &= ((1 << t) - 1); + else x[0] = 0; + this.fromString(x, 256); + } + } + // (public) convert to bigendian byte array + function bnToByteArray() + { + var i = this.t, + r = new Array(); + r[0] = this.s; + var p = this.DB - (i * this.DB) % 8, + d, k = 0; + if (i-- > 0) + { + if (p < this.DB && (d = this[i] >> p) != (this.s & this.DM) >> p) + r[k++] = d | (this.s << (this.DB - p)); + while (i >= 0) + { + if (p < 8) + { + d = (this[i] & ((1 << p) - 1)) << (8 - p); + d |= this[--i] >> (p += this.DB - 8); + } + else + { + d = (this[i] >> (p -= 8)) & 0xff; + if (p <= 0) + { + p += this.DB; + --i; + } + } + if ((d & 0x80) != 0) d |= -256; + if (k == 0 && (this.s & 0x80) != (d & 0x80))++k; + if (k > 0 || d != this.s) r[k++] = d; + } + } + return r; + } + + function bnEquals(a) + { + return (this.compareTo(a) == 0); + } + + function bnMin(a) + { + return (this.compareTo(a) < 0) ? this : a; + } + + function bnMax(a) + { + return (this.compareTo(a) > 0) ? this : a; + } + // (protected) r = this op a (bitwise) + function bnpBitwiseTo(a, op, r) + { + var i, f, m = Math.min(a.t, this.t); + for (i = 0; i < m; ++i) r[i] = op(this[i], a[i]); + if (a.t < this.t) + { + f = a.s & this.DM; + for (i = m; i < this.t; ++i) r[i] = op(this[i], f); + r.t = this.t; + } + else + { + f = this.s & this.DM; + for (i = m; i < a.t; ++i) r[i] = op(f, a[i]); + r.t = a.t; + } + r.s = op(this.s, a.s); + r.clamp(); + } + // (public) this & a + function op_and(x, y) + { + return x & y; + } + + function bnAnd(a) + { + var r = nbi(); + this.bitwiseTo(a, op_and, r); + return r; + } + // (public) this | a + function op_or(x, y) + { + return x | y; + } + + function bnOr(a) + { + var r = nbi(); + this.bitwiseTo(a, op_or, r); + return r; + } + // (public) this ^ a + function op_xor(x, y) + { + return x ^ y; + } + + function bnXor(a) + { + var r = nbi(); + this.bitwiseTo(a, op_xor, r); + return r; + } + // (public) this & ~a + function op_andnot(x, y) + { + return x & ~y; + } + + function bnAndNot(a) + { + var r = nbi(); + this.bitwiseTo(a, op_andnot, r); + return r; + } + // (public) ~this + function bnNot() + { + var r = nbi(); + for (var i = 0; i < this.t; ++i) r[i] = this.DM & ~this[i]; + r.t = this.t; + r.s = ~this.s; + return r; + } + // (public) this << n + function bnShiftLeft(n) + { + var r = nbi(); + if (n < 0) this.rShiftTo(-n, r); + else this.lShiftTo(n, r); + return r; + } + // (public) this >> n + function bnShiftRight(n) + { + var r = nbi(); + if (n < 0) this.lShiftTo(-n, r); + else this.rShiftTo(n, r); + return r; + } + // return index of lowest 1-bit in x, x < 2^31 + function lbit(x) + { + if (x == 0) return -1; + var r = 0; + if ((x & 0xffff) == 0) + { + x >>= 16; + r += 16; + } + if ((x & 0xff) == 0) + { + x >>= 8; + r += 8; + } + if ((x & 0xf) == 0) + { + x >>= 4; + r += 4; + } + if ((x & 3) == 0) + { + x >>= 2; + r += 2; + } + if ((x & 1) == 0)++r; + return r; + } + // (public) returns index of lowest 1-bit (or -1 if none) + function bnGetLowestSetBit() + { + for (var i = 0; i < this.t; ++i) + if (this[i] != 0) return i * this.DB + lbit(this[i]); + if (this.s < 0) return this.t * this.DB; + return -1; + } + // return number of 1 bits in x + function cbit(x) + { + var r = 0; + while (x != 0) + { + x &= x - 1; + ++r; + } + return r; + } + // (public) return number of set bits + function bnBitCount() + { + var r = 0, + x = this.s & this.DM; + for (var i = 0; i < this.t; ++i) r += cbit(this[i] ^ x); + return r; + } + // (public) true iff nth bit is set + function bnTestBit(n) + { + var j = Math.floor(n / this.DB); + if (j >= this.t) return (this.s != 0); + return ((this[j] & (1 << (n % this.DB))) != 0); + } + // (protected) this op (1<>= this.DB; + } + if (a.t < this.t) + { + c += a.s; + while (i < this.t) + { + c += this[i]; + r[i++] = c & this.DM; + c >>= this.DB; + } + c += this.s; + } + else + { + c += this.s; + while (i < a.t) + { + c += a[i]; + r[i++] = c & this.DM; + c >>= this.DB; + } + c += a.s; + } + r.s = (c < 0) ? -1 : 0; + if (c > 0) r[i++] = c; + else if (c < -1) r[i++] = this.DV + c; + r.t = i; + r.clamp(); + } + // (public) this + a + function bnAdd(a) + { + var r = nbi(); + this.addTo(a, r); + return r; + } + // (public) this - a + function bnSubtract(a) + { + var r = nbi(); + this.subTo(a, r); + return r; + } + // (public) this * a + function bnMultiply(a) + { + var r = nbi(); + this.multiplyTo(a, r); + return r; + } + // (public) this^2 + function bnSquare() + { + var r = nbi(); + this.squareTo(r); + return r; + } + // (public) this / a + function bnDivide(a) + { + var r = nbi(); + this.divRemTo(a, r, null); + return r; + } + // (public) this % a + function bnRemainder(a) + { + var r = nbi(); + this.divRemTo(a, null, r); + return r; + } + // (public) [this/a,this%a] + function bnDivideAndRemainder(a) + { + var q = nbi(), + r = nbi(); + this.divRemTo(a, q, r); + return new Array(q, r); + } + // (protected) this *= n, this >= 0, 1 < n < DV + function bnpDMultiply(n) + { + this[this.t] = this.am(0, n - 1, this, 0, 0, this.t); + ++this.t; + this.clamp(); + } + // (protected) this += n << w words, this >= 0 + function bnpDAddOffset(n, w) + { + if (n == 0) return; + while (this.t <= w) this[this.t++] = 0; + this[w] += n; + while (this[w] >= this.DV) + { + this[w] -= this.DV; + if (++w >= this.t) this[this.t++] = 0; + ++this[w]; + } + } + // A "null" reducer + function NullExp() + {} + + function nNop(x) + { + return x; + } + + function nMulTo(x, y, r) + { + x.multiplyTo(y, r); + } + + function nSqrTo(x, r) + { + x.squareTo(r); + } + NullExp.prototype.convert = nNop; + NullExp.prototype.revert = nNop; + NullExp.prototype.mulTo = nMulTo; + NullExp.prototype.sqrTo = nSqrTo; + // (public) this^e + function bnPow(e) + { + return this.exp(e, new NullExp()); + } + // (protected) r = lower n words of "this * a", a.t <= n + // "this" should be the larger one if appropriate. + function bnpMultiplyLowerTo(a, n, r) + { + var i = Math.min(this.t + a.t, n); + r.s = 0; // assumes a,this >= 0 + r.t = i; + while (i > 0) r[--i] = 0; + var j; + for (j = r.t - this.t; i < j; ++i) r[i + this.t] = this.am(0, a[i], r, i, 0, this.t); + for (j = Math.min(a.t, n); i < j; ++i) this.am(0, a[i], r, i, 0, n - i); + r.clamp(); + } + // (protected) r = "this * a" without lower n words, n > 0 + // "this" should be the larger one if appropriate. + function bnpMultiplyUpperTo(a, n, r) + { + --n; + var i = r.t = this.t + a.t - n; + r.s = 0; // assumes a,this >= 0 + while (--i >= 0) r[i] = 0; + for (i = Math.max(n - this.t, 0); i < a.t; ++i) + r[this.t + i - n] = this.am(n - i, a[i], r, 0, 0, this.t + i - n); + r.clamp(); + r.drShiftTo(1, r); + } + // Barrett modular reduction + function Barrett(m) + { + // setup Barrett + this.r2 = nbi(); + this.q3 = nbi(); + BigInteger.ONE.dlShiftTo(2 * m.t, this.r2); + this.mu = this.r2.divide(m); + this.m = m; + } + + function barrettConvert(x) + { + if (x.s < 0 || x.t > 2 * this.m.t) return x.mod(this.m); + else if (x.compareTo(this.m) < 0) return x; + else + { + var r = nbi(); + x.copyTo(r); + this.reduce(r); + return r; + } + } + + function barrettRevert(x) + { + return x; + } + // x = x mod m (HAC 14.42) + function barrettReduce(x) + { + x.drShiftTo(this.m.t - 1, this.r2); + if (x.t > this.m.t + 1) + { + x.t = this.m.t + 1; + x.clamp(); + } + this.mu.multiplyUpperTo(this.r2, this.m.t + 1, this.q3); + this.m.multiplyLowerTo(this.q3, this.m.t + 1, this.r2); + while (x.compareTo(this.r2) < 0) x.dAddOffset(1, this.m.t + 1); + x.subTo(this.r2, x); + while (x.compareTo(this.m) >= 0) x.subTo(this.m, x); + } + // r = x^2 mod m; x != r + function barrettSqrTo(x, r) + { + x.squareTo(r); + this.reduce(r); + } + // r = x*y mod m; x,y != r + function barrettMulTo(x, y, r) + { + x.multiplyTo(y, r); + this.reduce(r); + } + Barrett.prototype.convert = barrettConvert; + Barrett.prototype.revert = barrettRevert; + Barrett.prototype.reduce = barrettReduce; + Barrett.prototype.mulTo = barrettMulTo; + Barrett.prototype.sqrTo = barrettSqrTo; + // (public) this^e % m (HAC 14.85) + function bnModPow(e, m) + { + var i = e.bitLength(), + k, r = nbv(1), + z; + if (i <= 0) return r; + else if (i < 18) k = 1; + else if (i < 48) k = 3; + else if (i < 144) k = 4; + else if (i < 768) k = 5; + else k = 6; + if (i < 8) + z = new Classic(m); + else if (m.isEven()) + z = new Barrett(m); + else + z = new Montgomery(m); + // precomputation + var g = new Array(), + n = 3, + k1 = k - 1, + km = (1 << k) - 1; + g[1] = z.convert(this); + if (k > 1) + { + var g2 = nbi(); + z.sqrTo(g[1], g2); + while (n <= km) + { + g[n] = nbi(); + z.mulTo(g2, g[n - 2], g[n]); + n += 2; + } + } + var j = e.t - 1, + w, is1 = true, + r2 = nbi(), + t; + i = nbits(e[j]) - 1; + while (j >= 0) + { + if (i >= k1) w = (e[j] >> (i - k1)) & km; + else + { + w = (e[j] & ((1 << (i + 1)) - 1)) << (k1 - i); + if (j > 0) w |= e[j - 1] >> (this.DB + i - k1); + } + n = k; + while ((w & 1) == 0) + { + w >>= 1; + --n; + } + if ((i -= n) < 0) + { + i += this.DB; + --j; + } + if (is1) + { // ret == 1, don't bother squaring or multiplying it + g[w].copyTo(r); + is1 = false; + } + else + { + while (n > 1) + { + z.sqrTo(r, r2); + z.sqrTo(r2, r); + n -= 2; + } + if (n > 0) z.sqrTo(r, r2); + else + { + t = r; + r = r2; + r2 = t; + } + z.mulTo(r2, g[w], r); + } + while (j >= 0 && (e[j] & (1 << i)) == 0) + { + z.sqrTo(r, r2); + t = r; + r = r2; + r2 = t; + if (--i < 0) + { + i = this.DB - 1; + --j; + } + } + } + return z.revert(r); + } + // (public) gcd(this,a) (HAC 14.54) + function bnGCD(a) + { + var x = (this.s < 0) ? this.negate() : this.clone(); + var y = (a.s < 0) ? a.negate() : a.clone(); + if (x.compareTo(y) < 0) + { + var t = x; + x = y; + y = t; + } + var i = x.getLowestSetBit(), + g = y.getLowestSetBit(); + if (g < 0) return x; + if (i < g) g = i; + if (g > 0) + { + x.rShiftTo(g, x); + y.rShiftTo(g, y); + } + while (x.signum() > 0) + { + if ((i = x.getLowestSetBit()) > 0) x.rShiftTo(i, x); + if ((i = y.getLowestSetBit()) > 0) y.rShiftTo(i, y); + if (x.compareTo(y) >= 0) + { + x.subTo(y, x); + x.rShiftTo(1, x); + } + else + { + y.subTo(x, y); + y.rShiftTo(1, y); + } + } + if (g > 0) y.lShiftTo(g, y); + return y; + } + // (protected) this % n, n < 2^26 + function bnpModInt(n) + { + if (n <= 0) return 0; + var d = this.DV % n, + r = (this.s < 0) ? n - 1 : 0; + if (this.t > 0) + if (d == 0) r = this[0] % n; + else + for (var i = this.t - 1; i >= 0; --i) r = (d * r + this[i]) % n; + return r; + } + // (public) 1/this % m (HAC 14.61) + function bnModInverse(m) + { + var ac = m.isEven(); + if ((this.isEven() && ac) || m.signum() == 0) return BigInteger.ZERO; + var u = m.clone(), + v = this.clone(); + var a = nbv(1), + b = nbv(0), + c = nbv(0), + d = nbv(1); + while (u.signum() != 0) + { + while (u.isEven()) + { + u.rShiftTo(1, u); + if (ac) + { + if (!a.isEven() || !b.isEven()) + { + a.addTo(this, a); + b.subTo(m, b); + } + a.rShiftTo(1, a); + } + else if (!b.isEven()) b.subTo(m, b); + b.rShiftTo(1, b); + } + while (v.isEven()) + { + v.rShiftTo(1, v); + if (ac) + { + if (!c.isEven() || !d.isEven()) + { + c.addTo(this, c); + d.subTo(m, d); + } + c.rShiftTo(1, c); + } + else if (!d.isEven()) d.subTo(m, d); + d.rShiftTo(1, d); + } + if (u.compareTo(v) >= 0) + { + u.subTo(v, u); + if (ac) a.subTo(c, a); + b.subTo(d, b); + } + else + { + v.subTo(u, v); + if (ac) c.subTo(a, c); + d.subTo(b, d); + } + } + if (v.compareTo(BigInteger.ONE) != 0) return BigInteger.ZERO; + if (d.compareTo(m) >= 0) return d.subtract(m); + if (d.signum() < 0) d.addTo(m, d); + else return d; + if (d.signum() < 0) return d.add(m); + else return d; + } + var lowprimes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997]; + var lplim = (1 << 26) / lowprimes[lowprimes.length - 1]; + // (public) test primality with certainty >= 1-.5^t + function bnIsProbablePrime(t) + { + var i, x = this.abs(); + if (x.t == 1 && x[0] <= lowprimes[lowprimes.length - 1]) + { + for (i = 0; i < lowprimes.length; ++i) + if (x[0] == lowprimes[i]) return true; + return false; + } + if (x.isEven()) return false; + i = 1; + while (i < lowprimes.length) + { + var m = lowprimes[i], + j = i + 1; + while (j < lowprimes.length && m < lplim) m *= lowprimes[j++]; + m = x.modInt(m); + while (i < j) + if (m % lowprimes[i++] == 0) return false; + } + return x.millerRabin(t); + } + // (protected) true if probably prime (HAC 4.24, Miller-Rabin) + function bnpMillerRabin(t) + { + var n1 = this.subtract(BigInteger.ONE); + var k = n1.getLowestSetBit(); + if (k <= 0) return false; + var r = n1.shiftRight(k); + t = (t + 1) >> 1; + if (t > lowprimes.length) t = lowprimes.length; + var a = nbi(); + for (var i = 0; i < t; ++i) + { + //Pick bases at random, instead of starting at 2 + a.fromInt(lowprimes[Math.floor(Math.random() * lowprimes.length)]); + var y = a.modPow(r, this); + if (y.compareTo(BigInteger.ONE) != 0 && y.compareTo(n1) != 0) + { + var j = 1; + while (j++ < k && y.compareTo(n1) != 0) + { + y = y.modPowInt(2, this); + if (y.compareTo(BigInteger.ONE) == 0) return false; + } + if (y.compareTo(n1) != 0) return false; + } + } + return true; + } + // protected + BigInteger.prototype.chunkSize = bnpChunkSize; + BigInteger.prototype.toRadix = bnpToRadix; + BigInteger.prototype.fromRadix = bnpFromRadix; + BigInteger.prototype.fromNumber = bnpFromNumber; + BigInteger.prototype.bitwiseTo = bnpBitwiseTo; + BigInteger.prototype.changeBit = bnpChangeBit; + BigInteger.prototype.addTo = bnpAddTo; + BigInteger.prototype.dMultiply = bnpDMultiply; + BigInteger.prototype.dAddOffset = bnpDAddOffset; + BigInteger.prototype.multiplyLowerTo = bnpMultiplyLowerTo; + BigInteger.prototype.multiplyUpperTo = bnpMultiplyUpperTo; + BigInteger.prototype.modInt = bnpModInt; + BigInteger.prototype.millerRabin = bnpMillerRabin; + // public + BigInteger.prototype.clone = bnClone; + BigInteger.prototype.intValue = bnIntValue; + BigInteger.prototype.byteValue = bnByteValue; + BigInteger.prototype.shortValue = bnShortValue; + BigInteger.prototype.signum = bnSigNum; + BigInteger.prototype.toByteArray = bnToByteArray; + BigInteger.prototype.equals = bnEquals; + BigInteger.prototype.min = bnMin; + BigInteger.prototype.max = bnMax; + BigInteger.prototype.and = bnAnd; + BigInteger.prototype.or = bnOr; + BigInteger.prototype.xor = bnXor; + BigInteger.prototype.andNot = bnAndNot; + BigInteger.prototype.not = bnNot; + BigInteger.prototype.shiftLeft = bnShiftLeft; + BigInteger.prototype.shiftRight = bnShiftRight; + BigInteger.prototype.getLowestSetBit = bnGetLowestSetBit; + BigInteger.prototype.bitCount = bnBitCount; + BigInteger.prototype.testBit = bnTestBit; + BigInteger.prototype.setBit = bnSetBit; + BigInteger.prototype.clearBit = bnClearBit; + BigInteger.prototype.flipBit = bnFlipBit; + BigInteger.prototype.add = bnAdd; + BigInteger.prototype.subtract = bnSubtract; + BigInteger.prototype.multiply = bnMultiply; + BigInteger.prototype.divide = bnDivide; + BigInteger.prototype.remainder = bnRemainder; + BigInteger.prototype.divideAndRemainder = bnDivideAndRemainder; + BigInteger.prototype.modPow = bnModPow; + BigInteger.prototype.modInverse = bnModInverse; + BigInteger.prototype.pow = bnPow; + BigInteger.prototype.gcd = bnGCD; + BigInteger.prototype.isProbablePrime = bnIsProbablePrime; + // JSBN-specific extension + BigInteger.prototype.square = bnSquare; + var Int128 = BigInteger; + // BigInteger interfaces not implemented in jsbn: + // BigInteger(int signum, byte[] magnitude) + // double doubleValue() + // float floatValue() + // int hashCode() + // long longValue() + // static BigInteger valueOf(long val) + // Helper functions to make BigInteger functions callable with two parameters + // as in original C# Clipper + Int128.prototype.IsNegative = function () + { + if (this.compareTo(Int128.ZERO) == -1) return true; + else return false; + }; + Int128.op_Equality = function (val1, val2) + { + if (val1.compareTo(val2) == 0) return true; + else return false; + }; + Int128.op_Inequality = function (val1, val2) + { + if (val1.compareTo(val2) != 0) return true; + else return false; + }; + Int128.op_GreaterThan = function (val1, val2) + { + if (val1.compareTo(val2) > 0) return true; + else return false; + }; + Int128.op_LessThan = function (val1, val2) + { + if (val1.compareTo(val2) < 0) return true; + else return false; + }; + Int128.op_Addition = function (lhs, rhs) + { + return new Int128(lhs).add(new Int128(rhs)); + }; + Int128.op_Subtraction = function (lhs, rhs) + { + return new Int128(lhs).subtract(new Int128(rhs)); + }; + Int128.Int128Mul = function (lhs, rhs) + { + return new Int128(lhs).multiply(new Int128(rhs)); + }; + Int128.op_Division = function (lhs, rhs) + { + return lhs.divide(rhs); + }; + Int128.prototype.ToDouble = function () + { + return parseFloat(this.toString()); // This could be something faster + }; + // end of Int128 section + /* + // Uncomment the following two lines if you want to use Int128 outside ClipperLib + if (typeof(document) !== "undefined") window.Int128 = Int128; + else self.Int128 = Int128; + */ + // --------------------------------------------- + // Here starts the actual Clipper library: + // Helper function to support Inheritance in Javascript + if (typeof (Inherit) == 'undefined') + { + var Inherit = function (ce, ce2) + { + var p; + if (typeof (Object.getOwnPropertyNames) == 'undefined') + { + for (p in ce2.prototype) + if (typeof (ce.prototype[p]) == 'undefined' || ce.prototype[p] == Object.prototype[p]) ce.prototype[p] = ce2.prototype[p]; + for (p in ce2) + if (typeof (ce[p]) == 'undefined') ce[p] = ce2[p]; + ce.$baseCtor = ce2; + } + else + { + var props = Object.getOwnPropertyNames(ce2.prototype); + for (var i = 0; i < props.length; i++) + if (typeof (Object.getOwnPropertyDescriptor(ce.prototype, props[i])) == 'undefined') Object.defineProperty(ce.prototype, props[i], Object.getOwnPropertyDescriptor(ce2.prototype, props[i])); + for (p in ce2) + if (typeof (ce[p]) == 'undefined') ce[p] = ce2[p]; + ce.$baseCtor = ce2; + } + }; + } + ClipperLib.Path = function () + { + return []; + }; + ClipperLib.Paths = function () + { + return []; // Was previously [[]], but caused problems when pushed + }; + // Preserves the calling way of original C# Clipper + // Is essential due to compatibility, because DoublePoint is public class in original C# version + ClipperLib.DoublePoint = function () + { + var a = arguments; + this.X = 0; + this.Y = 0; + // public DoublePoint(DoublePoint dp) + // public DoublePoint(IntPoint ip) + if (a.length == 1) + { + this.X = a[0].X; + this.Y = a[0].Y; + } + else if (a.length == 2) + { + this.X = a[0]; + this.Y = a[1]; + } + }; // This is internal faster function when called without arguments + ClipperLib.DoublePoint0 = function () + { + this.X = 0; + this.Y = 0; + }; + // This is internal faster function when called with 1 argument (dp or ip) + ClipperLib.DoublePoint1 = function (dp) + { + this.X = dp.X; + this.Y = dp.Y; + }; + // This is internal faster function when called with 2 arguments (x and y) + ClipperLib.DoublePoint2 = function (x, y) + { + this.X = x; + this.Y = y; + }; + // PolyTree & PolyNode start + // ------------------------------- + ClipperLib.PolyNode = function () + { + this.m_Parent = null; + this.m_polygon = new ClipperLib.Path(); + this.m_Index = 0; + this.m_jointype = 0; + this.m_endtype = 0; + this.m_Childs = []; + this.IsOpen = false; + }; + ClipperLib.PolyNode.prototype.IsHoleNode = function () + { + var result = true; + var node = this.m_Parent; + while (node !== null) + { + result = !result; + node = node.m_Parent; + } + return result; + }; + ClipperLib.PolyNode.prototype.ChildCount = function () + { + return this.m_Childs.length; + }; + ClipperLib.PolyNode.prototype.Contour = function () + { + return this.m_polygon; + }; + ClipperLib.PolyNode.prototype.AddChild = function (Child) + { + var cnt = this.m_Childs.length; + this.m_Childs.push(Child); + Child.m_Parent = this; + Child.m_Index = cnt; + }; + ClipperLib.PolyNode.prototype.GetNext = function () + { + if (this.m_Childs.length > 0) + return this.m_Childs[0]; + else + return this.GetNextSiblingUp(); + }; + ClipperLib.PolyNode.prototype.GetNextSiblingUp = function () + { + if (this.m_Parent === null) + return null; + else if (this.m_Index == this.m_Parent.m_Childs.length - 1) + return this.m_Parent.GetNextSiblingUp(); + else + return this.m_Parent.m_Childs[this.m_Index + 1]; + }; + ClipperLib.PolyNode.prototype.Childs = function () + { + return this.m_Childs; + }; + ClipperLib.PolyNode.prototype.Parent = function () + { + return this.m_Parent; + }; + ClipperLib.PolyNode.prototype.IsHole = function () + { + return this.IsHoleNode(); + }; + // PolyTree : PolyNode + ClipperLib.PolyTree = function () + { + this.m_AllPolys = []; + ClipperLib.PolyNode.call(this); + }; + ClipperLib.PolyTree.prototype.Clear = function () + { + for (var i = 0, ilen = this.m_AllPolys.length; i < ilen; i++) + this.m_AllPolys[i] = null; + this.m_AllPolys.length = 0; + this.m_Childs.length = 0; + }; + ClipperLib.PolyTree.prototype.GetFirst = function () + { + if (this.m_Childs.length > 0) + return this.m_Childs[0]; + else + return null; + }; + ClipperLib.PolyTree.prototype.Total = function () + { + return this.m_AllPolys.length; + }; + Inherit(ClipperLib.PolyTree, ClipperLib.PolyNode); + // ------------------------------- + // PolyTree & PolyNode end + ClipperLib.Math_Abs_Int64 = ClipperLib.Math_Abs_Int32 = ClipperLib.Math_Abs_Double = function (a) + { + return Math.abs(a); + }; + ClipperLib.Math_Max_Int32_Int32 = function (a, b) + { + return Math.max(a, b); + }; + /* + ----------------------------------- + cast_32 speedtest: http://jsperf.com/truncate-float-to-integer/2 + ----------------------------------- + */ + if (browser.msie || browser.opera || browser.safari) ClipperLib.Cast_Int32 = function (a) + { + return a | 0; + }; + else ClipperLib.Cast_Int32 = function (a) + { // eg. browser.chrome || browser.chromium || browser.firefox + return~~ a; + }; + /* + -------------------------- + cast_64 speedtests: http://jsperf.com/truncate-float-to-integer + Chrome: bitwise_not_floor + Firefox17: toInteger (typeof test) + IE9: bitwise_or_floor + IE7 and IE8: to_parseint + Chromium: to_floor_or_ceil + Firefox3: to_floor_or_ceil + Firefox15: to_floor_or_ceil + Opera: to_floor_or_ceil + Safari: to_floor_or_ceil + -------------------------- + */ + if (browser.chrome) ClipperLib.Cast_Int64 = function (a) + { + if (a < -2147483648 || a > 2147483647) + return a < 0 ? Math.ceil(a) : Math.floor(a); + else return~~ a; + }; + else if (browser.firefox && typeof (Number.toInteger) == "function") ClipperLib.Cast_Int64 = function (a) + { + return Number.toInteger(a); + }; + else if (browser.msie7 || browser.msie8) ClipperLib.Cast_Int64 = function (a) + { + return parseInt(a, 10); + }; + else if (browser.msie) ClipperLib.Cast_Int64 = function (a) + { + if (a < -2147483648 || a > 2147483647) + return a < 0 ? Math.ceil(a) : Math.floor(a); + return a | 0; + }; + // eg. browser.chromium || browser.firefox || browser.opera || browser.safari + else ClipperLib.Cast_Int64 = function (a) + { + return a < 0 ? Math.ceil(a) : Math.floor(a); + }; + ClipperLib.Clear = function (a) + { + a.length = 0; + }; + //ClipperLib.MaxSteps = 64; // How many steps at maximum in arc in BuildArc() function + ClipperLib.PI = 3.141592653589793; + ClipperLib.PI2 = 2 * 3.141592653589793; + ClipperLib.IntPoint = function () + { + var a = arguments, + alen = a.length; + this.X = 0; + this.Y = 0; + if (use_xyz) + { + this.Z = 0; + if (alen == 3) // public IntPoint(cInt x, cInt y, cInt z = 0) + { + this.X = a[0]; + this.Y = a[1]; + this.Z = a[2]; + } + else if (alen == 2) // public IntPoint(cInt x, cInt y) + { + this.X = a[0]; + this.Y = a[1]; + this.Z = 0; + } + else if (alen == 1) + { + if (a[0] instanceof ClipperLib.DoublePoint) // public IntPoint(DoublePoint dp) + { + var dp = a[0]; + this.X = ClipperLib.Clipper.Round(dp.X); + this.Y = ClipperLib.Clipper.Round(dp.Y); + this.Z = 0; + } + else // public IntPoint(IntPoint pt) + { + var pt = a[0]; + if (typeof (pt.Z) == "undefined") pt.Z = 0; + this.X = pt.X; + this.Y = pt.Y; + this.Z = pt.Z; + } + } + else // public IntPoint() + { + this.X = 0; + this.Y = 0; + this.Z = 0; + } + } + else // if (!use_xyz) + { + if (alen == 2) // public IntPoint(cInt X, cInt Y) + { + this.X = a[0]; + this.Y = a[1]; + } + else if (alen == 1) + { + if (a[0] instanceof ClipperLib.DoublePoint) // public IntPoint(DoublePoint dp) + { + var dp = a[0]; + this.X = ClipperLib.Clipper.Round(dp.X); + this.Y = ClipperLib.Clipper.Round(dp.Y); + } + else // public IntPoint(IntPoint pt) + { + var pt = a[0]; + this.X = pt.X; + this.Y = pt.Y; + } + } + else // public IntPoint(IntPoint pt) + { + this.X = 0; + this.Y = 0; + } + } + }; + ClipperLib.IntPoint.op_Equality = function (a, b) + { + //return a == b; + return a.X == b.X && a.Y == b.Y; + }; + ClipperLib.IntPoint.op_Inequality = function (a, b) + { + //return a != b; + return a.X != b.X || a.Y != b.Y; + }; + /* + ClipperLib.IntPoint.prototype.Equals = function (obj) + { + if (obj === null) + return false; + if (obj instanceof ClipperLib.IntPoint) + { + var a = Cast(obj, ClipperLib.IntPoint); + return (this.X == a.X) && (this.Y == a.Y); + } + else + return false; + }; +*/ + if (use_xyz) + { + ClipperLib.IntPoint0 = function () + { + this.X = 0; + this.Y = 0; + this.Z = 0; + }; + ClipperLib.IntPoint1 = function (pt) + { + this.X = pt.X; + this.Y = pt.Y; + this.Z = pt.Z; + }; + ClipperLib.IntPoint1dp = function (dp) + { + this.X = ClipperLib.Clipper.Round(dp.X); + this.Y = ClipperLib.Clipper.Round(dp.Y); + this.Z = 0; + }; + ClipperLib.IntPoint2 = function (x, y) + { + this.X = x; + this.Y = y; + this.Z = 0; + }; + ClipperLib.IntPoint3 = function (x, y, z) + { + this.X = x; + this.Y = y; + this.Z = z; + }; + } + else // if (!use_xyz) + { + ClipperLib.IntPoint0 = function () + { + this.X = 0; + this.Y = 0; + }; + ClipperLib.IntPoint1 = function (pt) + { + this.X = pt.X; + this.Y = pt.Y; + }; + ClipperLib.IntPoint1dp = function (dp) + { + this.X = ClipperLib.Clipper.Round(dp.X); + this.Y = ClipperLib.Clipper.Round(dp.Y); + }; + ClipperLib.IntPoint2 = function (x, y) + { + this.X = x; + this.Y = y; + }; + } + ClipperLib.IntRect = function () + { + var a = arguments, + alen = a.length; + if (alen == 4) // function (l, t, r, b) + { + this.left = a[0]; + this.top = a[1]; + this.right = a[2]; + this.bottom = a[3]; + } + else if (alen == 1) // function (ir) + { + this.left = ir.left; + this.top = ir.top; + this.right = ir.right; + this.bottom = ir.bottom; + } + else // function () + { + this.left = 0; + this.top = 0; + this.right = 0; + this.bottom = 0; + } + }; + ClipperLib.IntRect0 = function () + { + this.left = 0; + this.top = 0; + this.right = 0; + this.bottom = 0; + }; + ClipperLib.IntRect1 = function (ir) + { + this.left = ir.left; + this.top = ir.top; + this.right = ir.right; + this.bottom = ir.bottom; + }; + ClipperLib.IntRect4 = function (l, t, r, b) + { + this.left = l; + this.top = t; + this.right = r; + this.bottom = b; + }; + ClipperLib.ClipType = { + ctIntersection: 0, + ctUnion: 1, + ctDifference: 2, + ctXor: 3 + }; + ClipperLib.PolyType = { + ptSubject: 0, + ptClip: 1 + }; + ClipperLib.PolyFillType = { + pftEvenOdd: 0, + pftNonZero: 1, + pftPositive: 2, + pftNegative: 3 + }; + ClipperLib.JoinType = { + jtSquare: 0, + jtRound: 1, + jtMiter: 2 + }; + ClipperLib.EndType = { + etOpenSquare: 0, + etOpenRound: 1, + etOpenButt: 2, + etClosedLine: 3, + etClosedPolygon: 4 + }; + if (use_deprecated) + ClipperLib.EndType_ = { + etSquare: 0, + etRound: 1, + etButt: 2, + etClosed: 3 + }; + ClipperLib.EdgeSide = { + esLeft: 0, + esRight: 1 + }; + ClipperLib.Direction = { + dRightToLeft: 0, + dLeftToRight: 1 + }; + ClipperLib.TEdge = function () + { + this.Bot = new ClipperLib.IntPoint(); + this.Curr = new ClipperLib.IntPoint(); + this.Top = new ClipperLib.IntPoint(); + this.Delta = new ClipperLib.IntPoint(); + this.Dx = 0; + this.PolyTyp = ClipperLib.PolyType.ptSubject; + this.Side = ClipperLib.EdgeSide.esLeft; + this.WindDelta = 0; + this.WindCnt = 0; + this.WindCnt2 = 0; + this.OutIdx = 0; + this.Next = null; + this.Prev = null; + this.NextInLML = null; + this.NextInAEL = null; + this.PrevInAEL = null; + this.NextInSEL = null; + this.PrevInSEL = null; + }; + ClipperLib.IntersectNode = function () + { + this.Edge1 = null; + this.Edge2 = null; + this.Pt = new ClipperLib.IntPoint(); + }; + ClipperLib.MyIntersectNodeSort = function () {}; + ClipperLib.MyIntersectNodeSort.Compare = function (node1, node2) + { + return (node2.Pt.Y - node1.Pt.Y); + }; + ClipperLib.LocalMinima = function () + { + this.Y = 0; + this.LeftBound = null; + this.RightBound = null; + this.Next = null; + }; + ClipperLib.Scanbeam = function () + { + this.Y = 0; + this.Next = null; + }; + ClipperLib.OutRec = function () + { + this.Idx = 0; + this.IsHole = false; + this.IsOpen = false; + this.FirstLeft = null; + this.Pts = null; + this.BottomPt = null; + this.PolyNode = null; + }; + ClipperLib.OutPt = function () + { + this.Idx = 0; + this.Pt = new ClipperLib.IntPoint(); + this.Next = null; + this.Prev = null; + }; + ClipperLib.Join = function () + { + this.OutPt1 = null; + this.OutPt2 = null; + this.OffPt = new ClipperLib.IntPoint(); + }; + ClipperLib.ClipperBase = function () + { + this.m_MinimaList = null; + this.m_CurrentLM = null; + this.m_edges = new Array(); + this.m_UseFullRange = false; + this.m_HasOpenPaths = false; + this.PreserveCollinear = false; + this.m_MinimaList = null; + this.m_CurrentLM = null; + this.m_UseFullRange = false; + this.m_HasOpenPaths = false; + }; + // Ranges are in original C# too high for Javascript (in current state 2013 september): + // protected const double horizontal = -3.4E+38; + // internal const cInt loRange = 0x3FFFFFFF; // = 1073741823 = sqrt(2^63 -1)/2 + // internal const cInt hiRange = 0x3FFFFFFFFFFFFFFFL; // = 4611686018427387903 = sqrt(2^127 -1)/2 + // So had to adjust them to more suitable for Javascript. + // If JS some day supports truly 64-bit integers, then these ranges can be as in C# + // and biginteger library can be more simpler (as then 128bit can be represented as two 64bit numbers) + ClipperLib.ClipperBase.horizontal = -9007199254740992; //-2^53 + ClipperLib.ClipperBase.Skip = -2; + ClipperLib.ClipperBase.Unassigned = -1; + ClipperLib.ClipperBase.tolerance = 1E-20; + if (use_int32) + { + ClipperLib.ClipperBase.loRange = 46340; + ClipperLib.ClipperBase.hiRange = 46340; + } + else + { + ClipperLib.ClipperBase.loRange = 47453132; // sqrt(2^53 -1)/2 + ClipperLib.ClipperBase.hiRange = 4503599627370495; // sqrt(2^106 -1)/2 + } + ClipperLib.ClipperBase.near_zero = function (val) + { + return (val > -ClipperLib.ClipperBase.tolerance) && (val < ClipperLib.ClipperBase.tolerance); + }; + ClipperLib.ClipperBase.IsHorizontal = function (e) + { + return e.Delta.Y === 0; + }; + ClipperLib.ClipperBase.prototype.PointIsVertex = function (pt, pp) + { + var pp2 = pp; + do { + if (ClipperLib.IntPoint.op_Equality(pp2.Pt, pt)) + return true; + pp2 = pp2.Next; + } + while (pp2 != pp) + return false; + }; + ClipperLib.ClipperBase.prototype.PointOnLineSegment = function (pt, linePt1, linePt2, UseFullRange) + { + if (UseFullRange) + return ((pt.X == linePt1.X) && (pt.Y == linePt1.Y)) || + ((pt.X == linePt2.X) && (pt.Y == linePt2.Y)) || + (((pt.X > linePt1.X) == (pt.X < linePt2.X)) && + ((pt.Y > linePt1.Y) == (pt.Y < linePt2.Y)) && + (Int128.op_Equality(Int128.Int128Mul((pt.X - linePt1.X), (linePt2.Y - linePt1.Y)), + Int128.Int128Mul((linePt2.X - linePt1.X), (pt.Y - linePt1.Y))))); + else + return ((pt.X == linePt1.X) && (pt.Y == linePt1.Y)) || ((pt.X == linePt2.X) && (pt.Y == linePt2.Y)) || (((pt.X > linePt1.X) == (pt.X < linePt2.X)) && ((pt.Y > linePt1.Y) == (pt.Y < linePt2.Y)) && ((pt.X - linePt1.X) * (linePt2.Y - linePt1.Y) == (linePt2.X - linePt1.X) * (pt.Y - linePt1.Y))); + }; + ClipperLib.ClipperBase.prototype.PointOnPolygon = function (pt, pp, UseFullRange) + { + var pp2 = pp; + while (true) + { + if (this.PointOnLineSegment(pt, pp2.Pt, pp2.Next.Pt, UseFullRange)) + return true; + pp2 = pp2.Next; + if (pp2 == pp) + break; + } + return false; + }; + ClipperLib.ClipperBase.prototype.SlopesEqual = ClipperLib.ClipperBase.SlopesEqual = function () + { + var a = arguments, + alen = a.length; + var e1, e2, pt1, pt2, pt3, pt4, UseFullRange; + if (alen == 3) // function (e1, e2, UseFullRange) + { + e1 = a[0]; + e2 = a[1]; + UseFullRange = a[2]; + if (UseFullRange) + return Int128.op_Equality(Int128.Int128Mul(e1.Delta.Y, e2.Delta.X), Int128.Int128Mul(e1.Delta.X, e2.Delta.Y)); + else + return ClipperLib.Cast_Int64((e1.Delta.Y) * (e2.Delta.X)) == ClipperLib.Cast_Int64((e1.Delta.X) * (e2.Delta.Y)); + } + else if (alen == 4) // function (pt1, pt2, pt3, UseFullRange) + { + pt1 = a[0]; + pt2 = a[1]; + pt3 = a[2]; + UseFullRange = a[3]; + if (UseFullRange) + return Int128.op_Equality(Int128.Int128Mul(pt1.Y - pt2.Y, pt2.X - pt3.X), Int128.Int128Mul(pt1.X - pt2.X, pt2.Y - pt3.Y)); + else + return ClipperLib.Cast_Int64((pt1.Y - pt2.Y) * (pt2.X - pt3.X)) - ClipperLib.Cast_Int64((pt1.X - pt2.X) * (pt2.Y - pt3.Y)) === 0; + } + else // function (pt1, pt2, pt3, pt4, UseFullRange) + { + pt1 = a[0]; + pt2 = a[1]; + pt3 = a[2]; + pt4 = a[3]; + UseFullRange = a[4]; + if (UseFullRange) + return Int128.op_Equality(Int128.Int128Mul(pt1.Y - pt2.Y, pt3.X - pt4.X), Int128.Int128Mul(pt1.X - pt2.X, pt3.Y - pt4.Y)); + else + return ClipperLib.Cast_Int64((pt1.Y - pt2.Y) * (pt3.X - pt4.X)) - ClipperLib.Cast_Int64((pt1.X - pt2.X) * (pt3.Y - pt4.Y)) === 0; + } + }; + ClipperLib.ClipperBase.SlopesEqual3 = function (e1, e2, UseFullRange) + { + if (UseFullRange) + return Int128.op_Equality(Int128.Int128Mul(e1.Delta.Y, e2.Delta.X), Int128.Int128Mul(e1.Delta.X, e2.Delta.Y)); + else + return ClipperLib.Cast_Int64((e1.Delta.Y) * (e2.Delta.X)) == ClipperLib.Cast_Int64((e1.Delta.X) * (e2.Delta.Y)); + }; + ClipperLib.ClipperBase.SlopesEqual4 = function (pt1, pt2, pt3, UseFullRange) + { + if (UseFullRange) + return Int128.op_Equality(Int128.Int128Mul(pt1.Y - pt2.Y, pt2.X - pt3.X), Int128.Int128Mul(pt1.X - pt2.X, pt2.Y - pt3.Y)); + else + return ClipperLib.Cast_Int64((pt1.Y - pt2.Y) * (pt2.X - pt3.X)) - ClipperLib.Cast_Int64((pt1.X - pt2.X) * (pt2.Y - pt3.Y)) === 0; + }; + ClipperLib.ClipperBase.SlopesEqual5 = function (pt1, pt2, pt3, pt4, UseFullRange) + { + if (UseFullRange) + return Int128.op_Equality(Int128.Int128Mul(pt1.Y - pt2.Y, pt3.X - pt4.X), Int128.Int128Mul(pt1.X - pt2.X, pt3.Y - pt4.Y)); + else + return ClipperLib.Cast_Int64((pt1.Y - pt2.Y) * (pt3.X - pt4.X)) - ClipperLib.Cast_Int64((pt1.X - pt2.X) * (pt3.Y - pt4.Y)) === 0; + }; + ClipperLib.ClipperBase.prototype.Clear = function () + { + this.DisposeLocalMinimaList(); + for (var i = 0, ilen = this.m_edges.length; i < ilen; ++i) + { + for (var j = 0, jlen = this.m_edges[i].length; j < jlen; ++j) + this.m_edges[i][j] = null; + ClipperLib.Clear(this.m_edges[i]); + } + ClipperLib.Clear(this.m_edges); + this.m_UseFullRange = false; + this.m_HasOpenPaths = false; + }; + ClipperLib.ClipperBase.prototype.DisposeLocalMinimaList = function () + { + while (this.m_MinimaList !== null) + { + var tmpLm = this.m_MinimaList.Next; + this.m_MinimaList = null; + this.m_MinimaList = tmpLm; + } + this.m_CurrentLM = null; + }; + ClipperLib.ClipperBase.prototype.RangeTest = function (Pt, useFullRange) + { + if (useFullRange.Value) + { + if (Pt.X > ClipperLib.ClipperBase.hiRange || Pt.Y > ClipperLib.ClipperBase.hiRange || -Pt.X > ClipperLib.ClipperBase.hiRange || -Pt.Y > ClipperLib.ClipperBase.hiRange) + ClipperLib.Error("Coordinate outside allowed range in RangeTest()."); + } + else if (Pt.X > ClipperLib.ClipperBase.loRange || Pt.Y > ClipperLib.ClipperBase.loRange || -Pt.X > ClipperLib.ClipperBase.loRange || -Pt.Y > ClipperLib.ClipperBase.loRange) + { + useFullRange.Value = true; + this.RangeTest(Pt, useFullRange); + } + }; + ClipperLib.ClipperBase.prototype.InitEdge = function (e, eNext, ePrev, pt) + { + e.Next = eNext; + e.Prev = ePrev; + //e.Curr = pt; + e.Curr.X = pt.X; + e.Curr.Y = pt.Y; + e.OutIdx = -1; + }; + ClipperLib.ClipperBase.prototype.InitEdge2 = function (e, polyType) + { + if (e.Curr.Y >= e.Next.Curr.Y) + { + //e.Bot = e.Curr; + e.Bot.X = e.Curr.X; + e.Bot.Y = e.Curr.Y; + //e.Top = e.Next.Curr; + e.Top.X = e.Next.Curr.X; + e.Top.Y = e.Next.Curr.Y; + } + else + { + //e.Top = e.Curr; + e.Top.X = e.Curr.X; + e.Top.Y = e.Curr.Y; + //e.Bot = e.Next.Curr; + e.Bot.X = e.Next.Curr.X; + e.Bot.Y = e.Next.Curr.Y; + } + this.SetDx(e); + e.PolyTyp = polyType; + }; + ClipperLib.ClipperBase.prototype.FindNextLocMin = function (E) + { + var E2; + for (;;) + { + while (ClipperLib.IntPoint.op_Inequality(E.Bot, E.Prev.Bot) || ClipperLib.IntPoint.op_Equality(E.Curr, E.Top)) + E = E.Next; + if (E.Dx != ClipperLib.ClipperBase.horizontal && E.Prev.Dx != ClipperLib.ClipperBase.horizontal) + break; + while (E.Prev.Dx == ClipperLib.ClipperBase.horizontal) + E = E.Prev; + E2 = E; + while (E.Dx == ClipperLib.ClipperBase.horizontal) + E = E.Next; + if (E.Top.Y == E.Prev.Bot.Y) + continue; + //ie just an intermediate horz. + if (E2.Prev.Bot.X < E.Bot.X) + E = E2; + break; + } + return E; + }; + ClipperLib.ClipperBase.prototype.ProcessBound = function (E, IsClockwise) + { + var EStart = E, + Result = E; + var Horz; + var StartX; + if (E.Dx == ClipperLib.ClipperBase.horizontal) + { + //it's possible for adjacent overlapping horz edges to start heading left + //before finishing right, so ... + if (IsClockwise) + StartX = E.Prev.Bot.X; + else + StartX = E.Next.Bot.X; + if (E.Bot.X != StartX) + this.ReverseHorizontal(E); + } + if (Result.OutIdx != ClipperLib.ClipperBase.Skip) + { + if (IsClockwise) + { + while (Result.Top.Y == Result.Next.Bot.Y && Result.Next.OutIdx != ClipperLib.ClipperBase.Skip) + Result = Result.Next; + if (Result.Dx == ClipperLib.ClipperBase.horizontal && Result.Next.OutIdx != ClipperLib.ClipperBase.Skip) + { + //nb: at the top of a bound, horizontals are added to the bound + //only when the preceding edge attaches to the horizontal's left vertex + //unless a Skip edge is encountered when that becomes the top divide + Horz = Result; + while (Horz.Prev.Dx == ClipperLib.ClipperBase.horizontal) + Horz = Horz.Prev; + if (Horz.Prev.Top.X == Result.Next.Top.X) + { + if (!IsClockwise) + Result = Horz.Prev; + } + else if (Horz.Prev.Top.X > Result.Next.Top.X) + Result = Horz.Prev; + } + while (E != Result) + { + E.NextInLML = E.Next; + if (E.Dx == ClipperLib.ClipperBase.horizontal && E != EStart && E.Bot.X != E.Prev.Top.X) + this.ReverseHorizontal(E); + E = E.Next; + } + if (E.Dx == ClipperLib.ClipperBase.horizontal && E != EStart && E.Bot.X != E.Prev.Top.X) + this.ReverseHorizontal(E); + Result = Result.Next; + //move to the edge just beyond current bound + } + else + { + while (Result.Top.Y == Result.Prev.Bot.Y && Result.Prev.OutIdx != ClipperLib.ClipperBase.Skip) + Result = Result.Prev; + if (Result.Dx == ClipperLib.ClipperBase.horizontal && Result.Prev.OutIdx != ClipperLib.ClipperBase.Skip) + { + Horz = Result; + while (Horz.Next.Dx == ClipperLib.ClipperBase.horizontal) + Horz = Horz.Next; + if (Horz.Next.Top.X == Result.Prev.Top.X) + { + if (!IsClockwise) + Result = Horz.Next; + } + else if (Horz.Next.Top.X > Result.Prev.Top.X) + Result = Horz.Next; + } + while (E != Result) + { + E.NextInLML = E.Prev; + if (E.Dx == ClipperLib.ClipperBase.horizontal && E != EStart && E.Bot.X != E.Next.Top.X) + this.ReverseHorizontal(E); + E = E.Prev; + } + if (E.Dx == ClipperLib.ClipperBase.horizontal && E != EStart && E.Bot.X != E.Next.Top.X) + this.ReverseHorizontal(E); + Result = Result.Prev; + //move to the edge just beyond current bound + } + } + if (Result.OutIdx == ClipperLib.ClipperBase.Skip) + { + //if edges still remain in the current bound beyond the skip edge then + //create another LocMin and call ProcessBound once more + E = Result; + if (IsClockwise) + { + while (E.Top.Y == E.Next.Bot.Y) + E = E.Next; + //don't include top horizontals when parsing a bound a second time, + //they will be contained in the opposite bound ... + while (E != Result && E.Dx == ClipperLib.ClipperBase.horizontal) + E = E.Prev; + } + else + { + while (E.Top.Y == E.Prev.Bot.Y) + E = E.Prev; + while (E != Result && E.Dx == ClipperLib.ClipperBase.horizontal) + E = E.Next; + } + if (E == Result) + { + if (IsClockwise) + Result = E.Next; + else + Result = E.Prev; + } + else + { + //there are more edges in the bound beyond result starting with E + if (IsClockwise) + E = Result.Next; + else + E = Result.Prev; + var locMin = new ClipperLib.LocalMinima(); + locMin.Next = null; + locMin.Y = E.Bot.Y; + locMin.LeftBound = null; + locMin.RightBound = E; + locMin.RightBound.WindDelta = 0; + Result = this.ProcessBound(locMin.RightBound, IsClockwise); + this.InsertLocalMinima(locMin); + } + } + return Result; + }; + ClipperLib.ClipperBase.prototype.AddPath = function (pg, polyType, Closed) + { + if (use_lines) + { + if (!Closed && polyType == ClipperLib.PolyType.ptClip) + ClipperLib.Error("AddPath: Open paths must be subject."); + } + else + { + if (!Closed) + ClipperLib.Error("AddPath: Open paths have been disabled."); + } + var highI = pg.length - 1; + if (Closed) + while (highI > 0 && (ClipperLib.IntPoint.op_Equality(pg[highI], pg[0]))) + --highI; + while (highI > 0 && (ClipperLib.IntPoint.op_Equality(pg[highI], pg[highI - 1]))) + --highI; + if ((Closed && highI < 2) || (!Closed && highI < 1)) + return false; + //create a new edge array ... + var edges = new Array(); + for (var i = 0; i <= highI; i++) + edges.push(new ClipperLib.TEdge()); + var IsFlat = true; + //1. Basic (first) edge initialization ... + + //edges[1].Curr = pg[1]; + edges[1].Curr.X = pg[1].X; + edges[1].Curr.Y = pg[1].Y; + + var $1 = {Value: this.m_UseFullRange}; + this.RangeTest(pg[0], $1); + this.m_UseFullRange = $1.Value; + + $1.Value = this.m_UseFullRange; + this.RangeTest(pg[highI], $1); + this.m_UseFullRange = $1.Value; + + this.InitEdge(edges[0], edges[1], edges[highI], pg[0]); + this.InitEdge(edges[highI], edges[0], edges[highI - 1], pg[highI]); + for (var i = highI - 1; i >= 1; --i) + { + $1.Value = this.m_UseFullRange; + this.RangeTest(pg[i], $1); + this.m_UseFullRange = $1.Value; + + this.InitEdge(edges[i], edges[i + 1], edges[i - 1], pg[i]); + } + + var eStart = edges[0]; + //2. Remove duplicate vertices, and (when closed) collinear edges ... + var E = eStart, + eLoopStop = eStart; + for (;;) + { + if (ClipperLib.IntPoint.op_Equality(E.Curr, E.Next.Curr) && (Closed || E.Next != eStart)) + { + if (E == E.Next) + break; + if (E == eStart && !Closed) + eStart = E.Next; + E = this.RemoveEdge(E); + eLoopStop = E; + continue; + } + if (E.Prev == E.Next) + break; + else if (Closed && ClipperLib.ClipperBase.SlopesEqual(E.Prev.Curr, E.Curr, E.Next.Curr, this.m_UseFullRange) && (!this.PreserveCollinear || !this.Pt2IsBetweenPt1AndPt3(E.Prev.Curr, E.Curr, E.Next.Curr))) + { + //Collinear edges are allowed for open paths but in closed paths + //the default is to merge adjacent collinear edges into a single edge. + //However, if the PreserveCollinear property is enabled, only overlapping + //collinear edges (ie spikes) will be removed from closed paths. + if (E == eStart) + eStart = E.Next; + E = this.RemoveEdge(E); + E = E.Prev; + eLoopStop = E; + continue; + } + E = E.Next; + if (E == eLoopStop) + break; + } + if ((!Closed && (E == E.Next)) || (Closed && (E.Prev == E.Next))) + return false; + if (!Closed) + { + this.m_HasOpenPaths = true; + eStart.Prev.OutIdx = ClipperLib.ClipperBase.Skip; + } + //3. Do second stage of edge initialization ... + var eHighest = eStart; + E = eStart; + do { + this.InitEdge2(E, polyType); + E = E.Next; + if (IsFlat && E.Curr.Y != eStart.Curr.Y) + IsFlat = false; + } + while (E != eStart) + //4. Finally, add edge bounds to LocalMinima list ... + //Totally flat paths must be handled differently when adding them + //to LocalMinima list to avoid endless loops etc ... + if (IsFlat) + { + if (Closed) + return false; + E.Prev.OutIdx = ClipperLib.ClipperBase.Skip; + if (E.Prev.Bot.X < E.Prev.Top.X) + this.ReverseHorizontal(E.Prev); + var locMin = new ClipperLib.LocalMinima(); + locMin.Next = null; + locMin.Y = E.Bot.Y; + locMin.LeftBound = null; + locMin.RightBound = E; + locMin.RightBound.Side = ClipperLib.EdgeSide.esRight; + locMin.RightBound.WindDelta = 0; + while (E.Next.OutIdx != ClipperLib.ClipperBase.Skip) + { + E.NextInLML = E.Next; + if (E.Bot.X != E.Prev.Top.X) + this.ReverseHorizontal(E); + E = E.Next; + } + this.InsertLocalMinima(locMin); + this.m_edges.push(edges); + return true; + } + this.m_edges.push(edges); + var clockwise; + var EMin = null; + for (;;) + { + E = this.FindNextLocMin(E); + if (E == EMin) + break; + else if (EMin == null) + EMin = E; + //E and E.Prev now share a local minima (left aligned if horizontal). + //Compare their slopes to find which starts which bound ... + var locMin = new ClipperLib.LocalMinima(); + locMin.Next = null; + locMin.Y = E.Bot.Y; + if (E.Dx < E.Prev.Dx) + { + locMin.LeftBound = E.Prev; + locMin.RightBound = E; + clockwise = false; + //Q.nextInLML = Q.prev + } + else + { + locMin.LeftBound = E; + locMin.RightBound = E.Prev; + clockwise = true; + //Q.nextInLML = Q.next + } + locMin.LeftBound.Side = ClipperLib.EdgeSide.esLeft; + locMin.RightBound.Side = ClipperLib.EdgeSide.esRight; + if (!Closed) + locMin.LeftBound.WindDelta = 0; + else if (locMin.LeftBound.Next == locMin.RightBound) + locMin.LeftBound.WindDelta = -1; + else + locMin.LeftBound.WindDelta = 1; + locMin.RightBound.WindDelta = -locMin.LeftBound.WindDelta; + E = this.ProcessBound(locMin.LeftBound, clockwise); + var E2 = this.ProcessBound(locMin.RightBound, !clockwise); + if (locMin.LeftBound.OutIdx == ClipperLib.ClipperBase.Skip) + locMin.LeftBound = null; + else if (locMin.RightBound.OutIdx == ClipperLib.ClipperBase.Skip) + locMin.RightBound = null; + this.InsertLocalMinima(locMin); + if (!clockwise) + E = E2; + } + return true; + }; + ClipperLib.ClipperBase.prototype.AddPaths = function (ppg, polyType, closed) + { + // console.log("-------------------------------------------"); + // console.log(JSON.stringify(ppg)); + var result = false; + for (var i = 0, ilen = ppg.length; i < ilen; ++i) + if (this.AddPath(ppg[i], polyType, closed)) + result = true; + return result; + }; + //------------------------------------------------------------------------------ + ClipperLib.ClipperBase.prototype.Pt2IsBetweenPt1AndPt3 = function (pt1, pt2, pt3) + { + if ((ClipperLib.IntPoint.op_Equality(pt1, pt3)) || (ClipperLib.IntPoint.op_Equality(pt1, pt2)) || + (ClipperLib.IntPoint.op_Equality(pt3, pt2))) + return false; + else if (pt1.X != pt3.X) + return (pt2.X > pt1.X) == (pt2.X < pt3.X); + else + return (pt2.Y > pt1.Y) == (pt2.Y < pt3.Y); + }; + ClipperLib.ClipperBase.prototype.RemoveEdge = function (e) + { + //removes e from double_linked_list (but without removing from memory) + e.Prev.Next = e.Next; + e.Next.Prev = e.Prev; + var result = e.Next; + e.Prev = null; //flag as removed (see ClipperBase.Clear) + return result; + }; + ClipperLib.ClipperBase.prototype.SetDx = function (e) + { + e.Delta.X = (e.Top.X - e.Bot.X); + e.Delta.Y = (e.Top.Y - e.Bot.Y); + if (e.Delta.Y === 0) e.Dx = ClipperLib.ClipperBase.horizontal; + else e.Dx = (e.Delta.X) / (e.Delta.Y); + }; + ClipperLib.ClipperBase.prototype.InsertLocalMinima = function (newLm) + { + if (this.m_MinimaList === null) + { + this.m_MinimaList = newLm; + } + else if (newLm.Y >= this.m_MinimaList.Y) + { + newLm.Next = this.m_MinimaList; + this.m_MinimaList = newLm; + } + else + { + var tmpLm = this.m_MinimaList; + while (tmpLm.Next !== null && (newLm.Y < tmpLm.Next.Y)) + tmpLm = tmpLm.Next; + newLm.Next = tmpLm.Next; + tmpLm.Next = newLm; + } + }; + ClipperLib.ClipperBase.prototype.PopLocalMinima = function () + { + if (this.m_CurrentLM === null) + return; + this.m_CurrentLM = this.m_CurrentLM.Next; + }; + ClipperLib.ClipperBase.prototype.ReverseHorizontal = function (e) + { + //swap horizontal edges' top and bottom x's so they follow the natural + //progression of the bounds - ie so their xbots will align with the + //adjoining lower edge. [Helpful in the ProcessHorizontal() method.] + var tmp = e.Top.X; + e.Top.X = e.Bot.X; + e.Bot.X = tmp; + if (use_xyz) + { + tmp = e.Top.Z; + e.Top.Z = e.Bot.Z; + e.Bot.Z = tmp; + } + }; + ClipperLib.ClipperBase.prototype.Reset = function () + { + this.m_CurrentLM = this.m_MinimaList; + if (this.m_CurrentLM == null) + return; + //ie nothing to process + //reset all edges ... + var lm = this.m_MinimaList; + while (lm != null) + { + var e = lm.LeftBound; + if (e != null) + { + //e.Curr = e.Bot; + e.Curr.X = e.Bot.X; + e.Curr.Y = e.Bot.Y; + e.Side = ClipperLib.EdgeSide.esLeft; + e.OutIdx = ClipperLib.ClipperBase.Unassigned; + } + e = lm.RightBound; + if (e != null) + { + //e.Curr = e.Bot; + e.Curr.X = e.Bot.X; + e.Curr.Y = e.Bot.Y; + e.Side = ClipperLib.EdgeSide.esRight; + e.OutIdx = ClipperLib.ClipperBase.Unassigned; + } + lm = lm.Next; + } + }; + ClipperLib.Clipper = function (InitOptions) // public Clipper(int InitOptions = 0) + { + if (typeof (InitOptions) == "undefined") InitOptions = 0; + this.m_PolyOuts = null; + this.m_ClipType = ClipperLib.ClipType.ctIntersection; + this.m_Scanbeam = null; + this.m_ActiveEdges = null; + this.m_SortedEdges = null; + this.m_IntersectList = null; + this.m_IntersectNodeComparer = null; + this.m_ExecuteLocked = false; + this.m_ClipFillType = ClipperLib.PolyFillType.pftEvenOdd; + this.m_SubjFillType = ClipperLib.PolyFillType.pftEvenOdd; + this.m_Joins = null; + this.m_GhostJoins = null; + this.m_UsingPolyTree = false; + this.ReverseSolution = false; + this.StrictlySimple = false; + ClipperLib.ClipperBase.call(this); + this.m_Scanbeam = null; + this.m_ActiveEdges = null; + this.m_SortedEdges = null; + this.m_IntersectList = new Array(); + this.m_IntersectNodeComparer = ClipperLib.MyIntersectNodeSort.Compare; + this.m_ExecuteLocked = false; + this.m_UsingPolyTree = false; + this.m_PolyOuts = new Array(); + this.m_Joins = new Array(); + this.m_GhostJoins = new Array(); + this.ReverseSolution = (1 & InitOptions) !== 0; + this.StrictlySimple = (2 & InitOptions) !== 0; + this.PreserveCollinear = (4 & InitOptions) !== 0; + if (use_xyz) + { + this.ZFillFunction = null; // function (IntPoint vert1, IntPoint vert2, ref IntPoint intersectPt); + } + }; + ClipperLib.Clipper.ioReverseSolution = 1; + ClipperLib.Clipper.ioStrictlySimple = 2; + ClipperLib.Clipper.ioPreserveCollinear = 4; + + ClipperLib.Clipper.prototype.Clear = function () + { + if (this.m_edges.length === 0) + return; + //avoids problems with ClipperBase destructor + this.DisposeAllPolyPts(); + ClipperLib.ClipperBase.prototype.Clear.call(this); + }; + + ClipperLib.Clipper.prototype.DisposeScanbeamList = function () + { + while (this.m_Scanbeam !== null) + { + var sb2 = this.m_Scanbeam.Next; + this.m_Scanbeam = null; + this.m_Scanbeam = sb2; + } + }; + ClipperLib.Clipper.prototype.Reset = function () + { + ClipperLib.ClipperBase.prototype.Reset.call(this); + this.m_Scanbeam = null; + this.m_ActiveEdges = null; + this.m_SortedEdges = null; + + var lm = this.m_MinimaList; + while (lm !== null) + { + this.InsertScanbeam(lm.Y); + lm = lm.Next; + } + }; + ClipperLib.Clipper.prototype.InsertScanbeam = function (Y) + { + if (this.m_Scanbeam === null) + { + this.m_Scanbeam = new ClipperLib.Scanbeam(); + this.m_Scanbeam.Next = null; + this.m_Scanbeam.Y = Y; + } + else if (Y > this.m_Scanbeam.Y) + { + var newSb = new ClipperLib.Scanbeam(); + newSb.Y = Y; + newSb.Next = this.m_Scanbeam; + this.m_Scanbeam = newSb; + } + else + { + var sb2 = this.m_Scanbeam; + while (sb2.Next !== null && (Y <= sb2.Next.Y)) + sb2 = sb2.Next; + if (Y == sb2.Y) + return; + //ie ignores duplicates + var newSb = new ClipperLib.Scanbeam(); + newSb.Y = Y; + newSb.Next = sb2.Next; + sb2.Next = newSb; + } + }; + // ************************************ + ClipperLib.Clipper.prototype.Execute = function () + { + var a = arguments, + alen = a.length, + ispolytree = a[1] instanceof ClipperLib.PolyTree; + if (alen == 4 && !ispolytree) // function (clipType, solution, subjFillType, clipFillType) + { + var clipType = a[0], + solution = a[1], + subjFillType = a[2], + clipFillType = a[3]; + if (this.m_ExecuteLocked) + return false; + if (this.m_HasOpenPaths) + ClipperLib.Error("Error: PolyTree struct is need for open path clipping."); + this.m_ExecuteLocked = true; + ClipperLib.Clear(solution); + this.m_SubjFillType = subjFillType; + this.m_ClipFillType = clipFillType; + this.m_ClipType = clipType; + this.m_UsingPolyTree = false; + try + { + var succeeded = this.ExecuteInternal(); + //build the return polygons ... + if (succeeded) this.BuildResult(solution); + } + finally + { + this.DisposeAllPolyPts(); + this.m_ExecuteLocked = false; + } + return succeeded; + } + else if (alen == 4 && ispolytree) // function (clipType, polytree, subjFillType, clipFillType) + { + var clipType = a[0], + polytree = a[1], + subjFillType = a[2], + clipFillType = a[3]; + if (this.m_ExecuteLocked) + return false; + this.m_ExecuteLocked = true; + this.m_SubjFillType = subjFillType; + this.m_ClipFillType = clipFillType; + this.m_ClipType = clipType; + this.m_UsingPolyTree = true; + try + { + var succeeded = this.ExecuteInternal(); + //build the return polygons ... + if (succeeded) this.BuildResult2(polytree); + } + finally + { + this.DisposeAllPolyPts(); + this.m_ExecuteLocked = false; + } + return succeeded; + } + else if (alen == 2 && !ispolytree) // function (clipType, solution) + { + var clipType = a[0], + solution = a[1]; + return this.Execute(clipType, solution, ClipperLib.PolyFillType.pftEvenOdd, ClipperLib.PolyFillType.pftEvenOdd); + } + else if (alen == 2 && ispolytree) // function (clipType, polytree) + { + var clipType = a[0], + polytree = a[1]; + return this.Execute(clipType, polytree, ClipperLib.PolyFillType.pftEvenOdd, ClipperLib.PolyFillType.pftEvenOdd); + } + }; + ClipperLib.Clipper.prototype.FixHoleLinkage = function (outRec) + { + //skip if an outermost polygon or + //already already points to the correct FirstLeft ... + if (outRec.FirstLeft === null || (outRec.IsHole != outRec.FirstLeft.IsHole && outRec.FirstLeft.Pts !== null)) + return; + var orfl = outRec.FirstLeft; + while (orfl !== null && ((orfl.IsHole == outRec.IsHole) || orfl.Pts === null)) + orfl = orfl.FirstLeft; + outRec.FirstLeft = orfl; + }; + ClipperLib.Clipper.prototype.ExecuteInternal = function () + { + try + { + this.Reset(); + if (this.m_CurrentLM === null) + return false; + var botY = this.PopScanbeam(); + do { + this.InsertLocalMinimaIntoAEL(botY); + ClipperLib.Clear(this.m_GhostJoins); + this.ProcessHorizontals(false); + if (this.m_Scanbeam === null) + break; + var topY = this.PopScanbeam(); + //console.log("botY:" + botY + ", topY:" + topY); + if (!this.ProcessIntersections(botY, topY)) + return false; + this.ProcessEdgesAtTopOfScanbeam(topY); + botY = topY; + } + while (this.m_Scanbeam !== null || this.m_CurrentLM !== null) + //fix orientations ... + for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) + { + var outRec = this.m_PolyOuts[i]; + if (outRec.Pts === null || outRec.IsOpen) + continue; + if ((outRec.IsHole ^ this.ReverseSolution) == (this.Area(outRec) > 0)) + this.ReversePolyPtLinks(outRec.Pts); + } + this.JoinCommonEdges(); + for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) + { + var outRec = this.m_PolyOuts[i]; + if (outRec.Pts !== null && !outRec.IsOpen) + this.FixupOutPolygon(outRec); + } + if (this.StrictlySimple) + this.DoSimplePolygons(); + return true; + } + finally + { + ClipperLib.Clear(this.m_Joins); + ClipperLib.Clear(this.m_GhostJoins); + } + }; + ClipperLib.Clipper.prototype.PopScanbeam = function () + { + var Y = this.m_Scanbeam.Y; + var sb2 = this.m_Scanbeam; + this.m_Scanbeam = this.m_Scanbeam.Next; + sb2 = null; + return Y; + }; + ClipperLib.Clipper.prototype.DisposeAllPolyPts = function () + { + for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; ++i) + this.DisposeOutRec(i); + ClipperLib.Clear(this.m_PolyOuts); + }; + ClipperLib.Clipper.prototype.DisposeOutRec = function (index) + { + var outRec = this.m_PolyOuts[index]; + if (outRec.Pts !== null) + this.DisposeOutPts(outRec.Pts); + outRec = null; + this.m_PolyOuts[index] = null; + }; + ClipperLib.Clipper.prototype.DisposeOutPts = function (pp) + { + if (pp === null) + return; + var tmpPp = null; + pp.Prev.Next = null; + while (pp !== null) + { + tmpPp = pp; + pp = pp.Next; + tmpPp = null; + } + }; + ClipperLib.Clipper.prototype.AddJoin = function (Op1, Op2, OffPt) + { + var j = new ClipperLib.Join(); + j.OutPt1 = Op1; + j.OutPt2 = Op2; + //j.OffPt = OffPt; + j.OffPt.X = OffPt.X; + j.OffPt.Y = OffPt.Y; + this.m_Joins.push(j); + }; + ClipperLib.Clipper.prototype.AddGhostJoin = function (Op, OffPt) + { + var j = new ClipperLib.Join(); + j.OutPt1 = Op; + //j.OffPt = OffPt; + j.OffPt.X = OffPt.X; + j.OffPt.Y = OffPt.Y; + this.m_GhostJoins.push(j); + }; + if (use_xyz) + { + ClipperLib.Clipper.prototype.SetZ = function (pt, e) + { + pt.Z = 0; + if (this.ZFillFunction !== null) + { + //put the 'preferred' point as first parameter ... + if (e.OutIdx < 0) + this.ZFillFunction(e.Bot, e.Top, pt); //outside a path so presume entering + else + this.ZFillFunction(e.Top, e.Bot, pt); //inside a path so presume exiting + } + }; + //------------------------------------------------------------------------------ + } + ClipperLib.Clipper.prototype.InsertLocalMinimaIntoAEL = function (botY) + { + while (this.m_CurrentLM !== null && (this.m_CurrentLM.Y == botY)) + { + var lb = this.m_CurrentLM.LeftBound; + var rb = this.m_CurrentLM.RightBound; + this.PopLocalMinima(); + var Op1 = null; + if (lb === null) + { + this.InsertEdgeIntoAEL(rb, null); + this.SetWindingCount(rb); + if (this.IsContributing(rb)) + Op1 = this.AddOutPt(rb, rb.Bot); + } + else if (rb == null) + { + this.InsertEdgeIntoAEL(lb, null); + this.SetWindingCount(lb); + if (this.IsContributing(lb)) + Op1 = this.AddOutPt(lb, lb.Bot); + this.InsertScanbeam(lb.Top.Y); + } + else + { + this.InsertEdgeIntoAEL(lb, null); + this.InsertEdgeIntoAEL(rb, lb); + this.SetWindingCount(lb); + rb.WindCnt = lb.WindCnt; + rb.WindCnt2 = lb.WindCnt2; + if (this.IsContributing(lb)) + Op1 = this.AddLocalMinPoly(lb, rb, lb.Bot); + this.InsertScanbeam(lb.Top.Y); + } + if (rb != null) + { + if (ClipperLib.ClipperBase.IsHorizontal(rb)) + this.AddEdgeToSEL(rb); + else + this.InsertScanbeam(rb.Top.Y); + } + if (lb == null || rb == null) continue; + //if output polygons share an Edge with a horizontal rb, they'll need joining later ... + if (Op1 !== null && ClipperLib.ClipperBase.IsHorizontal(rb) && this.m_GhostJoins.length > 0 && rb.WindDelta !== 0) + { + for (var i = 0, ilen = this.m_GhostJoins.length; i < ilen; i++) + { + //if the horizontal Rb and a 'ghost' horizontal overlap, then convert + //the 'ghost' join to a real join ready for later ... + var j = this.m_GhostJoins[i]; + if (this.HorzSegmentsOverlap(j.OutPt1.Pt, j.OffPt, rb.Bot, rb.Top)) + this.AddJoin(j.OutPt1, Op1, j.OffPt); + } + } + if (lb.OutIdx >= 0 && lb.PrevInAEL !== null && + lb.PrevInAEL.Curr.X == lb.Bot.X && + lb.PrevInAEL.OutIdx >= 0 && + ClipperLib.ClipperBase.SlopesEqual(lb.PrevInAEL, lb, this.m_UseFullRange) && + lb.WindDelta !== 0 && lb.PrevInAEL.WindDelta !== 0) + { + var Op2 = this.AddOutPt(lb.PrevInAEL, lb.Bot); + this.AddJoin(Op1, Op2, lb.Top); + } + if (lb.NextInAEL != rb) + { + if (rb.OutIdx >= 0 && rb.PrevInAEL.OutIdx >= 0 && + ClipperLib.ClipperBase.SlopesEqual(rb.PrevInAEL, rb, this.m_UseFullRange) && + rb.WindDelta !== 0 && rb.PrevInAEL.WindDelta !== 0) + { + var Op2 = this.AddOutPt(rb.PrevInAEL, rb.Bot); + this.AddJoin(Op1, Op2, rb.Top); + } + var e = lb.NextInAEL; + if (e !== null) + while (e != rb) + { + //nb: For calculating winding counts etc, IntersectEdges() assumes + //that param1 will be to the right of param2 ABOVE the intersection ... + this.IntersectEdges(rb, e, lb.Curr, false); + //order important here + e = e.NextInAEL; + } + } + } + }; + ClipperLib.Clipper.prototype.InsertEdgeIntoAEL = function (edge, startEdge) + { + if (this.m_ActiveEdges === null) + { + edge.PrevInAEL = null; + edge.NextInAEL = null; + this.m_ActiveEdges = edge; + } + else if (startEdge === null && this.E2InsertsBeforeE1(this.m_ActiveEdges, edge)) + { + edge.PrevInAEL = null; + edge.NextInAEL = this.m_ActiveEdges; + this.m_ActiveEdges.PrevInAEL = edge; + this.m_ActiveEdges = edge; + } + else + { + if (startEdge === null) + startEdge = this.m_ActiveEdges; + while (startEdge.NextInAEL !== null && !this.E2InsertsBeforeE1(startEdge.NextInAEL, edge)) + startEdge = startEdge.NextInAEL; + edge.NextInAEL = startEdge.NextInAEL; + if (startEdge.NextInAEL !== null) + startEdge.NextInAEL.PrevInAEL = edge; + edge.PrevInAEL = startEdge; + startEdge.NextInAEL = edge; + } + }; + ClipperLib.Clipper.prototype.E2InsertsBeforeE1 = function (e1, e2) + { + if (e2.Curr.X == e1.Curr.X) + { + if (e2.Top.Y > e1.Top.Y) + return e2.Top.X < ClipperLib.Clipper.TopX(e1, e2.Top.Y); + else + return e1.Top.X > ClipperLib.Clipper.TopX(e2, e1.Top.Y); + } + else + return e2.Curr.X < e1.Curr.X; + }; + ClipperLib.Clipper.prototype.IsEvenOddFillType = function (edge) + { + if (edge.PolyTyp == ClipperLib.PolyType.ptSubject) + return this.m_SubjFillType == ClipperLib.PolyFillType.pftEvenOdd; + else + return this.m_ClipFillType == ClipperLib.PolyFillType.pftEvenOdd; + }; + ClipperLib.Clipper.prototype.IsEvenOddAltFillType = function (edge) + { + if (edge.PolyTyp == ClipperLib.PolyType.ptSubject) + return this.m_ClipFillType == ClipperLib.PolyFillType.pftEvenOdd; + else + return this.m_SubjFillType == ClipperLib.PolyFillType.pftEvenOdd; + }; + ClipperLib.Clipper.prototype.IsContributing = function (edge) + { + var pft, pft2; + if (edge.PolyTyp == ClipperLib.PolyType.ptSubject) + { + pft = this.m_SubjFillType; + pft2 = this.m_ClipFillType; + } + else + { + pft = this.m_ClipFillType; + pft2 = this.m_SubjFillType; + } + switch (pft) + { + case ClipperLib.PolyFillType.pftEvenOdd: + if (edge.WindDelta === 0 && edge.WindCnt != 1) + return false; + break; + case ClipperLib.PolyFillType.pftNonZero: + if (Math.abs(edge.WindCnt) != 1) + return false; + break; + case ClipperLib.PolyFillType.pftPositive: + if (edge.WindCnt != 1) + return false; + break; + default: + if (edge.WindCnt != -1) + return false; + break; + } + switch (this.m_ClipType) + { + case ClipperLib.ClipType.ctIntersection: + switch (pft2) + { + case ClipperLib.PolyFillType.pftEvenOdd: + case ClipperLib.PolyFillType.pftNonZero: + return (edge.WindCnt2 !== 0); + case ClipperLib.PolyFillType.pftPositive: + return (edge.WindCnt2 > 0); + default: + return (edge.WindCnt2 < 0); + } + case ClipperLib.ClipType.ctUnion: + switch (pft2) + { + case ClipperLib.PolyFillType.pftEvenOdd: + case ClipperLib.PolyFillType.pftNonZero: + return (edge.WindCnt2 === 0); + case ClipperLib.PolyFillType.pftPositive: + return (edge.WindCnt2 <= 0); + default: + return (edge.WindCnt2 >= 0); + } + case ClipperLib.ClipType.ctDifference: + if (edge.PolyTyp == ClipperLib.PolyType.ptSubject) + switch (pft2) + { + case ClipperLib.PolyFillType.pftEvenOdd: + case ClipperLib.PolyFillType.pftNonZero: + return (edge.WindCnt2 === 0); + case ClipperLib.PolyFillType.pftPositive: + return (edge.WindCnt2 <= 0); + default: + return (edge.WindCnt2 >= 0); + } + else + switch (pft2) + { + case ClipperLib.PolyFillType.pftEvenOdd: + case ClipperLib.PolyFillType.pftNonZero: + return (edge.WindCnt2 !== 0); + case ClipperLib.PolyFillType.pftPositive: + return (edge.WindCnt2 > 0); + default: + return (edge.WindCnt2 < 0); + } + case ClipperLib.ClipType.ctXor: + if (edge.WindDelta === 0) + switch (pft2) + { + case ClipperLib.PolyFillType.pftEvenOdd: + case ClipperLib.PolyFillType.pftNonZero: + return (edge.WindCnt2 === 0); + case ClipperLib.PolyFillType.pftPositive: + return (edge.WindCnt2 <= 0); + default: + return (edge.WindCnt2 >= 0); + } + else + return true; + } + return true; + }; + ClipperLib.Clipper.prototype.SetWindingCount = function (edge) + { + var e = edge.PrevInAEL; + //find the edge of the same polytype that immediately preceeds 'edge' in AEL + while (e !== null && ((e.PolyTyp != edge.PolyTyp) || (e.WindDelta === 0))) + e = e.PrevInAEL; + if (e === null) + { + edge.WindCnt = (edge.WindDelta === 0 ? 1 : edge.WindDelta); + edge.WindCnt2 = 0; + e = this.m_ActiveEdges; + //ie get ready to calc WindCnt2 + } + else if (edge.WindDelta === 0 && this.m_ClipType != ClipperLib.ClipType.ctUnion) + { + edge.WindCnt = 1; + edge.WindCnt2 = e.WindCnt2; + e = e.NextInAEL; + //ie get ready to calc WindCnt2 + } + else if (this.IsEvenOddFillType(edge)) + { + //EvenOdd filling ... + if (edge.WindDelta === 0) + { + //are we inside a subj polygon ... + var Inside = true; + var e2 = e.PrevInAEL; + while (e2 !== null) + { + if (e2.PolyTyp == e.PolyTyp && e2.WindDelta !== 0) + Inside = !Inside; + e2 = e2.PrevInAEL; + } + edge.WindCnt = (Inside ? 0 : 1); + } + else + { + edge.WindCnt = edge.WindDelta; + } + edge.WindCnt2 = e.WindCnt2; + e = e.NextInAEL; + //ie get ready to calc WindCnt2 + } + else + { + //nonZero, Positive or Negative filling ... + if (e.WindCnt * e.WindDelta < 0) + { + //prev edge is 'decreasing' WindCount (WC) toward zero + //so we're outside the previous polygon ... + if (Math.abs(e.WindCnt) > 1) + { + //outside prev poly but still inside another. + //when reversing direction of prev poly use the same WC + if (e.WindDelta * edge.WindDelta < 0) + edge.WindCnt = e.WindCnt; + else + edge.WindCnt = e.WindCnt + edge.WindDelta; + } + else + edge.WindCnt = (edge.WindDelta === 0 ? 1 : edge.WindDelta); + } + else + { + //prev edge is 'increasing' WindCount (WC) away from zero + //so we're inside the previous polygon ... + if (edge.WindDelta === 0) + edge.WindCnt = (e.WindCnt < 0 ? e.WindCnt - 1 : e.WindCnt + 1); + else if (e.WindDelta * edge.WindDelta < 0) + edge.WindCnt = e.WindCnt; + else + edge.WindCnt = e.WindCnt + edge.WindDelta; + } + edge.WindCnt2 = e.WindCnt2; + e = e.NextInAEL; + //ie get ready to calc WindCnt2 + } + //update WindCnt2 ... + if (this.IsEvenOddAltFillType(edge)) + { + //EvenOdd filling ... + while (e != edge) + { + if (e.WindDelta !== 0) + edge.WindCnt2 = (edge.WindCnt2 === 0 ? 1 : 0); + e = e.NextInAEL; + } + } + else + { + //nonZero, Positive or Negative filling ... + while (e != edge) + { + edge.WindCnt2 += e.WindDelta; + e = e.NextInAEL; + } + } + }; + ClipperLib.Clipper.prototype.AddEdgeToSEL = function (edge) + { + //SEL pointers in PEdge are reused to build a list of horizontal edges. + //However, we don't need to worry about order with horizontal edge processing. + if (this.m_SortedEdges === null) + { + this.m_SortedEdges = edge; + edge.PrevInSEL = null; + edge.NextInSEL = null; + } + else + { + edge.NextInSEL = this.m_SortedEdges; + edge.PrevInSEL = null; + this.m_SortedEdges.PrevInSEL = edge; + this.m_SortedEdges = edge; + } + }; + ClipperLib.Clipper.prototype.CopyAELToSEL = function () + { + var e = this.m_ActiveEdges; + this.m_SortedEdges = e; + while (e !== null) + { + e.PrevInSEL = e.PrevInAEL; + e.NextInSEL = e.NextInAEL; + e = e.NextInAEL; + } + }; + ClipperLib.Clipper.prototype.SwapPositionsInAEL = function (edge1, edge2) + { + //check that one or other edge hasn't already been removed from AEL ... + if (edge1.NextInAEL == edge1.PrevInAEL || edge2.NextInAEL == edge2.PrevInAEL) + return; + if (edge1.NextInAEL == edge2) + { + var next = edge2.NextInAEL; + if (next !== null) + next.PrevInAEL = edge1; + var prev = edge1.PrevInAEL; + if (prev !== null) + prev.NextInAEL = edge2; + edge2.PrevInAEL = prev; + edge2.NextInAEL = edge1; + edge1.PrevInAEL = edge2; + edge1.NextInAEL = next; + } + else if (edge2.NextInAEL == edge1) + { + var next = edge1.NextInAEL; + if (next !== null) + next.PrevInAEL = edge2; + var prev = edge2.PrevInAEL; + if (prev !== null) + prev.NextInAEL = edge1; + edge1.PrevInAEL = prev; + edge1.NextInAEL = edge2; + edge2.PrevInAEL = edge1; + edge2.NextInAEL = next; + } + else + { + var next = edge1.NextInAEL; + var prev = edge1.PrevInAEL; + edge1.NextInAEL = edge2.NextInAEL; + if (edge1.NextInAEL !== null) + edge1.NextInAEL.PrevInAEL = edge1; + edge1.PrevInAEL = edge2.PrevInAEL; + if (edge1.PrevInAEL !== null) + edge1.PrevInAEL.NextInAEL = edge1; + edge2.NextInAEL = next; + if (edge2.NextInAEL !== null) + edge2.NextInAEL.PrevInAEL = edge2; + edge2.PrevInAEL = prev; + if (edge2.PrevInAEL !== null) + edge2.PrevInAEL.NextInAEL = edge2; + } + if (edge1.PrevInAEL === null) + this.m_ActiveEdges = edge1; + else if (edge2.PrevInAEL === null) + this.m_ActiveEdges = edge2; + }; + ClipperLib.Clipper.prototype.SwapPositionsInSEL = function (edge1, edge2) + { + if (edge1.NextInSEL === null && edge1.PrevInSEL === null) + return; + if (edge2.NextInSEL === null && edge2.PrevInSEL === null) + return; + if (edge1.NextInSEL == edge2) + { + var next = edge2.NextInSEL; + if (next !== null) + next.PrevInSEL = edge1; + var prev = edge1.PrevInSEL; + if (prev !== null) + prev.NextInSEL = edge2; + edge2.PrevInSEL = prev; + edge2.NextInSEL = edge1; + edge1.PrevInSEL = edge2; + edge1.NextInSEL = next; + } + else if (edge2.NextInSEL == edge1) + { + var next = edge1.NextInSEL; + if (next !== null) + next.PrevInSEL = edge2; + var prev = edge2.PrevInSEL; + if (prev !== null) + prev.NextInSEL = edge1; + edge1.PrevInSEL = prev; + edge1.NextInSEL = edge2; + edge2.PrevInSEL = edge1; + edge2.NextInSEL = next; + } + else + { + var next = edge1.NextInSEL; + var prev = edge1.PrevInSEL; + edge1.NextInSEL = edge2.NextInSEL; + if (edge1.NextInSEL !== null) + edge1.NextInSEL.PrevInSEL = edge1; + edge1.PrevInSEL = edge2.PrevInSEL; + if (edge1.PrevInSEL !== null) + edge1.PrevInSEL.NextInSEL = edge1; + edge2.NextInSEL = next; + if (edge2.NextInSEL !== null) + edge2.NextInSEL.PrevInSEL = edge2; + edge2.PrevInSEL = prev; + if (edge2.PrevInSEL !== null) + edge2.PrevInSEL.NextInSEL = edge2; + } + if (edge1.PrevInSEL === null) + this.m_SortedEdges = edge1; + else if (edge2.PrevInSEL === null) + this.m_SortedEdges = edge2; + }; + ClipperLib.Clipper.prototype.AddLocalMaxPoly = function (e1, e2, pt) + { + this.AddOutPt(e1, pt); + if (e2.WindDelta == 0) this.AddOutPt(e2, pt); + if (e1.OutIdx == e2.OutIdx) + { + e1.OutIdx = -1; + e2.OutIdx = -1; + } + else if (e1.OutIdx < e2.OutIdx) + this.AppendPolygon(e1, e2); + else + this.AppendPolygon(e2, e1); + }; + ClipperLib.Clipper.prototype.AddLocalMinPoly = function (e1, e2, pt) + { + var result; + var e, prevE; + if (ClipperLib.ClipperBase.IsHorizontal(e2) || (e1.Dx > e2.Dx)) + { + result = this.AddOutPt(e1, pt); + e2.OutIdx = e1.OutIdx; + e1.Side = ClipperLib.EdgeSide.esLeft; + e2.Side = ClipperLib.EdgeSide.esRight; + e = e1; + if (e.PrevInAEL == e2) + prevE = e2.PrevInAEL; + else + prevE = e.PrevInAEL; + } + else + { + result = this.AddOutPt(e2, pt); + e1.OutIdx = e2.OutIdx; + e1.Side = ClipperLib.EdgeSide.esRight; + e2.Side = ClipperLib.EdgeSide.esLeft; + e = e2; + if (e.PrevInAEL == e1) + prevE = e1.PrevInAEL; + else + prevE = e.PrevInAEL; + } + if (prevE !== null && prevE.OutIdx >= 0 && (ClipperLib.Clipper.TopX(prevE, pt.Y) == ClipperLib.Clipper.TopX(e, pt.Y)) && ClipperLib.ClipperBase.SlopesEqual(e, prevE, this.m_UseFullRange) && (e.WindDelta !== 0) && (prevE.WindDelta !== 0)) + { + var outPt = this.AddOutPt(prevE, pt); + this.AddJoin(result, outPt, e.Top); + } + return result; + }; + ClipperLib.Clipper.prototype.CreateOutRec = function () + { + var result = new ClipperLib.OutRec(); + result.Idx = -1; + result.IsHole = false; + result.IsOpen = false; + result.FirstLeft = null; + result.Pts = null; + result.BottomPt = null; + result.PolyNode = null; + this.m_PolyOuts.push(result); + result.Idx = this.m_PolyOuts.length - 1; + return result; + }; + ClipperLib.Clipper.prototype.AddOutPt = function (e, pt) + { + var ToFront = (e.Side == ClipperLib.EdgeSide.esLeft); + if (e.OutIdx < 0) + { + var outRec = this.CreateOutRec(); + outRec.IsOpen = (e.WindDelta === 0); + var newOp = new ClipperLib.OutPt(); + outRec.Pts = newOp; + newOp.Idx = outRec.Idx; + //newOp.Pt = pt; + newOp.Pt.X = pt.X; + newOp.Pt.Y = pt.Y; + newOp.Next = newOp; + newOp.Prev = newOp; + if (!outRec.IsOpen) + this.SetHoleState(e, outRec); + if (use_xyz) + { + if (ClipperLib.IntPoint.op_Equality(pt, e.Bot)) + { + //newOp.Pt = e.Bot; + newOp.Pt.X = e.Bot.X; + newOp.Pt.Y = e.Bot.Y; + newOp.Pt.Z = e.Bot.Z; + } + else if (ClipperLib.IntPoint.op_Equality(pt, e.Top)) + { + //newOp.Pt = e.Top; + newOp.Pt.X = e.Top.X; + newOp.Pt.Y = e.Top.Y; + newOp.Pt.Z = e.Top.Z; + } + else + this.SetZ(newOp.Pt, e); + } + e.OutIdx = outRec.Idx; + //nb: do this after SetZ ! + return newOp; + } + else + { + var outRec = this.m_PolyOuts[e.OutIdx]; + //OutRec.Pts is the 'Left-most' point & OutRec.Pts.Prev is the 'Right-most' + var op = outRec.Pts; + if (ToFront && ClipperLib.IntPoint.op_Equality(pt, op.Pt)) + return op; + else if (!ToFront && ClipperLib.IntPoint.op_Equality(pt, op.Prev.Pt)) + return op.Prev; + var newOp = new ClipperLib.OutPt(); + newOp.Idx = outRec.Idx; + //newOp.Pt = pt; + newOp.Pt.X = pt.X; + newOp.Pt.Y = pt.Y; + newOp.Next = op; + newOp.Prev = op.Prev; + newOp.Prev.Next = newOp; + op.Prev = newOp; + if (ToFront) + outRec.Pts = newOp; + if (use_xyz) + { + if (ClipperLib.IntPoint.op_Equality(pt, e.Bot)) + { + //newOp.Pt = e.Bot; + newOp.Pt.X = e.Bot.X; + newOp.Pt.Y = e.Bot.Y; + newOp.Pt.Z = e.Bot.Z; + } + else if (ClipperLib.IntPoint.op_Equality(pt, e.Top)) + { + //newOp.Pt = e.Top; + newOp.Pt.X = e.Top.X; + newOp.Pt.Y = e.Top.Y; + newOp.Pt.Z = e.Top.Z; + } + else + this.SetZ(newOp.Pt, e); + } + return newOp; + } + }; + ClipperLib.Clipper.prototype.SwapPoints = function (pt1, pt2) + { + var tmp = new ClipperLib.IntPoint(pt1.Value); + //pt1.Value = pt2.Value; + pt1.Value.X = pt2.Value.X; + pt1.Value.Y = pt2.Value.Y; + //pt2.Value = tmp; + pt2.Value.X = tmp.X; + pt2.Value.Y = tmp.Y; + }; + ClipperLib.Clipper.prototype.HorzSegmentsOverlap = function (Pt1a, Pt1b, Pt2a, Pt2b) + { + //precondition: both segments are horizontal + if ((Pt1a.X > Pt2a.X) == (Pt1a.X < Pt2b.X)) + return true; + else if ((Pt1b.X > Pt2a.X) == (Pt1b.X < Pt2b.X)) + return true; + else if ((Pt2a.X > Pt1a.X) == (Pt2a.X < Pt1b.X)) + return true; + else if ((Pt2b.X > Pt1a.X) == (Pt2b.X < Pt1b.X)) + return true; + else if ((Pt1a.X == Pt2a.X) && (Pt1b.X == Pt2b.X)) + return true; + else if ((Pt1a.X == Pt2b.X) && (Pt1b.X == Pt2a.X)) + return true; + else + return false; + }; + ClipperLib.Clipper.prototype.InsertPolyPtBetween = function (p1, p2, pt) + { + var result = new ClipperLib.OutPt(); + //result.Pt = pt; + result.Pt.X = pt.X; + result.Pt.Y = pt.Y; + if (p2 == p1.Next) + { + p1.Next = result; + p2.Prev = result; + result.Next = p2; + result.Prev = p1; + } + else + { + p2.Next = result; + p1.Prev = result; + result.Next = p1; + result.Prev = p2; + } + return result; + }; + ClipperLib.Clipper.prototype.SetHoleState = function (e, outRec) + { + var isHole = false; + var e2 = e.PrevInAEL; + while (e2 !== null) + { + if (e2.OutIdx >= 0 && e2.WindDelta != 0) + { + isHole = !isHole; + if (outRec.FirstLeft === null) + outRec.FirstLeft = this.m_PolyOuts[e2.OutIdx]; + } + e2 = e2.PrevInAEL; + } + if (isHole) + outRec.IsHole = true; + }; + ClipperLib.Clipper.prototype.GetDx = function (pt1, pt2) + { + if (pt1.Y == pt2.Y) + return ClipperLib.ClipperBase.horizontal; + else + return (pt2.X - pt1.X) / (pt2.Y - pt1.Y); + }; + ClipperLib.Clipper.prototype.FirstIsBottomPt = function (btmPt1, btmPt2) + { + var p = btmPt1.Prev; + while ((ClipperLib.IntPoint.op_Equality(p.Pt, btmPt1.Pt)) && (p != btmPt1)) + p = p.Prev; + var dx1p = Math.abs(this.GetDx(btmPt1.Pt, p.Pt)); + p = btmPt1.Next; + while ((ClipperLib.IntPoint.op_Equality(p.Pt, btmPt1.Pt)) && (p != btmPt1)) + p = p.Next; + var dx1n = Math.abs(this.GetDx(btmPt1.Pt, p.Pt)); + p = btmPt2.Prev; + while ((ClipperLib.IntPoint.op_Equality(p.Pt, btmPt2.Pt)) && (p != btmPt2)) + p = p.Prev; + var dx2p = Math.abs(this.GetDx(btmPt2.Pt, p.Pt)); + p = btmPt2.Next; + while ((ClipperLib.IntPoint.op_Equality(p.Pt, btmPt2.Pt)) && (p != btmPt2)) + p = p.Next; + var dx2n = Math.abs(this.GetDx(btmPt2.Pt, p.Pt)); + return (dx1p >= dx2p && dx1p >= dx2n) || (dx1n >= dx2p && dx1n >= dx2n); + }; + ClipperLib.Clipper.prototype.GetBottomPt = function (pp) + { + var dups = null; + var p = pp.Next; + while (p != pp) + { + if (p.Pt.Y > pp.Pt.Y) + { + pp = p; + dups = null; + } + else if (p.Pt.Y == pp.Pt.Y && p.Pt.X <= pp.Pt.X) + { + if (p.Pt.X < pp.Pt.X) + { + dups = null; + pp = p; + } + else + { + if (p.Next != pp && p.Prev != pp) + dups = p; + } + } + p = p.Next; + } + if (dups !== null) + { + //there appears to be at least 2 vertices at bottomPt so ... + while (dups != p) + { + if (!this.FirstIsBottomPt(p, dups)) + pp = dups; + dups = dups.Next; + while (ClipperLib.IntPoint.op_Inequality(dups.Pt, pp.Pt)) + dups = dups.Next; + } + } + return pp; + }; + ClipperLib.Clipper.prototype.GetLowermostRec = function (outRec1, outRec2) + { + //work out which polygon fragment has the correct hole state ... + if (outRec1.BottomPt === null) + outRec1.BottomPt = this.GetBottomPt(outRec1.Pts); + if (outRec2.BottomPt === null) + outRec2.BottomPt = this.GetBottomPt(outRec2.Pts); + var bPt1 = outRec1.BottomPt; + var bPt2 = outRec2.BottomPt; + if (bPt1.Pt.Y > bPt2.Pt.Y) + return outRec1; + else if (bPt1.Pt.Y < bPt2.Pt.Y) + return outRec2; + else if (bPt1.Pt.X < bPt2.Pt.X) + return outRec1; + else if (bPt1.Pt.X > bPt2.Pt.X) + return outRec2; + else if (bPt1.Next == bPt1) + return outRec2; + else if (bPt2.Next == bPt2) + return outRec1; + else if (this.FirstIsBottomPt(bPt1, bPt2)) + return outRec1; + else + return outRec2; + }; + ClipperLib.Clipper.prototype.Param1RightOfParam2 = function (outRec1, outRec2) + { + do { + outRec1 = outRec1.FirstLeft; + if (outRec1 == outRec2) + return true; + } + while (outRec1 !== null) + return false; + }; + ClipperLib.Clipper.prototype.GetOutRec = function (idx) + { + var outrec = this.m_PolyOuts[idx]; + while (outrec != this.m_PolyOuts[outrec.Idx]) + outrec = this.m_PolyOuts[outrec.Idx]; + return outrec; + }; + ClipperLib.Clipper.prototype.AppendPolygon = function (e1, e2) + { + //get the start and ends of both output polygons ... + var outRec1 = this.m_PolyOuts[e1.OutIdx]; + var outRec2 = this.m_PolyOuts[e2.OutIdx]; + var holeStateRec; + if (this.Param1RightOfParam2(outRec1, outRec2)) + holeStateRec = outRec2; + else if (this.Param1RightOfParam2(outRec2, outRec1)) + holeStateRec = outRec1; + else + holeStateRec = this.GetLowermostRec(outRec1, outRec2); + var p1_lft = outRec1.Pts; + var p1_rt = p1_lft.Prev; + var p2_lft = outRec2.Pts; + var p2_rt = p2_lft.Prev; + var side; + //join e2 poly onto e1 poly and delete pointers to e2 ... + if (e1.Side == ClipperLib.EdgeSide.esLeft) + { + if (e2.Side == ClipperLib.EdgeSide.esLeft) + { + //z y x a b c + this.ReversePolyPtLinks(p2_lft); + p2_lft.Next = p1_lft; + p1_lft.Prev = p2_lft; + p1_rt.Next = p2_rt; + p2_rt.Prev = p1_rt; + outRec1.Pts = p2_rt; + } + else + { + //x y z a b c + p2_rt.Next = p1_lft; + p1_lft.Prev = p2_rt; + p2_lft.Prev = p1_rt; + p1_rt.Next = p2_lft; + outRec1.Pts = p2_lft; + } + side = ClipperLib.EdgeSide.esLeft; + } + else + { + if (e2.Side == ClipperLib.EdgeSide.esRight) + { + //a b c z y x + this.ReversePolyPtLinks(p2_lft); + p1_rt.Next = p2_rt; + p2_rt.Prev = p1_rt; + p2_lft.Next = p1_lft; + p1_lft.Prev = p2_lft; + } + else + { + //a b c x y z + p1_rt.Next = p2_lft; + p2_lft.Prev = p1_rt; + p1_lft.Prev = p2_rt; + p2_rt.Next = p1_lft; + } + side = ClipperLib.EdgeSide.esRight; + } + outRec1.BottomPt = null; + if (holeStateRec == outRec2) + { + if (outRec2.FirstLeft != outRec1) + outRec1.FirstLeft = outRec2.FirstLeft; + outRec1.IsHole = outRec2.IsHole; + } + outRec2.Pts = null; + outRec2.BottomPt = null; + outRec2.FirstLeft = outRec1; + var OKIdx = e1.OutIdx; + var ObsoleteIdx = e2.OutIdx; + e1.OutIdx = -1; + //nb: safe because we only get here via AddLocalMaxPoly + e2.OutIdx = -1; + var e = this.m_ActiveEdges; + while (e !== null) + { + if (e.OutIdx == ObsoleteIdx) + { + e.OutIdx = OKIdx; + e.Side = side; + break; + } + e = e.NextInAEL; + } + outRec2.Idx = outRec1.Idx; + }; + ClipperLib.Clipper.prototype.ReversePolyPtLinks = function (pp) + { + if (pp === null) + return; + var pp1; + var pp2; + pp1 = pp; + do { + pp2 = pp1.Next; + pp1.Next = pp1.Prev; + pp1.Prev = pp2; + pp1 = pp2; + } + while (pp1 != pp) + }; + ClipperLib.Clipper.SwapSides = function (edge1, edge2) + { + var side = edge1.Side; + edge1.Side = edge2.Side; + edge2.Side = side; + }; + ClipperLib.Clipper.SwapPolyIndexes = function (edge1, edge2) + { + var outIdx = edge1.OutIdx; + edge1.OutIdx = edge2.OutIdx; + edge2.OutIdx = outIdx; + }; + ClipperLib.Clipper.prototype.IntersectEdges = function (e1, e2, pt, protect) + { + //e1 will be to the left of e2 BELOW the intersection. Therefore e1 is before + //e2 in AEL except when e1 is being inserted at the intersection point ... + var e1stops = !protect && e1.NextInLML === null && + e1.Top.X == pt.X && e1.Top.Y == pt.Y; + var e2stops = !protect && e2.NextInLML === null && + e2.Top.X == pt.X && e2.Top.Y == pt.Y; + var e1Contributing = (e1.OutIdx >= 0); + var e2Contributing = (e2.OutIdx >= 0); + if (use_lines) + { + //if either edge is on an OPEN path ... + if (e1.WindDelta === 0 || e2.WindDelta === 0) + { + //ignore subject-subject open path intersections UNLESS they + //are both open paths, AND they are both 'contributing maximas' ... + if (e1.WindDelta === 0 && e2.WindDelta === 0) + { + if ((e1stops || e2stops) && e1Contributing && e2Contributing) + this.AddLocalMaxPoly(e1, e2, pt); + } + //if intersecting a subj line with a subj poly ... + else if (e1.PolyTyp == e2.PolyTyp && + e1.WindDelta != e2.WindDelta && this.m_ClipType == ClipperLib.ClipType.ctUnion) + { + if (e1.WindDelta === 0) + { + if (e2Contributing) + { + this.AddOutPt(e1, pt); + if (e1Contributing) + e1.OutIdx = -1; + } + } + else + { + if (e1Contributing) + { + this.AddOutPt(e2, pt); + if (e2Contributing) + e2.OutIdx = -1; + } + } + } + else if (e1.PolyTyp != e2.PolyTyp) + { + if ((e1.WindDelta === 0) && Math.abs(e2.WindCnt) == 1 && + (this.m_ClipType != ClipperLib.ClipType.ctUnion || e2.WindCnt2 === 0)) + { + this.AddOutPt(e1, pt); + if (e1Contributing) + e1.OutIdx = -1; + } + else if ((e2.WindDelta === 0) && (Math.abs(e1.WindCnt) == 1) && + (this.m_ClipType != ClipperLib.ClipType.ctUnion || e1.WindCnt2 === 0)) + { + this.AddOutPt(e2, pt); + if (e2Contributing) + e2.OutIdx = -1; + } + } + if (e1stops) + if (e1.OutIdx < 0) + this.DeleteFromAEL(e1); + else + ClipperLib.Error("Error intersecting polylines"); + if (e2stops) + if (e2.OutIdx < 0) + this.DeleteFromAEL(e2); + else + ClipperLib.Error("Error intersecting polylines"); + return; + } + } + //update winding counts... + //assumes that e1 will be to the Right of e2 ABOVE the intersection + if (e1.PolyTyp == e2.PolyTyp) + { + if (this.IsEvenOddFillType(e1)) + { + var oldE1WindCnt = e1.WindCnt; + e1.WindCnt = e2.WindCnt; + e2.WindCnt = oldE1WindCnt; + } + else + { + if (e1.WindCnt + e2.WindDelta === 0) + e1.WindCnt = -e1.WindCnt; + else + e1.WindCnt += e2.WindDelta; + if (e2.WindCnt - e1.WindDelta === 0) + e2.WindCnt = -e2.WindCnt; + else + e2.WindCnt -= e1.WindDelta; + } + } + else + { + if (!this.IsEvenOddFillType(e2)) + e1.WindCnt2 += e2.WindDelta; + else + e1.WindCnt2 = (e1.WindCnt2 === 0) ? 1 : 0; + if (!this.IsEvenOddFillType(e1)) + e2.WindCnt2 -= e1.WindDelta; + else + e2.WindCnt2 = (e2.WindCnt2 === 0) ? 1 : 0; + } + var e1FillType, e2FillType, e1FillType2, e2FillType2; + if (e1.PolyTyp == ClipperLib.PolyType.ptSubject) + { + e1FillType = this.m_SubjFillType; + e1FillType2 = this.m_ClipFillType; + } + else + { + e1FillType = this.m_ClipFillType; + e1FillType2 = this.m_SubjFillType; + } + if (e2.PolyTyp == ClipperLib.PolyType.ptSubject) + { + e2FillType = this.m_SubjFillType; + e2FillType2 = this.m_ClipFillType; + } + else + { + e2FillType = this.m_ClipFillType; + e2FillType2 = this.m_SubjFillType; + } + var e1Wc, e2Wc; + switch (e1FillType) + { + case ClipperLib.PolyFillType.pftPositive: + e1Wc = e1.WindCnt; + break; + case ClipperLib.PolyFillType.pftNegative: + e1Wc = -e1.WindCnt; + break; + default: + e1Wc = Math.abs(e1.WindCnt); + break; + } + switch (e2FillType) + { + case ClipperLib.PolyFillType.pftPositive: + e2Wc = e2.WindCnt; + break; + case ClipperLib.PolyFillType.pftNegative: + e2Wc = -e2.WindCnt; + break; + default: + e2Wc = Math.abs(e2.WindCnt); + break; + } + if (e1Contributing && e2Contributing) + { + if (e1stops || e2stops || (e1Wc !== 0 && e1Wc != 1) || (e2Wc !== 0 && e2Wc != 1) || + (e1.PolyTyp != e2.PolyTyp && this.m_ClipType != ClipperLib.ClipType.ctXor)) + this.AddLocalMaxPoly(e1, e2, pt); + else + { + this.AddOutPt(e1, pt); + this.AddOutPt(e2, pt); + ClipperLib.Clipper.SwapSides(e1, e2); + ClipperLib.Clipper.SwapPolyIndexes(e1, e2); + } + } + else if (e1Contributing) + { + if (e2Wc === 0 || e2Wc == 1) + { + this.AddOutPt(e1, pt); + ClipperLib.Clipper.SwapSides(e1, e2); + ClipperLib.Clipper.SwapPolyIndexes(e1, e2); + } + } + else if (e2Contributing) + { + if (e1Wc === 0 || e1Wc == 1) + { + this.AddOutPt(e2, pt); + ClipperLib.Clipper.SwapSides(e1, e2); + ClipperLib.Clipper.SwapPolyIndexes(e1, e2); + } + } + else if ((e1Wc === 0 || e1Wc == 1) && + (e2Wc === 0 || e2Wc == 1) && !e1stops && !e2stops) + { + //neither edge is currently contributing ... + var e1Wc2, e2Wc2; + switch (e1FillType2) + { + case ClipperLib.PolyFillType.pftPositive: + e1Wc2 = e1.WindCnt2; + break; + case ClipperLib.PolyFillType.pftNegative: + e1Wc2 = -e1.WindCnt2; + break; + default: + e1Wc2 = Math.abs(e1.WindCnt2); + break; + } + switch (e2FillType2) + { + case ClipperLib.PolyFillType.pftPositive: + e2Wc2 = e2.WindCnt2; + break; + case ClipperLib.PolyFillType.pftNegative: + e2Wc2 = -e2.WindCnt2; + break; + default: + e2Wc2 = Math.abs(e2.WindCnt2); + break; + } + if (e1.PolyTyp != e2.PolyTyp) + this.AddLocalMinPoly(e1, e2, pt); + else if (e1Wc == 1 && e2Wc == 1) + switch (this.m_ClipType) + { + case ClipperLib.ClipType.ctIntersection: + if (e1Wc2 > 0 && e2Wc2 > 0) + this.AddLocalMinPoly(e1, e2, pt); + break; + case ClipperLib.ClipType.ctUnion: + if (e1Wc2 <= 0 && e2Wc2 <= 0) + this.AddLocalMinPoly(e1, e2, pt); + break; + case ClipperLib.ClipType.ctDifference: + if (((e1.PolyTyp == ClipperLib.PolyType.ptClip) && (e1Wc2 > 0) && (e2Wc2 > 0)) || + ((e1.PolyTyp == ClipperLib.PolyType.ptSubject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) + this.AddLocalMinPoly(e1, e2, pt); + break; + case ClipperLib.ClipType.ctXor: + this.AddLocalMinPoly(e1, e2, pt); + break; + } + else + ClipperLib.Clipper.SwapSides(e1, e2); + } + if ((e1stops != e2stops) && + ((e1stops && (e1.OutIdx >= 0)) || (e2stops && (e2.OutIdx >= 0)))) + { + ClipperLib.Clipper.SwapSides(e1, e2); + ClipperLib.Clipper.SwapPolyIndexes(e1, e2); + } + //finally, delete any non-contributing maxima edges ... + if (e1stops) + this.DeleteFromAEL(e1); + if (e2stops) + this.DeleteFromAEL(e2); + }; + ClipperLib.Clipper.prototype.DeleteFromAEL = function (e) + { + var AelPrev = e.PrevInAEL; + var AelNext = e.NextInAEL; + if (AelPrev === null && AelNext === null && (e != this.m_ActiveEdges)) + return; + //already deleted + if (AelPrev !== null) + AelPrev.NextInAEL = AelNext; + else + this.m_ActiveEdges = AelNext; + if (AelNext !== null) + AelNext.PrevInAEL = AelPrev; + e.NextInAEL = null; + e.PrevInAEL = null; + }; + ClipperLib.Clipper.prototype.DeleteFromSEL = function (e) + { + var SelPrev = e.PrevInSEL; + var SelNext = e.NextInSEL; + if (SelPrev === null && SelNext === null && (e != this.m_SortedEdges)) + return; + //already deleted + if (SelPrev !== null) + SelPrev.NextInSEL = SelNext; + else + this.m_SortedEdges = SelNext; + if (SelNext !== null) + SelNext.PrevInSEL = SelPrev; + e.NextInSEL = null; + e.PrevInSEL = null; + }; + ClipperLib.Clipper.prototype.UpdateEdgeIntoAEL = function (e) + { + if (e.NextInLML === null) + ClipperLib.Error("UpdateEdgeIntoAEL: invalid call"); + var AelPrev = e.PrevInAEL; + var AelNext = e.NextInAEL; + e.NextInLML.OutIdx = e.OutIdx; + if (AelPrev !== null) + AelPrev.NextInAEL = e.NextInLML; + else + this.m_ActiveEdges = e.NextInLML; + if (AelNext !== null) + AelNext.PrevInAEL = e.NextInLML; + e.NextInLML.Side = e.Side; + e.NextInLML.WindDelta = e.WindDelta; + e.NextInLML.WindCnt = e.WindCnt; + e.NextInLML.WindCnt2 = e.WindCnt2; + e = e.NextInLML; + // e.Curr = e.Bot; + e.Curr.X = e.Bot.X; + e.Curr.Y = e.Bot.Y; + e.PrevInAEL = AelPrev; + e.NextInAEL = AelNext; + if (!ClipperLib.ClipperBase.IsHorizontal(e)) + this.InsertScanbeam(e.Top.Y); + return e; + }; + ClipperLib.Clipper.prototype.ProcessHorizontals = function (isTopOfScanbeam) + { + var horzEdge = this.m_SortedEdges; + while (horzEdge !== null) + { + this.DeleteFromSEL(horzEdge); + this.ProcessHorizontal(horzEdge, isTopOfScanbeam); + horzEdge = this.m_SortedEdges; + } + }; + ClipperLib.Clipper.prototype.GetHorzDirection = function (HorzEdge, $var) + { + if (HorzEdge.Bot.X < HorzEdge.Top.X) + { + $var.Left = HorzEdge.Bot.X; + $var.Right = HorzEdge.Top.X; + $var.Dir = ClipperLib.Direction.dLeftToRight; + } + else + { + $var.Left = HorzEdge.Top.X; + $var.Right = HorzEdge.Bot.X; + $var.Dir = ClipperLib.Direction.dRightToLeft; + } + }; + ClipperLib.Clipper.prototype.PrepareHorzJoins = function (horzEdge, isTopOfScanbeam) + { + //get the last Op for this horizontal edge + //the point may be anywhere along the horizontal ... + var outPt = this.m_PolyOuts[horzEdge.OutIdx].Pts; + if (horzEdge.Side != ClipperLib.EdgeSide.esLeft) + outPt = outPt.Prev; + //First, match up overlapping horizontal edges (eg when one polygon's + //intermediate horz edge overlaps an intermediate horz edge of another, or + //when one polygon sits on top of another) ... + //for (var i = 0, ilen = this.m_GhostJoins.length; i < ilen; ++i) { + // var j = this.m_GhostJoins[i]; + // if (this.HorzSegmentsOverlap(j.OutPt1.Pt, j.OffPt, horzEdge.Bot, horzEdge.Top)) + // this.AddJoin(j.OutPt1, outPt, j.OffPt); + //} + + //Also, since horizontal edges at the top of one SB are often removed from + //the AEL before we process the horizontal edges at the bottom of the next, + //we need to create 'ghost' Join records of 'contrubuting' horizontals that + //we can compare with horizontals at the bottom of the next SB. + if (isTopOfScanbeam) + if (ClipperLib.IntPoint.op_Equality(outPt.Pt, horzEdge.Top)) + this.AddGhostJoin(outPt, horzEdge.Bot); + else + this.AddGhostJoin(outPt, horzEdge.Top); + }; + ClipperLib.Clipper.prototype.ProcessHorizontal = function (horzEdge, isTopOfScanbeam) + { + var $var = {Dir: null, Left: null, Right: null}; + this.GetHorzDirection(horzEdge, $var); + var dir = $var.Dir; + var horzLeft = $var.Left; + var horzRight = $var.Right; + + var eLastHorz = horzEdge, + eMaxPair = null; + while (eLastHorz.NextInLML !== null && ClipperLib.ClipperBase.IsHorizontal(eLastHorz.NextInLML)) + eLastHorz = eLastHorz.NextInLML; + if (eLastHorz.NextInLML === null) + eMaxPair = this.GetMaximaPair(eLastHorz); + for (;;) + { + var IsLastHorz = (horzEdge == eLastHorz); + var e = this.GetNextInAEL(horzEdge, dir); + while (e !== null) + { + //Break if we've got to the end of an intermediate horizontal edge ... + //nb: Smaller Dx's are to the right of larger Dx's ABOVE the horizontal. + if (e.Curr.X == horzEdge.Top.X && horzEdge.NextInLML !== null && e.Dx < horzEdge.NextInLML.Dx) + break; + var eNext = this.GetNextInAEL(e, dir); + //saves eNext for later + if ((dir == ClipperLib.Direction.dLeftToRight && e.Curr.X <= horzRight) || (dir == ClipperLib.Direction.dRightToLeft && e.Curr.X >= horzLeft)) + { + + if (horzEdge.OutIdx >= 0 && horzEdge.WindDelta != 0) + this.PrepareHorzJoins(horzEdge, isTopOfScanbeam); + + //so far we're still in range of the horizontal Edge but make sure + //we're at the last of consec. horizontals when matching with eMaxPair + if (e == eMaxPair && IsLastHorz) + { + if (dir == ClipperLib.Direction.dLeftToRight) + this.IntersectEdges(horzEdge, e, e.Top, false); + else + this.IntersectEdges(e, horzEdge, e.Top, false); + if (eMaxPair.OutIdx >= 0) + ClipperLib.Error("ProcessHorizontal error"); + return; + } + else if (dir == ClipperLib.Direction.dLeftToRight) + { + var Pt = new ClipperLib.IntPoint(e.Curr.X, horzEdge.Curr.Y); + this.IntersectEdges(horzEdge, e, Pt, true); + } + else + { + var Pt = new ClipperLib.IntPoint(e.Curr.X, horzEdge.Curr.Y); + this.IntersectEdges(e, horzEdge, Pt, true); + } + this.SwapPositionsInAEL(horzEdge, e); + } + else if ((dir == ClipperLib.Direction.dLeftToRight && e.Curr.X >= horzRight) || (dir == ClipperLib.Direction.dRightToLeft && e.Curr.X <= horzLeft)) + break; + e = eNext; + } + //end while + if (horzEdge.OutIdx >= 0 && horzEdge.WindDelta !== 0) + this.PrepareHorzJoins(horzEdge, isTopOfScanbeam); + if (horzEdge.NextInLML !== null && ClipperLib.ClipperBase.IsHorizontal(horzEdge.NextInLML)) + { + horzEdge = this.UpdateEdgeIntoAEL(horzEdge); + if (horzEdge.OutIdx >= 0) + this.AddOutPt(horzEdge, horzEdge.Bot); + + var $var = {Dir: dir, Left: horzLeft, Right: horzRight}; + this.GetHorzDirection(horzEdge, $var); + dir = $var.Dir; + horzLeft = $var.Left; + horzRight = $var.Right; + } + else + break; + } + //end for (;;) + if (horzEdge.NextInLML !== null) + { + if (horzEdge.OutIdx >= 0) + { + var op1 = this.AddOutPt(horzEdge, horzEdge.Top); + horzEdge = this.UpdateEdgeIntoAEL(horzEdge); + if (horzEdge.WindDelta === 0) + return; + //nb: HorzEdge is no longer horizontal here + var ePrev = horzEdge.PrevInAEL; + var eNext = horzEdge.NextInAEL; + if (ePrev !== null && ePrev.Curr.X == horzEdge.Bot.X && + ePrev.Curr.Y == horzEdge.Bot.Y && ePrev.WindDelta !== 0 && + (ePrev.OutIdx >= 0 && ePrev.Curr.Y > ePrev.Top.Y && + ClipperLib.ClipperBase.SlopesEqual(horzEdge, ePrev, this.m_UseFullRange))) + { + var op2 = this.AddOutPt(ePrev, horzEdge.Bot); + this.AddJoin(op1, op2, horzEdge.Top); + } + else if (eNext !== null && eNext.Curr.X == horzEdge.Bot.X && + eNext.Curr.Y == horzEdge.Bot.Y && eNext.WindDelta !== 0 && + eNext.OutIdx >= 0 && eNext.Curr.Y > eNext.Top.Y && + ClipperLib.ClipperBase.SlopesEqual(horzEdge, eNext, this.m_UseFullRange)) + { + var op2 = this.AddOutPt(eNext, horzEdge.Bot); + this.AddJoin(op1, op2, horzEdge.Top); + } + } + else horzEdge = this.UpdateEdgeIntoAEL(horzEdge); + } + else if (eMaxPair !== null) + { + if (eMaxPair.OutIdx >= 0) + { + if (dir == ClipperLib.Direction.dLeftToRight) + this.IntersectEdges(horzEdge, eMaxPair, horzEdge.Top, false); + else + this.IntersectEdges(eMaxPair, horzEdge, horzEdge.Top, false); + if (eMaxPair.OutIdx >= 0) + ClipperLib.Error("ProcessHorizontal error"); + } + else + { + this.DeleteFromAEL(horzEdge); + this.DeleteFromAEL(eMaxPair); + } + } + else + { + if (horzEdge.OutIdx >= 0) + this.AddOutPt(horzEdge, horzEdge.Top); + this.DeleteFromAEL(horzEdge); + } + }; + ClipperLib.Clipper.prototype.GetNextInAEL = function (e, Direction) + { + return Direction == ClipperLib.Direction.dLeftToRight ? e.NextInAEL : e.PrevInAEL; + }; + ClipperLib.Clipper.prototype.IsMinima = function (e) + { + return e !== null && (e.Prev.NextInLML != e) && (e.Next.NextInLML != e); + }; + ClipperLib.Clipper.prototype.IsMaxima = function (e, Y) + { + return (e !== null && e.Top.Y == Y && e.NextInLML === null); + }; + ClipperLib.Clipper.prototype.IsIntermediate = function (e, Y) + { + return (e.Top.Y == Y && e.NextInLML !== null); + }; + ClipperLib.Clipper.prototype.GetMaximaPair = function (e) + { + var result = null; + if ((ClipperLib.IntPoint.op_Equality(e.Next.Top, e.Top)) && e.Next.NextInLML === null) + result = e.Next; + else if ((ClipperLib.IntPoint.op_Equality(e.Prev.Top, e.Top)) && e.Prev.NextInLML === null) + result = e.Prev; + if (result !== null && (result.OutIdx == -2 || (result.NextInAEL == result.PrevInAEL && !ClipperLib.ClipperBase.IsHorizontal(result)))) + return null; + return result; + }; + ClipperLib.Clipper.prototype.ProcessIntersections = function (botY, topY) + { + if (this.m_ActiveEdges == null) + return true; + try + { + this.BuildIntersectList(botY, topY); + if (this.m_IntersectList.length == 0) + return true; + if (this.m_IntersectList.length == 1 || this.FixupIntersectionOrder()) + this.ProcessIntersectList(); + else + return false; + } + catch ($$e2) + { + this.m_SortedEdges = null; + this.m_IntersectList.length = 0; + ClipperLib.Error("ProcessIntersections error"); + } + this.m_SortedEdges = null; + return true; + }; + ClipperLib.Clipper.prototype.BuildIntersectList = function (botY, topY) + { + if (this.m_ActiveEdges === null) + return; + //prepare for sorting ... + var e = this.m_ActiveEdges; + //console.log(JSON.stringify(JSON.decycle( e ))); + this.m_SortedEdges = e; + while (e !== null) + { + e.PrevInSEL = e.PrevInAEL; + e.NextInSEL = e.NextInAEL; + e.Curr.X = ClipperLib.Clipper.TopX(e, topY); + e = e.NextInAEL; + } + //bubblesort ... + var isModified = true; + while (isModified && this.m_SortedEdges !== null) + { + isModified = false; + e = this.m_SortedEdges; + while (e.NextInSEL !== null) + { + var eNext = e.NextInSEL; + var pt = new ClipperLib.IntPoint(); + //console.log("e.Curr.X: " + e.Curr.X + " eNext.Curr.X" + eNext.Curr.X); + if (e.Curr.X > eNext.Curr.X) + { + if (!this.IntersectPoint(e, eNext, pt) && e.Curr.X > eNext.Curr.X + 1) + { + //console.log("e.Curr.X: "+JSON.stringify(JSON.decycle( e.Curr.X ))); + //console.log("eNext.Curr.X+1: "+JSON.stringify(JSON.decycle( eNext.Curr.X+1))); + ClipperLib.Error("Intersection error"); + } + if (pt.Y > botY) + { + pt.Y = botY; + if (Math.abs(e.Dx) > Math.abs(eNext.Dx)) + pt.X = ClipperLib.Clipper.TopX(eNext, botY); + else + pt.X = ClipperLib.Clipper.TopX(e, botY); + } + var newNode = new ClipperLib.IntersectNode(); + newNode.Edge1 = e; + newNode.Edge2 = eNext; + //newNode.Pt = pt; + newNode.Pt.X = pt.X; + newNode.Pt.Y = pt.Y; + this.m_IntersectList.push(newNode); + this.SwapPositionsInSEL(e, eNext); + isModified = true; + } + else + e = eNext; + } + if (e.PrevInSEL !== null) + e.PrevInSEL.NextInSEL = null; + else + break; + } + this.m_SortedEdges = null; + }; + ClipperLib.Clipper.prototype.EdgesAdjacent = function (inode) + { + return (inode.Edge1.NextInSEL == inode.Edge2) || (inode.Edge1.PrevInSEL == inode.Edge2); + }; + ClipperLib.Clipper.IntersectNodeSort = function (node1, node2) + { + //the following typecast is safe because the differences in Pt.Y will + //be limited to the height of the scanbeam. + return (node2.Pt.Y - node1.Pt.Y); + }; + ClipperLib.Clipper.prototype.FixupIntersectionOrder = function () + { + //pre-condition: intersections are sorted bottom-most first. + //Now it's crucial that intersections are made only between adjacent edges, + //so to ensure this the order of intersections may need adjusting ... + this.m_IntersectList.sort(this.m_IntersectNodeComparer); + this.CopyAELToSEL(); + var cnt = this.m_IntersectList.length; + for (var i = 0; i < cnt; i++) + { + if (!this.EdgesAdjacent(this.m_IntersectList[i])) + { + var j = i + 1; + while (j < cnt && !this.EdgesAdjacent(this.m_IntersectList[j])) + j++; + if (j == cnt) + return false; + var tmp = this.m_IntersectList[i]; + this.m_IntersectList[i] = this.m_IntersectList[j]; + this.m_IntersectList[j] = tmp; + } + this.SwapPositionsInSEL(this.m_IntersectList[i].Edge1, this.m_IntersectList[i].Edge2); + } + return true; + }; + ClipperLib.Clipper.prototype.ProcessIntersectList = function () + { + for (var i = 0, ilen = this.m_IntersectList.length; i < ilen; i++) + { + var iNode = this.m_IntersectList[i]; + this.IntersectEdges(iNode.Edge1, iNode.Edge2, iNode.Pt, true); + this.SwapPositionsInAEL(iNode.Edge1, iNode.Edge2); + } + this.m_IntersectList.length = 0; + }; + /* + -------------------------------- + Round speedtest: http://jsperf.com/fastest-round + -------------------------------- + */ + var R1 = function (a) + { + return a < 0 ? Math.ceil(a - 0.5) : Math.round(a) + }; + var R2 = function (a) + { + return a < 0 ? Math.ceil(a - 0.5) : Math.floor(a + 0.5) + }; + var R3 = function (a) + { + return a < 0 ? -Math.round(Math.abs(a)) : Math.round(a) + }; + var R4 = function (a) + { + if (a < 0) + { + a -= 0.5; + return a < -2147483648 ? Math.ceil(a) : a | 0; + } + else + { + a += 0.5; + return a > 2147483647 ? Math.floor(a) : a | 0; + } + }; + if (browser.msie) ClipperLib.Clipper.Round = R1; + else if (browser.chromium) ClipperLib.Clipper.Round = R3; + else if (browser.safari) ClipperLib.Clipper.Round = R4; + else ClipperLib.Clipper.Round = R2; // eg. browser.chrome || browser.firefox || browser.opera + ClipperLib.Clipper.TopX = function (edge, currentY) + { + //if (edge.Bot == edge.Curr) alert ("edge.Bot = edge.Curr"); + //if (edge.Bot == edge.Top) alert ("edge.Bot = edge.Top"); + if (currentY == edge.Top.Y) + return edge.Top.X; + return edge.Bot.X + ClipperLib.Clipper.Round(edge.Dx * (currentY - edge.Bot.Y)); + }; + ClipperLib.Clipper.prototype.IntersectPoint = function (edge1, edge2, ip) + { + ip.X = 0; + ip.Y = 0; + var b1, b2; + //nb: with very large coordinate values, it's possible for SlopesEqual() to + //return false but for the edge.Dx value be equal due to double precision rounding. + if (ClipperLib.ClipperBase.SlopesEqual(edge1, edge2, this.m_UseFullRange) || edge1.Dx == edge2.Dx) + { + if (edge2.Bot.Y > edge1.Bot.Y) + { + ip.X = edge2.Bot.X; + ip.Y = edge2.Bot.Y; + } + else + { + ip.X = edge1.Bot.X; + ip.Y = edge1.Bot.Y; + } + return false; + } + else if (edge1.Delta.X === 0) + { + ip.X = edge1.Bot.X; + if (ClipperLib.ClipperBase.IsHorizontal(edge2)) + { + ip.Y = edge2.Bot.Y; + } + else + { + b2 = edge2.Bot.Y - (edge2.Bot.X / edge2.Dx); + ip.Y = ClipperLib.Clipper.Round(ip.X / edge2.Dx + b2); + } + } + else if (edge2.Delta.X === 0) + { + ip.X = edge2.Bot.X; + if (ClipperLib.ClipperBase.IsHorizontal(edge1)) + { + ip.Y = edge1.Bot.Y; + } + else + { + b1 = edge1.Bot.Y - (edge1.Bot.X / edge1.Dx); + ip.Y = ClipperLib.Clipper.Round(ip.X / edge1.Dx + b1); + } + } + else + { + b1 = edge1.Bot.X - edge1.Bot.Y * edge1.Dx; + b2 = edge2.Bot.X - edge2.Bot.Y * edge2.Dx; + var q = (b2 - b1) / (edge1.Dx - edge2.Dx); + ip.Y = ClipperLib.Clipper.Round(q); + if (Math.abs(edge1.Dx) < Math.abs(edge2.Dx)) + ip.X = ClipperLib.Clipper.Round(edge1.Dx * q + b1); + else + ip.X = ClipperLib.Clipper.Round(edge2.Dx * q + b2); + } + if (ip.Y < edge1.Top.Y || ip.Y < edge2.Top.Y) + { + if (edge1.Top.Y > edge2.Top.Y) + { + ip.Y = edge1.Top.Y; + ip.X = ClipperLib.Clipper.TopX(edge2, edge1.Top.Y); + return ip.X < edge1.Top.X; + } + else + ip.Y = edge2.Top.Y; + if (Math.abs(edge1.Dx) < Math.abs(edge2.Dx)) + ip.X = ClipperLib.Clipper.TopX(edge1, ip.Y); + else + ip.X = ClipperLib.Clipper.TopX(edge2, ip.Y); + } + return true; + }; + ClipperLib.Clipper.prototype.ProcessEdgesAtTopOfScanbeam = function (topY) + { + var e = this.m_ActiveEdges; + while (e !== null) + { + //1. process maxima, treating them as if they're 'bent' horizontal edges, + // but exclude maxima with horizontal edges. nb: e can't be a horizontal. + var IsMaximaEdge = this.IsMaxima(e, topY); + if (IsMaximaEdge) + { + var eMaxPair = this.GetMaximaPair(e); + IsMaximaEdge = (eMaxPair === null || !ClipperLib.ClipperBase.IsHorizontal(eMaxPair)); + } + if (IsMaximaEdge) + { + var ePrev = e.PrevInAEL; + this.DoMaxima(e); + if (ePrev === null) + e = this.m_ActiveEdges; + else + e = ePrev.NextInAEL; + } + else + { + //2. promote horizontal edges, otherwise update Curr.X and Curr.Y ... + if (this.IsIntermediate(e, topY) && ClipperLib.ClipperBase.IsHorizontal(e.NextInLML)) + { + e = this.UpdateEdgeIntoAEL(e); + if (e.OutIdx >= 0) + this.AddOutPt(e, e.Bot); + this.AddEdgeToSEL(e); + } + else + { + e.Curr.X = ClipperLib.Clipper.TopX(e, topY); + e.Curr.Y = topY; + } + if (this.StrictlySimple) + { + var ePrev = e.PrevInAEL; + if ((e.OutIdx >= 0) && (e.WindDelta !== 0) && ePrev !== null && + (ePrev.OutIdx >= 0) && (ePrev.Curr.X == e.Curr.X) && + (ePrev.WindDelta !== 0)) + { + var op = this.AddOutPt(ePrev, e.Curr); + var op2 = this.AddOutPt(e, e.Curr); + this.AddJoin(op, op2, e.Curr); + //StrictlySimple (type-3) join + } + } + e = e.NextInAEL; + } + } + //3. Process horizontals at the Top of the scanbeam ... + this.ProcessHorizontals(true); + //4. Promote intermediate vertices ... + e = this.m_ActiveEdges; + while (e !== null) + { + if (this.IsIntermediate(e, topY)) + { + var op = null; + if (e.OutIdx >= 0) + op = this.AddOutPt(e, e.Top); + e = this.UpdateEdgeIntoAEL(e); + //if output polygons share an edge, they'll need joining later ... + var ePrev = e.PrevInAEL; + var eNext = e.NextInAEL; + if (ePrev !== null && ePrev.Curr.X == e.Bot.X && + ePrev.Curr.Y == e.Bot.Y && op !== null && + ePrev.OutIdx >= 0 && ePrev.Curr.Y > ePrev.Top.Y && + ClipperLib.ClipperBase.SlopesEqual(e, ePrev, this.m_UseFullRange) && + (e.WindDelta !== 0) && (ePrev.WindDelta !== 0)) + { + var op2 = this.AddOutPt(ePrev, e.Bot); + this.AddJoin(op, op2, e.Top); + } + else if (eNext !== null && eNext.Curr.X == e.Bot.X && + eNext.Curr.Y == e.Bot.Y && op !== null && + eNext.OutIdx >= 0 && eNext.Curr.Y > eNext.Top.Y && + ClipperLib.ClipperBase.SlopesEqual(e, eNext, this.m_UseFullRange) && + (e.WindDelta !== 0) && (eNext.WindDelta !== 0)) + { + var op2 = this.AddOutPt(eNext, e.Bot); + this.AddJoin(op, op2, e.Top); + } + } + e = e.NextInAEL; + } + }; + ClipperLib.Clipper.prototype.DoMaxima = function (e) + { + var eMaxPair = this.GetMaximaPair(e); + if (eMaxPair === null) + { + if (e.OutIdx >= 0) + this.AddOutPt(e, e.Top); + this.DeleteFromAEL(e); + return; + } + var eNext = e.NextInAEL; + var use_lines = true; + while (eNext !== null && eNext != eMaxPair) + { + this.IntersectEdges(e, eNext, e.Top, true); + this.SwapPositionsInAEL(e, eNext); + eNext = e.NextInAEL; + } + if (e.OutIdx == -1 && eMaxPair.OutIdx == -1) + { + this.DeleteFromAEL(e); + this.DeleteFromAEL(eMaxPair); + } + else if (e.OutIdx >= 0 && eMaxPair.OutIdx >= 0) + { + this.IntersectEdges(e, eMaxPair, e.Top, false); + } + else if (use_lines && e.WindDelta === 0) + { + if (e.OutIdx >= 0) + { + this.AddOutPt(e, e.Top); + e.OutIdx = -1; + } + this.DeleteFromAEL(e); + if (eMaxPair.OutIdx >= 0) + { + this.AddOutPt(eMaxPair, e.Top); + eMaxPair.OutIdx = -1; + } + this.DeleteFromAEL(eMaxPair); + } + else + ClipperLib.Error("DoMaxima error"); + }; + ClipperLib.Clipper.ReversePaths = function (polys) + { + for (var i = 0, len = polys.length; i < len; i++) + polys[i].reverse(); + }; + ClipperLib.Clipper.Orientation = function (poly) + { + return ClipperLib.Clipper.Area(poly) >= 0; + }; + ClipperLib.Clipper.prototype.PointCount = function (pts) + { + if (pts === null) + return 0; + var result = 0; + var p = pts; + do { + result++; + p = p.Next; + } + while (p != pts) + return result; + }; + ClipperLib.Clipper.prototype.BuildResult = function (polyg) + { + ClipperLib.Clear(polyg); + for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) + { + var outRec = this.m_PolyOuts[i]; + if (outRec.Pts === null) + continue; + var p = outRec.Pts.Prev; + var cnt = this.PointCount(p); + if (cnt < 2) + continue; + var pg = new Array(cnt); + for (var j = 0; j < cnt; j++) + { + pg[j] = p.Pt; + p = p.Prev; + } + polyg.push(pg); + } + }; + ClipperLib.Clipper.prototype.BuildResult2 = function (polytree) + { + polytree.Clear(); + //add each output polygon/contour to polytree ... + //polytree.m_AllPolys.set_Capacity(this.m_PolyOuts.length); + for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) + { + var outRec = this.m_PolyOuts[i]; + var cnt = this.PointCount(outRec.Pts); + if ((outRec.IsOpen && cnt < 2) || (!outRec.IsOpen && cnt < 3)) + continue; + this.FixHoleLinkage(outRec); + var pn = new ClipperLib.PolyNode(); + polytree.m_AllPolys.push(pn); + outRec.PolyNode = pn; + pn.m_polygon.length = cnt; + var op = outRec.Pts.Prev; + for (var j = 0; j < cnt; j++) + { + pn.m_polygon[j] = op.Pt; + op = op.Prev; + } + } + //fixup PolyNode links etc ... + //polytree.m_Childs.set_Capacity(this.m_PolyOuts.length); + for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) + { + var outRec = this.m_PolyOuts[i]; + if (outRec.PolyNode === null) + continue; + else if (outRec.IsOpen) + { + outRec.PolyNode.IsOpen = true; + polytree.AddChild(outRec.PolyNode); + } + else if (outRec.FirstLeft !== null && outRec.FirstLeft.PolyNode != null) + outRec.FirstLeft.PolyNode.AddChild(outRec.PolyNode); + else + polytree.AddChild(outRec.PolyNode); + } + }; + ClipperLib.Clipper.prototype.FixupOutPolygon = function (outRec) + { + //FixupOutPolygon() - removes duplicate points and simplifies consecutive + //parallel edges by removing the middle vertex. + var lastOK = null; + outRec.BottomPt = null; + var pp = outRec.Pts; + for (;;) + { + if (pp.Prev == pp || pp.Prev == pp.Next) + { + this.DisposeOutPts(pp); + outRec.Pts = null; + return; + } + //test for duplicate points and collinear edges ... + if ((ClipperLib.IntPoint.op_Equality(pp.Pt, pp.Next.Pt)) || (ClipperLib.IntPoint.op_Equality(pp.Pt, pp.Prev.Pt)) || + (ClipperLib.ClipperBase.SlopesEqual(pp.Prev.Pt, pp.Pt, pp.Next.Pt, this.m_UseFullRange) && + (!this.PreserveCollinear || !this.Pt2IsBetweenPt1AndPt3(pp.Prev.Pt, pp.Pt, pp.Next.Pt)))) + { + lastOK = null; + var tmp = pp; + pp.Prev.Next = pp.Next; + pp.Next.Prev = pp.Prev; + pp = pp.Prev; + tmp = null; + } + else if (pp == lastOK) + break; + else + { + if (lastOK === null) + lastOK = pp; + pp = pp.Next; + } + } + outRec.Pts = pp; + }; + ClipperLib.Clipper.prototype.DupOutPt = function (outPt, InsertAfter) + { + var result = new ClipperLib.OutPt(); + //result.Pt = outPt.Pt; + result.Pt.X = outPt.Pt.X; + result.Pt.Y = outPt.Pt.Y; + result.Idx = outPt.Idx; + if (InsertAfter) + { + result.Next = outPt.Next; + result.Prev = outPt; + outPt.Next.Prev = result; + outPt.Next = result; + } + else + { + result.Prev = outPt.Prev; + result.Next = outPt; + outPt.Prev.Next = result; + outPt.Prev = result; + } + return result; + }; + ClipperLib.Clipper.prototype.GetOverlap = function (a1, a2, b1, b2, $val) + { + if (a1 < a2) + { + if (b1 < b2) + { + $val.Left = Math.max(a1, b1); + $val.Right = Math.min(a2, b2); + } + else + { + $val.Left = Math.max(a1, b2); + $val.Right = Math.min(a2, b1); + } + } + else + { + if (b1 < b2) + { + $val.Left = Math.max(a2, b1); + $val.Right = Math.min(a1, b2); + } + else + { + $val.Left = Math.max(a2, b2); + $val.Right = Math.min(a1, b1); + } + } + return $val.Left < $val.Right; + }; + ClipperLib.Clipper.prototype.JoinHorz = function (op1, op1b, op2, op2b, Pt, DiscardLeft) + { + var Dir1 = (op1.Pt.X > op1b.Pt.X ? ClipperLib.Direction.dRightToLeft : ClipperLib.Direction.dLeftToRight); + var Dir2 = (op2.Pt.X > op2b.Pt.X ? ClipperLib.Direction.dRightToLeft : ClipperLib.Direction.dLeftToRight); + if (Dir1 == Dir2) + return false; + //When DiscardLeft, we want Op1b to be on the Left of Op1, otherwise we + //want Op1b to be on the Right. (And likewise with Op2 and Op2b.) + //So, to facilitate this while inserting Op1b and Op2b ... + //when DiscardLeft, make sure we're AT or RIGHT of Pt before adding Op1b, + //otherwise make sure we're AT or LEFT of Pt. (Likewise with Op2b.) + if (Dir1 == ClipperLib.Direction.dLeftToRight) + { + while (op1.Next.Pt.X <= Pt.X && + op1.Next.Pt.X >= op1.Pt.X && op1.Next.Pt.Y == Pt.Y) + op1 = op1.Next; + if (DiscardLeft && (op1.Pt.X != Pt.X)) + op1 = op1.Next; + op1b = this.DupOutPt(op1, !DiscardLeft); + if (ClipperLib.IntPoint.op_Inequality(op1b.Pt, Pt)) + { + op1 = op1b; + //op1.Pt = Pt; + op1.Pt.X = Pt.X; + op1.Pt.Y = Pt.Y; + op1b = this.DupOutPt(op1, !DiscardLeft); + } + } + else + { + while (op1.Next.Pt.X >= Pt.X && + op1.Next.Pt.X <= op1.Pt.X && op1.Next.Pt.Y == Pt.Y) + op1 = op1.Next; + if (!DiscardLeft && (op1.Pt.X != Pt.X)) + op1 = op1.Next; + op1b = this.DupOutPt(op1, DiscardLeft); + if (ClipperLib.IntPoint.op_Inequality(op1b.Pt, Pt)) + { + op1 = op1b; + //op1.Pt = Pt; + op1.Pt.X = Pt.X; + op1.Pt.Y = Pt.Y; + op1b = this.DupOutPt(op1, DiscardLeft); + } + } + if (Dir2 == ClipperLib.Direction.dLeftToRight) + { + while (op2.Next.Pt.X <= Pt.X && + op2.Next.Pt.X >= op2.Pt.X && op2.Next.Pt.Y == Pt.Y) + op2 = op2.Next; + if (DiscardLeft && (op2.Pt.X != Pt.X)) + op2 = op2.Next; + op2b = this.DupOutPt(op2, !DiscardLeft); + if (ClipperLib.IntPoint.op_Inequality(op2b.Pt, Pt)) + { + op2 = op2b; + //op2.Pt = Pt; + op2.Pt.X = Pt.X; + op2.Pt.Y = Pt.Y; + op2b = this.DupOutPt(op2, !DiscardLeft); + } + } + else + { + while (op2.Next.Pt.X >= Pt.X && + op2.Next.Pt.X <= op2.Pt.X && op2.Next.Pt.Y == Pt.Y) + op2 = op2.Next; + if (!DiscardLeft && (op2.Pt.X != Pt.X)) + op2 = op2.Next; + op2b = this.DupOutPt(op2, DiscardLeft); + if (ClipperLib.IntPoint.op_Inequality(op2b.Pt, Pt)) + { + op2 = op2b; + //op2.Pt = Pt; + op2.Pt.X = Pt.X; + op2.Pt.Y = Pt.Y; + op2b = this.DupOutPt(op2, DiscardLeft); + } + } + if ((Dir1 == ClipperLib.Direction.dLeftToRight) == DiscardLeft) + { + op1.Prev = op2; + op2.Next = op1; + op1b.Next = op2b; + op2b.Prev = op1b; + } + else + { + op1.Next = op2; + op2.Prev = op1; + op1b.Prev = op2b; + op2b.Next = op1b; + } + return true; + }; + ClipperLib.Clipper.prototype.JoinPoints = function (j, outRec1, outRec2) + { + var op1 = j.OutPt1, + op1b = new ClipperLib.OutPt(); + var op2 = j.OutPt2, + op2b = new ClipperLib.OutPt(); + //There are 3 kinds of joins for output polygons ... + //1. Horizontal joins where Join.OutPt1 & Join.OutPt2 are a vertices anywhere + //along (horizontal) collinear edges (& Join.OffPt is on the same horizontal). + //2. Non-horizontal joins where Join.OutPt1 & Join.OutPt2 are at the same + //location at the Bottom of the overlapping segment (& Join.OffPt is above). + //3. StrictlySimple joins where edges touch but are not collinear and where + //Join.OutPt1, Join.OutPt2 & Join.OffPt all share the same point. + var isHorizontal = (j.OutPt1.Pt.Y == j.OffPt.Y); + if (isHorizontal && (ClipperLib.IntPoint.op_Equality(j.OffPt, j.OutPt1.Pt)) && (ClipperLib.IntPoint.op_Equality(j.OffPt, j.OutPt2.Pt))) + { + //Strictly Simple join ... + op1b = j.OutPt1.Next; + while (op1b != op1 && (ClipperLib.IntPoint.op_Equality(op1b.Pt, j.OffPt))) + op1b = op1b.Next; + var reverse1 = (op1b.Pt.Y > j.OffPt.Y); + op2b = j.OutPt2.Next; + while (op2b != op2 && (ClipperLib.IntPoint.op_Equality(op2b.Pt, j.OffPt))) + op2b = op2b.Next; + var reverse2 = (op2b.Pt.Y > j.OffPt.Y); + if (reverse1 == reverse2) + return false; + if (reverse1) + { + op1b = this.DupOutPt(op1, false); + op2b = this.DupOutPt(op2, true); + op1.Prev = op2; + op2.Next = op1; + op1b.Next = op2b; + op2b.Prev = op1b; + j.OutPt1 = op1; + j.OutPt2 = op1b; + return true; + } + else + { + op1b = this.DupOutPt(op1, true); + op2b = this.DupOutPt(op2, false); + op1.Next = op2; + op2.Prev = op1; + op1b.Prev = op2b; + op2b.Next = op1b; + j.OutPt1 = op1; + j.OutPt2 = op1b; + return true; + } + } + else if (isHorizontal) + { + //treat horizontal joins differently to non-horizontal joins since with + //them we're not yet sure where the overlapping is. OutPt1.Pt & OutPt2.Pt + //may be anywhere along the horizontal edge. + op1b = op1; + while (op1.Prev.Pt.Y == op1.Pt.Y && op1.Prev != op1b && op1.Prev != op2) + op1 = op1.Prev; + while (op1b.Next.Pt.Y == op1b.Pt.Y && op1b.Next != op1 && op1b.Next != op2) + op1b = op1b.Next; + if (op1b.Next == op1 || op1b.Next == op2) + return false; + //a flat 'polygon' + op2b = op2; + while (op2.Prev.Pt.Y == op2.Pt.Y && op2.Prev != op2b && op2.Prev != op1b) + op2 = op2.Prev; + while (op2b.Next.Pt.Y == op2b.Pt.Y && op2b.Next != op2 && op2b.Next != op1) + op2b = op2b.Next; + if (op2b.Next == op2 || op2b.Next == op1) + return false; + //a flat 'polygon' + //Op1 -. Op1b & Op2 -. Op2b are the extremites of the horizontal edges + + var $val = {Left: null, Right: null}; + if (!this.GetOverlap(op1.Pt.X, op1b.Pt.X, op2.Pt.X, op2b.Pt.X, $val)) + return false; + var Left = $val.Left; + var Right = $val.Right; + + //DiscardLeftSide: when overlapping edges are joined, a spike will created + //which needs to be cleaned up. However, we don't want Op1 or Op2 caught up + //on the discard Side as either may still be needed for other joins ... + var Pt = new ClipperLib.IntPoint(); + var DiscardLeftSide; + if (op1.Pt.X >= Left && op1.Pt.X <= Right) + { + //Pt = op1.Pt; + Pt.X = op1.Pt.X; + Pt.Y = op1.Pt.Y; + DiscardLeftSide = (op1.Pt.X > op1b.Pt.X); + } + else if (op2.Pt.X >= Left && op2.Pt.X <= Right) + { + //Pt = op2.Pt; + Pt.X = op2.Pt.X; + Pt.Y = op2.Pt.Y; + DiscardLeftSide = (op2.Pt.X > op2b.Pt.X); + } + else if (op1b.Pt.X >= Left && op1b.Pt.X <= Right) + { + //Pt = op1b.Pt; + Pt.X = op1b.Pt.X; + Pt.Y = op1b.Pt.Y; + DiscardLeftSide = op1b.Pt.X > op1.Pt.X; + } + else + { + //Pt = op2b.Pt; + Pt.X = op2b.Pt.X; + Pt.Y = op2b.Pt.Y; + DiscardLeftSide = (op2b.Pt.X > op2.Pt.X); + } + j.OutPt1 = op1; + j.OutPt2 = op2; + return this.JoinHorz(op1, op1b, op2, op2b, Pt, DiscardLeftSide); + } + else + { + //nb: For non-horizontal joins ... + // 1. Jr.OutPt1.Pt.Y == Jr.OutPt2.Pt.Y + // 2. Jr.OutPt1.Pt > Jr.OffPt.Y + //make sure the polygons are correctly oriented ... + op1b = op1.Next; + while ((ClipperLib.IntPoint.op_Equality(op1b.Pt, op1.Pt)) && (op1b != op1)) + op1b = op1b.Next; + var Reverse1 = ((op1b.Pt.Y > op1.Pt.Y) || !ClipperLib.ClipperBase.SlopesEqual(op1.Pt, op1b.Pt, j.OffPt, this.m_UseFullRange)); + if (Reverse1) + { + op1b = op1.Prev; + while ((ClipperLib.IntPoint.op_Equality(op1b.Pt, op1.Pt)) && (op1b != op1)) + op1b = op1b.Prev; + if ((op1b.Pt.Y > op1.Pt.Y) || !ClipperLib.ClipperBase.SlopesEqual(op1.Pt, op1b.Pt, j.OffPt, this.m_UseFullRange)) + return false; + } + op2b = op2.Next; + while ((ClipperLib.IntPoint.op_Equality(op2b.Pt, op2.Pt)) && (op2b != op2)) + op2b = op2b.Next; + var Reverse2 = ((op2b.Pt.Y > op2.Pt.Y) || !ClipperLib.ClipperBase.SlopesEqual(op2.Pt, op2b.Pt, j.OffPt, this.m_UseFullRange)); + if (Reverse2) + { + op2b = op2.Prev; + while ((ClipperLib.IntPoint.op_Equality(op2b.Pt, op2.Pt)) && (op2b != op2)) + op2b = op2b.Prev; + if ((op2b.Pt.Y > op2.Pt.Y) || !ClipperLib.ClipperBase.SlopesEqual(op2.Pt, op2b.Pt, j.OffPt, this.m_UseFullRange)) + return false; + } + if ((op1b == op1) || (op2b == op2) || (op1b == op2b) || + ((outRec1 == outRec2) && (Reverse1 == Reverse2))) + return false; + if (Reverse1) + { + op1b = this.DupOutPt(op1, false); + op2b = this.DupOutPt(op2, true); + op1.Prev = op2; + op2.Next = op1; + op1b.Next = op2b; + op2b.Prev = op1b; + j.OutPt1 = op1; + j.OutPt2 = op1b; + return true; + } + else + { + op1b = this.DupOutPt(op1, true); + op2b = this.DupOutPt(op2, false); + op1.Next = op2; + op2.Prev = op1; + op1b.Prev = op2b; + op2b.Next = op1b; + j.OutPt1 = op1; + j.OutPt2 = op1b; + return true; + } + } + }; + ClipperLib.Clipper.GetBounds = function (paths) + { + var i = 0, + cnt = paths.length; + while (i < cnt && paths[i].length == 0) i++; + if (i == cnt) return new ClipperLib.IntRect(0, 0, 0, 0); + var result = new ClipperLib.IntRect(); + result.left = paths[i][0].X; + result.right = result.left; + result.top = paths[i][0].Y; + result.bottom = result.top; + for (; i < cnt; i++) + for (var j = 0, jlen = paths[i].length; j < jlen; j++) + { + if (paths[i][j].X < result.left) result.left = paths[i][j].X; + else if (paths[i][j].X > result.right) result.right = paths[i][j].X; + if (paths[i][j].Y < result.top) result.top = paths[i][j].Y; + else if (paths[i][j].Y > result.bottom) result.bottom = paths[i][j].Y; + } + return result; + } + ClipperLib.Clipper.prototype.GetBounds2 = function (ops) + { + var opStart = ops; + var result = new ClipperLib.IntRect(); + result.left = ops.Pt.X; + result.right = ops.Pt.X; + result.top = ops.Pt.Y; + result.bottom = ops.Pt.Y; + ops = ops.Next; + while (ops != opStart) + { + if (ops.Pt.X < result.left) + result.left = ops.Pt.X; + if (ops.Pt.X > result.right) + result.right = ops.Pt.X; + if (ops.Pt.Y < result.top) + result.top = ops.Pt.Y; + if (ops.Pt.Y > result.bottom) + result.bottom = ops.Pt.Y; + ops = ops.Next; + } + return result; + }; + + ClipperLib.Clipper.PointInPolygon = function (pt, path) + { + //returns 0 if false, +1 if true, -1 if pt ON polygon boundary + //http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.88.5498&rep=rep1&type=pdf + var result = 0, + cnt = path.length; + if (cnt < 3) + return 0; + var ip = path[0]; + for (var i = 1; i <= cnt; ++i) + { + var ipNext = (i == cnt ? path[0] : path[i]); + if (ipNext.Y == pt.Y) + { + if ((ipNext.X == pt.X) || (ip.Y == pt.Y && ((ipNext.X > pt.X) == (ip.X < pt.X)))) + return -1; + } + if ((ip.Y < pt.Y) != (ipNext.Y < pt.Y)) + { + if (ip.X >= pt.X) + { + if (ipNext.X > pt.X) + result = 1 - result; + else + { + var d = (ip.X - pt.X) * (ipNext.Y - pt.Y) - (ipNext.X - pt.X) * (ip.Y - pt.Y); + if (d == 0) + return -1; + else if ((d > 0) == (ipNext.Y > ip.Y)) + result = 1 - result; + } + } + else + { + if (ipNext.X > pt.X) + { + var d = (ip.X - pt.X) * (ipNext.Y - pt.Y) - (ipNext.X - pt.X) * (ip.Y - pt.Y); + if (d == 0) + return -1; + else if ((d > 0) == (ipNext.Y > ip.Y)) + result = 1 - result; + } + } + } + ip = ipNext; + } + return result; + }; + + ClipperLib.Clipper.prototype.PointInPolygon = function (pt, op) + { + //returns 0 if false, +1 if true, -1 if pt ON polygon boundary + //http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.88.5498&rep=rep1&type=pdf + var result = 0; + var startOp = op; + for (;;) + { + var poly0x = op.Pt.X, + poly0y = op.Pt.Y; + var poly1x = op.Next.Pt.X, + poly1y = op.Next.Pt.Y; + if (poly1y == pt.Y) + { + if ((poly1x == pt.X) || (poly0y == pt.Y && ((poly1x > pt.X) == (poly0x < pt.X)))) + return -1; + } + if ((poly0y < pt.Y) != (poly1y < pt.Y)) + { + if (poly0x >= pt.X) + { + if (poly1x > pt.X) + result = 1 - result; + else + { + var d = (poly0x - pt.X) * (poly1y - pt.Y) - (poly1x - pt.X) * (poly0y - pt.Y); + if (d == 0) + return -1; + if ((d > 0) == (poly1y > poly0y)) + result = 1 - result; + } + } + else + { + if (poly1x > pt.X) + { + var d = (poly0x - pt.X) * (poly1y - pt.Y) - (poly1x - pt.X) * (poly0y - pt.Y); + if (d == 0) + return -1; + if ((d > 0) == (poly1y > poly0y)) + result = 1 - result; + } + } + } + op = op.Next; + if (startOp == op) + break; + } + return result; + }; + + ClipperLib.Clipper.prototype.Poly2ContainsPoly1 = function (outPt1, outPt2) + { + var op = outPt1; + do { + var res = this.PointInPolygon(op.Pt, outPt2); + if (res >= 0) + return res != 0; + op = op.Next; + } + while (op != outPt1) + return true; + }; + ClipperLib.Clipper.prototype.FixupFirstLefts1 = function (OldOutRec, NewOutRec) + { + for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) + { + var outRec = this.m_PolyOuts[i]; + if (outRec.Pts !== null && outRec.FirstLeft == OldOutRec) + { + if (this.Poly2ContainsPoly1(outRec.Pts, NewOutRec.Pts)) + outRec.FirstLeft = NewOutRec; + } + } + }; + ClipperLib.Clipper.prototype.FixupFirstLefts2 = function (OldOutRec, NewOutRec) + { + for (var $i2 = 0, $t2 = this.m_PolyOuts, $l2 = $t2.length, outRec = $t2[$i2]; $i2 < $l2; $i2++, outRec = $t2[$i2]) + if (outRec.FirstLeft == OldOutRec) + outRec.FirstLeft = NewOutRec; + }; + ClipperLib.Clipper.ParseFirstLeft = function (FirstLeft) + { + while (FirstLeft != null && FirstLeft.Pts == null) + FirstLeft = FirstLeft.FirstLeft; + return FirstLeft; + }; + ClipperLib.Clipper.prototype.JoinCommonEdges = function () + { + for (var i = 0, ilen = this.m_Joins.length; i < ilen; i++) + { + var join = this.m_Joins[i]; + var outRec1 = this.GetOutRec(join.OutPt1.Idx); + var outRec2 = this.GetOutRec(join.OutPt2.Idx); + if (outRec1.Pts == null || outRec2.Pts == null) + continue; + //get the polygon fragment with the correct hole state (FirstLeft) + //before calling JoinPoints() ... + var holeStateRec; + if (outRec1 == outRec2) + holeStateRec = outRec1; + else if (this.Param1RightOfParam2(outRec1, outRec2)) + holeStateRec = outRec2; + else if (this.Param1RightOfParam2(outRec2, outRec1)) + holeStateRec = outRec1; + else + holeStateRec = this.GetLowermostRec(outRec1, outRec2); + + if (!this.JoinPoints(join, outRec1, outRec2)) continue; + + if (outRec1 == outRec2) + { + //instead of joining two polygons, we've just created a new one by + //splitting one polygon into two. + outRec1.Pts = join.OutPt1; + outRec1.BottomPt = null; + outRec2 = this.CreateOutRec(); + outRec2.Pts = join.OutPt2; + //update all OutRec2.Pts Idx's ... + this.UpdateOutPtIdxs(outRec2); + //We now need to check every OutRec.FirstLeft pointer. If it points + //to OutRec1 it may need to point to OutRec2 instead ... + if (this.m_UsingPolyTree) + for (var j = 0, jlen = this.m_PolyOuts.length; j < jlen - 1; j++) + { + var oRec = this.m_PolyOuts[j]; + if (oRec.Pts == null || ClipperLib.Clipper.ParseFirstLeft(oRec.FirstLeft) != outRec1 || oRec.IsHole == outRec1.IsHole) + continue; + if (this.Poly2ContainsPoly1(oRec.Pts, join.OutPt2)) + oRec.FirstLeft = outRec2; + } + if (this.Poly2ContainsPoly1(outRec2.Pts, outRec1.Pts)) + { + //outRec2 is contained by outRec1 ... + outRec2.IsHole = !outRec1.IsHole; + outRec2.FirstLeft = outRec1; + //fixup FirstLeft pointers that may need reassigning to OutRec1 + if (this.m_UsingPolyTree) + this.FixupFirstLefts2(outRec2, outRec1); + if ((outRec2.IsHole ^ this.ReverseSolution) == (this.Area(outRec2) > 0)) + this.ReversePolyPtLinks(outRec2.Pts); + } + else if (this.Poly2ContainsPoly1(outRec1.Pts, outRec2.Pts)) + { + //outRec1 is contained by outRec2 ... + outRec2.IsHole = outRec1.IsHole; + outRec1.IsHole = !outRec2.IsHole; + outRec2.FirstLeft = outRec1.FirstLeft; + outRec1.FirstLeft = outRec2; + //fixup FirstLeft pointers that may need reassigning to OutRec1 + if (this.m_UsingPolyTree) + this.FixupFirstLefts2(outRec1, outRec2); + if ((outRec1.IsHole ^ this.ReverseSolution) == (this.Area(outRec1) > 0)) + this.ReversePolyPtLinks(outRec1.Pts); + } + else + { + //the 2 polygons are completely separate ... + outRec2.IsHole = outRec1.IsHole; + outRec2.FirstLeft = outRec1.FirstLeft; + //fixup FirstLeft pointers that may need reassigning to OutRec2 + if (this.m_UsingPolyTree) + this.FixupFirstLefts1(outRec1, outRec2); + } + } + else + { + //joined 2 polygons together ... + outRec2.Pts = null; + outRec2.BottomPt = null; + outRec2.Idx = outRec1.Idx; + outRec1.IsHole = holeStateRec.IsHole; + if (holeStateRec == outRec2) + outRec1.FirstLeft = outRec2.FirstLeft; + outRec2.FirstLeft = outRec1; + //fixup FirstLeft pointers that may need reassigning to OutRec1 + if (this.m_UsingPolyTree) + this.FixupFirstLefts2(outRec2, outRec1); + } + } + }; + ClipperLib.Clipper.prototype.UpdateOutPtIdxs = function (outrec) + { + var op = outrec.Pts; + do { + op.Idx = outrec.Idx; + op = op.Prev; + } + while (op != outrec.Pts) + }; + ClipperLib.Clipper.prototype.DoSimplePolygons = function () + { + var i = 0; + while (i < this.m_PolyOuts.length) + { + var outrec = this.m_PolyOuts[i++]; + var op = outrec.Pts; + if (op === null) + continue; + do //for each Pt in Polygon until duplicate found do ... + { + var op2 = op.Next; + while (op2 != outrec.Pts) + { + if ((ClipperLib.IntPoint.op_Equality(op.Pt, op2.Pt)) && op2.Next != op && op2.Prev != op) + { + //split the polygon into two ... + var op3 = op.Prev; + var op4 = op2.Prev; + op.Prev = op4; + op4.Next = op; + op2.Prev = op3; + op3.Next = op2; + outrec.Pts = op; + var outrec2 = this.CreateOutRec(); + outrec2.Pts = op2; + this.UpdateOutPtIdxs(outrec2); + if (this.Poly2ContainsPoly1(outrec2.Pts, outrec.Pts)) + { + //OutRec2 is contained by OutRec1 ... + outrec2.IsHole = !outrec.IsHole; + outrec2.FirstLeft = outrec; + } + else if (this.Poly2ContainsPoly1(outrec.Pts, outrec2.Pts)) + { + //OutRec1 is contained by OutRec2 ... + outrec2.IsHole = outrec.IsHole; + outrec.IsHole = !outrec2.IsHole; + outrec2.FirstLeft = outrec.FirstLeft; + outrec.FirstLeft = outrec2; + } + else + { + //the 2 polygons are separate ... + outrec2.IsHole = outrec.IsHole; + outrec2.FirstLeft = outrec.FirstLeft; + } + op2 = op; + //ie get ready for the next iteration + } + op2 = op2.Next; + } + op = op.Next; + } + while (op != outrec.Pts) + } + }; + ClipperLib.Clipper.Area = function (poly) + { + var cnt = poly.length; + if (cnt < 3) + return 0; + var a = 0; + for (var i = 0, j = cnt - 1; i < cnt; ++i) + { + a += (poly[j].X + poly[i].X) * (poly[j].Y - poly[i].Y); + j = i; + } + return -a * 0.5; + }; + ClipperLib.Clipper.prototype.Area = function (outRec) + { + var op = outRec.Pts; + if (op == null) + return 0; + var a = 0; + do { + a = a + (op.Prev.Pt.X + op.Pt.X) * (op.Prev.Pt.Y - op.Pt.Y); + op = op.Next; + } + while (op != outRec.Pts) + return a * 0.5; + }; + if (use_deprecated) + { + ClipperLib.Clipper.OffsetPaths = function (polys, delta, jointype, endtype, MiterLimit) + { + var result = new ClipperLib.Paths(); + var co = new ClipperLib.ClipperOffset(MiterLimit, MiterLimit); + co.AddPaths(polys, jointype, endtype); + co.Execute(result, delta); + return result; + }; + } + ClipperLib.Clipper.SimplifyPolygon = function (poly, fillType) + { + var result = new Array(); + var c = new ClipperLib.Clipper(0); + c.StrictlySimple = true; + c.AddPath(poly, ClipperLib.PolyType.ptSubject, true); + c.Execute(ClipperLib.ClipType.ctUnion, result, fillType, fillType); + return result; + }; + ClipperLib.Clipper.SimplifyPolygons = function (polys, fillType) + { + if (typeof (fillType) == "undefined") fillType = ClipperLib.PolyFillType.pftEvenOdd; + var result = new Array(); + var c = new ClipperLib.Clipper(0); + c.StrictlySimple = true; + c.AddPaths(polys, ClipperLib.PolyType.ptSubject, true); + c.Execute(ClipperLib.ClipType.ctUnion, result, fillType, fillType); + return result; + }; + ClipperLib.Clipper.DistanceSqrd = function (pt1, pt2) + { + var dx = (pt1.X - pt2.X); + var dy = (pt1.Y - pt2.Y); + return (dx * dx + dy * dy); + }; + ClipperLib.Clipper.DistanceFromLineSqrd = function (pt, ln1, ln2) + { + //The equation of a line in general form (Ax + By + C = 0) + //given 2 points (x¹,y¹) & (x²,y²) is ... + //(y¹ - y²)x + (x² - x¹)y + (y² - y¹)x¹ - (x² - x¹)y¹ = 0 + //A = (y¹ - y²); B = (x² - x¹); C = (y² - y¹)x¹ - (x² - x¹)y¹ + //perpendicular distance of point (x³,y³) = (Ax³ + By³ + C)/Sqrt(A² + B²) + //see http://en.wikipedia.org/wiki/Perpendicular_distance + var A = ln1.Y - ln2.Y; + var B = ln2.X - ln1.X; + var C = A * ln1.X + B * ln1.Y; + C = A * pt.X + B * pt.Y - C; + return (C * C) / (A * A + B * B); + }; + ClipperLib.Clipper.SlopesNearCollinear = function (pt1, pt2, pt3, distSqrd) + { + return ClipperLib.Clipper.DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd; + }; + ClipperLib.Clipper.PointsAreClose = function (pt1, pt2, distSqrd) + { + var dx = pt1.X - pt2.X; + var dy = pt1.Y - pt2.Y; + return ((dx * dx) + (dy * dy) <= distSqrd); + }; + //------------------------------------------------------------------------------ + ClipperLib.Clipper.ExcludeOp = function (op) + { + var result = op.Prev; + result.Next = op.Next; + op.Next.Prev = result; + result.Idx = 0; + return result; + }; + ClipperLib.Clipper.CleanPolygon = function (path, distance) + { + if (typeof (distance) == "undefined") distance = 1.415; + //distance = proximity in units/pixels below which vertices will be stripped. + //Default ~= sqrt(2) so when adjacent vertices or semi-adjacent vertices have + //both x & y coords within 1 unit, then the second vertex will be stripped. + var cnt = path.length; + if (cnt == 0) + return new Array(); + var outPts = new Array(cnt); + for (var i = 0; i < cnt; ++i) + outPts[i] = new ClipperLib.OutPt(); + for (var i = 0; i < cnt; ++i) + { + outPts[i].Pt = path[i]; + outPts[i].Next = outPts[(i + 1) % cnt]; + outPts[i].Next.Prev = outPts[i]; + outPts[i].Idx = 0; + } + var distSqrd = distance * distance; + var op = outPts[0]; + while (op.Idx == 0 && op.Next != op.Prev) + { + if (ClipperLib.Clipper.PointsAreClose(op.Pt, op.Prev.Pt, distSqrd)) + { + op = ClipperLib.Clipper.ExcludeOp(op); + cnt--; + } + else if (ClipperLib.Clipper.PointsAreClose(op.Prev.Pt, op.Next.Pt, distSqrd)) + { + ClipperLib.Clipper.ExcludeOp(op.Next); + op = ClipperLib.Clipper.ExcludeOp(op); + cnt -= 2; + } + else if (ClipperLib.Clipper.SlopesNearCollinear(op.Prev.Pt, op.Pt, op.Next.Pt, distSqrd)) + { + op = ClipperLib.Clipper.ExcludeOp(op); + cnt--; + } + else + { + op.Idx = 1; + op = op.Next; + } + } + if (cnt < 3) + cnt = 0; + var result = new Array(cnt); + for (var i = 0; i < cnt; ++i) + { + result[i] = new ClipperLib.IntPoint(op.Pt); + op = op.Next; + } + outPts = null; + return result; + }; + ClipperLib.Clipper.CleanPolygons = function (polys, distance) + { + var result = new Array(polys.length); + for (var i = 0, ilen = polys.length; i < ilen; i++) + result[i] = ClipperLib.Clipper.CleanPolygon(polys[i], distance); + return result; + }; + ClipperLib.Clipper.Minkowski = function (pattern, path, IsSum, IsClosed) + { + var delta = (IsClosed ? 1 : 0); + var polyCnt = pattern.length; + var pathCnt = path.length; + var result = new Array(); + if (IsSum) + for (var i = 0; i < pathCnt; i++) + { + var p = new Array(polyCnt); + for (var j = 0, jlen = pattern.length, ip = pattern[j]; j < jlen; j++, ip = pattern[j]) + p[j] = new ClipperLib.IntPoint(path[i].X + ip.X, path[i].Y + ip.Y); + result.push(p); + } + else + for (var i = 0; i < pathCnt; i++) + { + var p = new Array(polyCnt); + for (var j = 0, jlen = pattern.length, ip = pattern[j]; j < jlen; j++, ip = pattern[j]) + p[j] = new ClipperLib.IntPoint(path[i].X - ip.X, path[i].Y - ip.Y); + result.push(p); + } + var quads = new Array(); + for (var i = 0; i < pathCnt - 1 + delta; i++) + for (var j = 0; j < polyCnt; j++) + { + var quad = new Array(); + quad.push(result[i % pathCnt][j % polyCnt]); + quad.push(result[(i + 1) % pathCnt][j % polyCnt]); + quad.push(result[(i + 1) % pathCnt][(j + 1) % polyCnt]); + quad.push(result[i % pathCnt][(j + 1) % polyCnt]); + if (!ClipperLib.Clipper.Orientation(quad)) + quad.reverse(); + quads.push(quad); + } + var c = new ClipperLib.Clipper(0); + c.AddPaths(quads, ClipperLib.PolyType.ptSubject, true); + c.Execute(ClipperLib.ClipType.ctUnion, result, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero); + return result; + }; + + ClipperLib.Clipper.MinkowskiSum = function () + { + var a = arguments, + alen = a.length; + if (alen == 3) // MinkowskiSum(Path pattern, path, pathIsClosed) + { + var pattern = a[0], + path = a[1], + pathIsClosed = a[2]; + return ClipperLib.Clipper.Minkowski(pattern, path, true, pathIsClosed); + } + else if (alen == 4) // MinkowskiSum(pattern, paths, pathFillType, pathIsClosed) + { + var pattern = a[0], + paths = a[1], + pathFillType = a[2], + pathIsClosed = a[3]; + var c = new ClipperLib.Clipper(), + tmp; + for (var i = 0, ilen = paths.length; i < ilen; ++i) + { + var tmp = ClipperLib.Clipper.Minkowski(pattern, paths[i], true, pathIsClosed); + c.AddPaths(tmp, ClipperLib.PolyType.ptSubject, true); + } + if (pathIsClosed) c.AddPaths(paths, ClipperLib.PolyType.ptClip, true); + var solution = new ClipperLib.Paths(); + c.Execute(ClipperLib.ClipType.ctUnion, solution, pathFillType, pathFillType); + return solution; + } + }; + + ClipperLib.Clipper.MinkowskiDiff = function (pattern, path, pathIsClosed) + { + return ClipperLib.Clipper.Minkowski(pattern, path, false, pathIsClosed); + }; + + ClipperLib.Clipper.PolyTreeToPaths = function (polytree) + { + var result = new Array(); + //result.set_Capacity(polytree.get_Total()); + ClipperLib.Clipper.AddPolyNodeToPaths(polytree, ClipperLib.Clipper.NodeType.ntAny, result); + return result; + }; + ClipperLib.Clipper.AddPolyNodeToPaths = function (polynode, nt, paths) + { + var match = true; + switch (nt) + { + case ClipperLib.Clipper.NodeType.ntOpen: + return; + case ClipperLib.Clipper.NodeType.ntClosed: + match = !polynode.IsOpen; + break; + default: + break; + } + if (polynode.m_polygon.length > 0 && match) + paths.push(polynode.m_polygon); + for (var $i3 = 0, $t3 = polynode.Childs(), $l3 = $t3.length, pn = $t3[$i3]; $i3 < $l3; $i3++, pn = $t3[$i3]) + ClipperLib.Clipper.AddPolyNodeToPaths(pn, nt, paths); + }; + ClipperLib.Clipper.OpenPathsFromPolyTree = function (polytree) + { + var result = new ClipperLib.Paths(); + //result.set_Capacity(polytree.ChildCount()); + for (var i = 0, ilen = polytree.ChildCount(); i < ilen; i++) + if (polytree.Childs()[i].IsOpen) + result.push(polytree.Childs()[i].m_polygon); + return result; + }; + ClipperLib.Clipper.ClosedPathsFromPolyTree = function (polytree) + { + var result = new ClipperLib.Paths(); + //result.set_Capacity(polytree.Total()); + ClipperLib.Clipper.AddPolyNodeToPaths(polytree, ClipperLib.Clipper.NodeType.ntClosed, result); + return result; + }; + Inherit(ClipperLib.Clipper, ClipperLib.ClipperBase); + ClipperLib.Clipper.NodeType = { + ntAny: 0, + ntOpen: 1, + ntClosed: 2 + }; + ClipperLib.ClipperOffset = function (miterLimit, arcTolerance) + { + if (typeof (miterLimit) == "undefined") miterLimit = 2; + if (typeof (arcTolerance) == "undefined") arcTolerance = ClipperLib.ClipperOffset.def_arc_tolerance; + this.m_destPolys = new ClipperLib.Paths(); + this.m_srcPoly = new ClipperLib.Path(); + this.m_destPoly = new ClipperLib.Path(); + this.m_normals = new Array(); + this.m_delta = 0; + this.m_sinA = 0; + this.m_sin = 0; + this.m_cos = 0; + this.m_miterLim = 0; + this.m_StepsPerRad = 0; + this.m_lowest = new ClipperLib.IntPoint(); + this.m_polyNodes = new ClipperLib.PolyNode(); + this.MiterLimit = miterLimit; + this.ArcTolerance = arcTolerance; + this.m_lowest.X = -1; + }; + ClipperLib.ClipperOffset.two_pi = 6.28318530717959; + ClipperLib.ClipperOffset.def_arc_tolerance = 0.25; + ClipperLib.ClipperOffset.prototype.Clear = function () + { + ClipperLib.Clear(this.m_polyNodes.Childs()); + this.m_lowest.X = -1; + }; + ClipperLib.ClipperOffset.Round = ClipperLib.Clipper.Round; + ClipperLib.ClipperOffset.prototype.AddPath = function (path, joinType, endType) + { + var highI = path.length - 1; + if (highI < 0) + return; + var newNode = new ClipperLib.PolyNode(); + newNode.m_jointype = joinType; + newNode.m_endtype = endType; + //strip duplicate points from path and also get index to the lowest point ... + if (endType == ClipperLib.EndType.etClosedLine || endType == ClipperLib.EndType.etClosedPolygon) + while (highI > 0 && ClipperLib.IntPoint.op_Equality(path[0], path[highI])) + highI--; + //newNode.m_polygon.set_Capacity(highI + 1); + newNode.m_polygon.push(path[0]); + var j = 0, + k = 0; + for (var i = 1; i <= highI; i++) + if (ClipperLib.IntPoint.op_Inequality(newNode.m_polygon[j], path[i])) + { + j++; + newNode.m_polygon.push(path[i]); + if (path[i].Y > newNode.m_polygon[k].Y || (path[i].Y == newNode.m_polygon[k].Y && path[i].X < newNode.m_polygon[k].X)) + k = j; + } + if ((endType == ClipperLib.EndType.etClosedPolygon && j < 2) || (endType != ClipperLib.EndType.etClosedPolygon && j < 0)) + return; + this.m_polyNodes.AddChild(newNode); + //if this path's lowest pt is lower than all the others then update m_lowest + if (endType != ClipperLib.EndType.etClosedPolygon) + return; + if (this.m_lowest.X < 0) + this.m_lowest = new ClipperLib.IntPoint(0, k); + else + { + var ip = this.m_polyNodes.Childs()[this.m_lowest.X].m_polygon[this.m_lowest.Y]; + if (newNode.m_polygon[k].Y > ip.Y || (newNode.m_polygon[k].Y == ip.Y && newNode.m_polygon[k].X < ip.X)) + this.m_lowest = new ClipperLib.IntPoint(this.m_polyNodes.ChildCount() - 1, k); + } + }; + ClipperLib.ClipperOffset.prototype.AddPaths = function (paths, joinType, endType) + { + for (var i = 0, ilen = paths.length; i < ilen; i++) + this.AddPath(paths[i], joinType, endType); + }; + ClipperLib.ClipperOffset.prototype.FixOrientations = function () + { + //fixup orientations of all closed paths if the orientation of the + //closed path with the lowermost vertex is wrong ... + if (this.m_lowest.X >= 0 && !ClipperLib.Clipper.Orientation(this.m_polyNodes.Childs()[this.m_lowest.X].m_polygon)) + { + for (var i = 0; i < this.m_polyNodes.ChildCount(); i++) + { + var node = this.m_polyNodes.Childs()[i]; + if (node.m_endtype == ClipperLib.EndType.etClosedPolygon || (node.m_endtype == ClipperLib.EndType.etClosedLine && ClipperLib.Clipper.Orientation(node.m_polygon))) + node.m_polygon.reverse(); + } + } + else + { + for (var i = 0; i < this.m_polyNodes.ChildCount(); i++) + { + var node = this.m_polyNodes.Childs()[i]; + if (node.m_endtype == ClipperLib.EndType.etClosedLine && !ClipperLib.Clipper.Orientation(node.m_polygon)) + node.m_polygon.reverse(); + } + } + }; + ClipperLib.ClipperOffset.GetUnitNormal = function (pt1, pt2) + { + var dx = (pt2.X - pt1.X); + var dy = (pt2.Y - pt1.Y); + if ((dx == 0) && (dy == 0)) + return new ClipperLib.DoublePoint(0, 0); + var f = 1 / Math.sqrt(dx * dx + dy * dy); + dx *= f; + dy *= f; + return new ClipperLib.DoublePoint(dy, -dx); + }; + ClipperLib.ClipperOffset.prototype.DoOffset = function (delta) + { + this.m_destPolys = new Array(); + this.m_delta = delta; + //if Zero offset, just copy any CLOSED polygons to m_p and return ... + if (ClipperLib.ClipperBase.near_zero(delta)) + { + //this.m_destPolys.set_Capacity(this.m_polyNodes.ChildCount); + for (var i = 0; i < this.m_polyNodes.ChildCount(); i++) + { + var node = this.m_polyNodes.Childs()[i]; + if (node.m_endtype == ClipperLib.EndType.etClosedPolygon) + this.m_destPolys.push(node.m_polygon); + } + return; + } + //see offset_triginometry3.svg in the documentation folder ... + if (this.MiterLimit > 2) + this.m_miterLim = 2 / (this.MiterLimit * this.MiterLimit); + else + this.m_miterLim = 0.5; + var y; + if (this.ArcTolerance <= 0) + y = ClipperLib.ClipperOffset.def_arc_tolerance; + else if (this.ArcTolerance > Math.abs(delta) * ClipperLib.ClipperOffset.def_arc_tolerance) + y = Math.abs(delta) * ClipperLib.ClipperOffset.def_arc_tolerance; + else + y = this.ArcTolerance; + //see offset_triginometry2.svg in the documentation folder ... + var steps = 3.14159265358979 / Math.acos(1 - y / Math.abs(delta)); + this.m_sin = Math.sin(ClipperLib.ClipperOffset.two_pi / steps); + this.m_cos = Math.cos(ClipperLib.ClipperOffset.two_pi / steps); + this.m_StepsPerRad = steps / ClipperLib.ClipperOffset.two_pi; + if (delta < 0) + this.m_sin = -this.m_sin; + //this.m_destPolys.set_Capacity(this.m_polyNodes.ChildCount * 2); + for (var i = 0; i < this.m_polyNodes.ChildCount(); i++) + { + var node = this.m_polyNodes.Childs()[i]; + this.m_srcPoly = node.m_polygon; + var len = this.m_srcPoly.length; + if (len == 0 || (delta <= 0 && (len < 3 || node.m_endtype != ClipperLib.EndType.etClosedPolygon))) + continue; + this.m_destPoly = new Array(); + if (len == 1) + { + if (node.m_jointype == ClipperLib.JoinType.jtRound) + { + var X = 1, + Y = 0; + for (var j = 1; j <= steps; j++) + { + this.m_destPoly.push(new ClipperLib.IntPoint(ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].X + X * delta), ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].Y + Y * delta))); + var X2 = X; + X = X * this.m_cos - this.m_sin * Y; + Y = X2 * this.m_sin + Y * this.m_cos; + } + } + else + { + var X = -1, + Y = -1; + for (var j = 0; j < 4; ++j) + { + this.m_destPoly.push(new ClipperLib.IntPoint(ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].X + X * delta), ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].Y + Y * delta))); + if (X < 0) + X = 1; + else if (Y < 0) + Y = 1; + else + X = -1; + } + } + this.m_destPolys.push(this.m_destPoly); + continue; + } + //build m_normals ... + this.m_normals.length = 0; + //this.m_normals.set_Capacity(len); + for (var j = 0; j < len - 1; j++) + this.m_normals.push(ClipperLib.ClipperOffset.GetUnitNormal(this.m_srcPoly[j], this.m_srcPoly[j + 1])); + if (node.m_endtype == ClipperLib.EndType.etClosedLine || node.m_endtype == ClipperLib.EndType.etClosedPolygon) + this.m_normals.push(ClipperLib.ClipperOffset.GetUnitNormal(this.m_srcPoly[len - 1], this.m_srcPoly[0])); + else + this.m_normals.push(new ClipperLib.DoublePoint(this.m_normals[len - 2])); + if (node.m_endtype == ClipperLib.EndType.etClosedPolygon) + { + var k = len - 1; + for (var j = 0; j < len; j++) + k = this.OffsetPoint(j, k, node.m_jointype); + this.m_destPolys.push(this.m_destPoly); + } + else if (node.m_endtype == ClipperLib.EndType.etClosedLine) + { + var k = len - 1; + for (var j = 0; j < len; j++) + k = this.OffsetPoint(j, k, node.m_jointype); + this.m_destPolys.push(this.m_destPoly); + this.m_destPoly = new Array(); + //re-build m_normals ... + var n = this.m_normals[len - 1]; + for (var j = len - 1; j > 0; j--) + this.m_normals[j] = new ClipperLib.DoublePoint(-this.m_normals[j - 1].X, -this.m_normals[j - 1].Y); + this.m_normals[0] = new ClipperLib.DoublePoint(-n.X, -n.Y); + k = 0; + for (var j = len - 1; j >= 0; j--) + k = this.OffsetPoint(j, k, node.m_jointype); + this.m_destPolys.push(this.m_destPoly); + } + else + { + var k = 0; + for (var j = 1; j < len - 1; ++j) + k = this.OffsetPoint(j, k, node.m_jointype); + var pt1; + if (node.m_endtype == ClipperLib.EndType.etOpenButt) + { + var j = len - 1; + pt1 = new ClipperLib.IntPoint(ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + this.m_normals[j].X * delta), ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + this.m_normals[j].Y * delta)); + this.m_destPoly.push(pt1); + pt1 = new ClipperLib.IntPoint(ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X - this.m_normals[j].X * delta), ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y - this.m_normals[j].Y * delta)); + this.m_destPoly.push(pt1); + } + else + { + var j = len - 1; + k = len - 2; + this.m_sinA = 0; + this.m_normals[j] = new ClipperLib.DoublePoint(-this.m_normals[j].X, -this.m_normals[j].Y); + if (node.m_endtype == ClipperLib.EndType.etOpenSquare) + this.DoSquare(j, k); + else + this.DoRound(j, k); + } + //re-build m_normals ... + for (var j = len - 1; j > 0; j--) + this.m_normals[j] = new ClipperLib.DoublePoint(-this.m_normals[j - 1].X, -this.m_normals[j - 1].Y); + this.m_normals[0] = new ClipperLib.DoublePoint(-this.m_normals[1].X, -this.m_normals[1].Y); + k = len - 1; + for (var j = k - 1; j > 0; --j) + k = this.OffsetPoint(j, k, node.m_jointype); + if (node.m_endtype == ClipperLib.EndType.etOpenButt) + { + pt1 = new ClipperLib.IntPoint(ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].X - this.m_normals[0].X * delta), ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].Y - this.m_normals[0].Y * delta)); + this.m_destPoly.push(pt1); + pt1 = new ClipperLib.IntPoint(ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].X + this.m_normals[0].X * delta), ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].Y + this.m_normals[0].Y * delta)); + this.m_destPoly.push(pt1); + } + else + { + k = 1; + this.m_sinA = 0; + if (node.m_endtype == ClipperLib.EndType.etOpenSquare) + this.DoSquare(0, 1); + else + this.DoRound(0, 1); + } + this.m_destPolys.push(this.m_destPoly); + } + } + }; + ClipperLib.ClipperOffset.prototype.Execute = function () + { + var a = arguments, + ispolytree = a[0] instanceof ClipperLib.PolyTree; + if (!ispolytree) // function (solution, delta) + { + var solution = a[0], + delta = a[1]; + ClipperLib.Clear(solution); + this.FixOrientations(); + this.DoOffset(delta); + //now clean up 'corners' ... + var clpr = new ClipperLib.Clipper(0); + clpr.AddPaths(this.m_destPolys, ClipperLib.PolyType.ptSubject, true); + if (delta > 0) + { + clpr.Execute(ClipperLib.ClipType.ctUnion, solution, ClipperLib.PolyFillType.pftPositive, ClipperLib.PolyFillType.pftPositive); + } + else + { + var r = ClipperLib.Clipper.GetBounds(this.m_destPolys); + var outer = new ClipperLib.Path(); + outer.push(new ClipperLib.IntPoint(r.left - 10, r.bottom + 10)); + outer.push(new ClipperLib.IntPoint(r.right + 10, r.bottom + 10)); + outer.push(new ClipperLib.IntPoint(r.right + 10, r.top - 10)); + outer.push(new ClipperLib.IntPoint(r.left - 10, r.top - 10)); + clpr.AddPath(outer, ClipperLib.PolyType.ptSubject, true); + clpr.ReverseSolution = true; + clpr.Execute(ClipperLib.ClipType.ctUnion, solution, ClipperLib.PolyFillType.pftNegative, ClipperLib.PolyFillType.pftNegative); + if (solution.length > 0) + solution.splice(0, 1); + } + //console.log(JSON.stringify(solution)); + } + else // function (polytree, delta) + { + var solution = a[0], + delta = a[1]; + solution.Clear(); + this.FixOrientations(); + this.DoOffset(delta); + //now clean up 'corners' ... + var clpr = new ClipperLib.Clipper(0); + clpr.AddPaths(this.m_destPolys, ClipperLib.PolyType.ptSubject, true); + if (delta > 0) + { + clpr.Execute(ClipperLib.ClipType.ctUnion, solution, ClipperLib.PolyFillType.pftPositive, ClipperLib.PolyFillType.pftPositive); + } + else + { + var r = ClipperLib.Clipper.GetBounds(this.m_destPolys); + var outer = new ClipperLib.Path(); + outer.push(new ClipperLib.IntPoint(r.left - 10, r.bottom + 10)); + outer.push(new ClipperLib.IntPoint(r.right + 10, r.bottom + 10)); + outer.push(new ClipperLib.IntPoint(r.right + 10, r.top - 10)); + outer.push(new ClipperLib.IntPoint(r.left - 10, r.top - 10)); + clpr.AddPath(outer, ClipperLib.PolyType.ptSubject, true); + clpr.ReverseSolution = true; + clpr.Execute(ClipperLib.ClipType.ctUnion, solution, ClipperLib.PolyFillType.pftNegative, ClipperLib.PolyFillType.pftNegative); + //remove the outer PolyNode rectangle ... + if (solution.ChildCount() == 1 && solution.Childs()[0].ChildCount() > 0) + { + var outerNode = solution.Childs()[0]; + //solution.Childs.set_Capacity(outerNode.ChildCount); + solution.Childs()[0] = outerNode.Childs()[0]; + for (var i = 1; i < outerNode.ChildCount(); i++) + solution.AddChild(outerNode.Childs()[i]); + } + else + solution.Clear(); + } + } + }; + ClipperLib.ClipperOffset.prototype.OffsetPoint = function (j, k, jointype) + { + this.m_sinA = (this.m_normals[k].X * this.m_normals[j].Y - this.m_normals[j].X * this.m_normals[k].Y); + if (this.m_sinA < 0.00005 && this.m_sinA > -0.00005) + return k; + else if (this.m_sinA > 1) + this.m_sinA = 1.0; + else if (this.m_sinA < -1) + this.m_sinA = -1.0; + if (this.m_sinA * this.m_delta < 0) + { + this.m_destPoly.push(new ClipperLib.IntPoint(ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + this.m_normals[k].X * this.m_delta), + ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + this.m_normals[k].Y * this.m_delta))); + this.m_destPoly.push(new ClipperLib.IntPoint(this.m_srcPoly[j])); + this.m_destPoly.push(new ClipperLib.IntPoint(ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + this.m_normals[j].X * this.m_delta), + ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + this.m_normals[j].Y * this.m_delta))); + } + else + switch (jointype) + { + case ClipperLib.JoinType.jtMiter: + { + var r = 1 + (this.m_normals[j].X * this.m_normals[k].X + this.m_normals[j].Y * this.m_normals[k].Y); + if (r >= this.m_miterLim) + this.DoMiter(j, k, r); + else + this.DoSquare(j, k); + break; + } + case ClipperLib.JoinType.jtSquare: + this.DoSquare(j, k); + break; + case ClipperLib.JoinType.jtRound: + this.DoRound(j, k); + break; + } + k = j; + return k; + }; + ClipperLib.ClipperOffset.prototype.DoSquare = function (j, k) + { + var dx = Math.tan(Math.atan2(this.m_sinA, + this.m_normals[k].X * this.m_normals[j].X + this.m_normals[k].Y * this.m_normals[j].Y) / 4); + this.m_destPoly.push(new ClipperLib.IntPoint( + ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + this.m_delta * (this.m_normals[k].X - this.m_normals[k].Y * dx)), + ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + this.m_delta * (this.m_normals[k].Y + this.m_normals[k].X * dx)))); + this.m_destPoly.push(new ClipperLib.IntPoint( + ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + this.m_delta * (this.m_normals[j].X + this.m_normals[j].Y * dx)), + ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + this.m_delta * (this.m_normals[j].Y - this.m_normals[j].X * dx)))); + }; + ClipperLib.ClipperOffset.prototype.DoMiter = function (j, k, r) + { + var q = this.m_delta / r; + this.m_destPoly.push(new ClipperLib.IntPoint( + ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + (this.m_normals[k].X + this.m_normals[j].X) * q), + ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + (this.m_normals[k].Y + this.m_normals[j].Y) * q))); + }; + ClipperLib.ClipperOffset.prototype.DoRound = function (j, k) + { + var a = Math.atan2(this.m_sinA, + this.m_normals[k].X * this.m_normals[j].X + this.m_normals[k].Y * this.m_normals[j].Y); + var steps = ClipperLib.Cast_Int32(ClipperLib.ClipperOffset.Round(this.m_StepsPerRad * Math.abs(a))); + var X = this.m_normals[k].X, + Y = this.m_normals[k].Y, + X2; + for (var i = 0; i < steps; ++i) + { + this.m_destPoly.push(new ClipperLib.IntPoint( + ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + X * this.m_delta), + ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + Y * this.m_delta))); + X2 = X; + X = X * this.m_cos - this.m_sin * Y; + Y = X2 * this.m_sin + Y * this.m_cos; + } + this.m_destPoly.push(new ClipperLib.IntPoint( + ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + this.m_normals[j].X * this.m_delta), + ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + this.m_normals[j].Y * this.m_delta))); + }; + ClipperLib.Error = function (message) + { + try + { + throw new Error(message); + } + catch (err) + { + alert(err.message); + } + }; + // --------------------------------- + // JS extension by Timo 2013 + ClipperLib.JS = {}; + ClipperLib.JS.AreaOfPolygon = function (poly, scale) + { + if (!scale) scale = 1; + return ClipperLib.Clipper.Area(poly) / (scale * scale); + }; + ClipperLib.JS.AreaOfPolygons = function (poly, scale) + { + if (!scale) scale = 1; + var area = 0; + for (var i = 0; i < poly.length; i++) + { + area += ClipperLib.Clipper.Area(poly[i]); + } + return area / (scale * scale); + }; + ClipperLib.JS.BoundsOfPath = function (path, scale) + { + return ClipperLib.JS.BoundsOfPaths([path], scale); + }; + ClipperLib.JS.BoundsOfPaths = function (paths, scale) + { + if (!scale) scale = 1; + var bounds = ClipperLib.Clipper.GetBounds(paths); + bounds.left /= scale; + bounds.bottom /= scale; + bounds.right /= scale; + bounds.top /= scale; + return bounds; + }; + // Clean() joins vertices that are too near each other + // and causes distortion to offsetted polygons without cleaning + ClipperLib.JS.Clean = function (polygon, delta) + { + if (!(polygon instanceof Array)) return []; + var isPolygons = polygon[0] instanceof Array; + var polygon = ClipperLib.JS.Clone(polygon); + if (typeof delta != "number" || delta === null) + { + ClipperLib.Error("Delta is not a number in Clean()."); + return polygon; + } + if (polygon.length === 0 || (polygon.length == 1 && polygon[0].length === 0) || delta < 0) return polygon; + if (!isPolygons) polygon = [polygon]; + var k_length = polygon.length; + var len, poly, result, d, p, j, i; + var results = []; + for (var k = 0; k < k_length; k++) + { + poly = polygon[k]; + len = poly.length; + if (len === 0) continue; + else if (len < 3) + { + result = poly; + results.push(result); + continue; + } + result = poly; + d = delta * delta; + //d = Math.floor(c_delta * c_delta); + p = poly[0]; + j = 1; + for (i = 1; i < len; i++) + { + if ((poly[i].X - p.X) * (poly[i].X - p.X) + + (poly[i].Y - p.Y) * (poly[i].Y - p.Y) <= d) + continue; + result[j] = poly[i]; + p = poly[i]; + j++; + } + p = poly[j - 1]; + if ((poly[0].X - p.X) * (poly[0].X - p.X) + + (poly[0].Y - p.Y) * (poly[0].Y - p.Y) <= d) + j--; + if (j < len) + result.splice(j, len - j); + if (result.length) results.push(result); + } + if (!isPolygons && results.length) results = results[0]; + else if (!isPolygons && results.length === 0) results = []; + else if (isPolygons && results.length === 0) results = [ + [] + ]; + return results; + } + // Make deep copy of Polygons or Polygon + // so that also IntPoint objects are cloned and not only referenced + // This should be the fastest way + ClipperLib.JS.Clone = function (polygon) + { + if (!(polygon instanceof Array)) return []; + if (polygon.length === 0) return []; + else if (polygon.length == 1 && polygon[0].length === 0) return [[]]; + var isPolygons = polygon[0] instanceof Array; + if (!isPolygons) polygon = [polygon]; + var len = polygon.length, + plen, i, j, result; + var results = new Array(len); + for (i = 0; i < len; i++) + { + plen = polygon[i].length; + result = new Array(plen); + for (j = 0; j < plen; j++) + { + result[j] = { + X: polygon[i][j].X, + Y: polygon[i][j].Y + }; + } + results[i] = result; + } + if (!isPolygons) results = results[0]; + return results; + }; + // Removes points that doesn't affect much to the visual appearance. + // If middle point is at or under certain distance (tolerance) of the line segment between + // start and end point, the middle point is removed. + ClipperLib.JS.Lighten = function (polygon, tolerance) + { + if (!(polygon instanceof Array)) return []; + if (typeof tolerance != "number" || tolerance === null) + { + ClipperLib.Error("Tolerance is not a number in Lighten().") + return ClipperLib.JS.Clone(polygon); + } + if (polygon.length === 0 || (polygon.length == 1 && polygon[0].length === 0) || tolerance < 0) + { + return ClipperLib.JS.Clone(polygon); + } + if (!(polygon[0] instanceof Array)) polygon = [polygon]; + var i, j, poly, k, poly2, plen, A, B, P, d, rem, addlast; + var bxax, byay, l, ax, ay; + var len = polygon.length; + var toleranceSq = tolerance * tolerance; + var results = []; + for (i = 0; i < len; i++) + { + poly = polygon[i]; + plen = poly.length; + if (plen == 0) continue; + for (k = 0; k < 1000000; k++) // could be forever loop, but wiser to restrict max repeat count + { + poly2 = []; + plen = poly.length; + // the first have to added to the end, if first and last are not the same + // this way we ensure that also the actual last point can be removed if needed + if (poly[plen - 1].X != poly[0].X || poly[plen - 1].Y != poly[0].Y) + { + addlast = 1; + poly.push( + { + X: poly[0].X, + Y: poly[0].Y + }); + plen = poly.length; + } + else addlast = 0; + rem = []; // Indexes of removed points + for (j = 0; j < plen - 2; j++) + { + A = poly[j]; // Start point of line segment + P = poly[j + 1]; // Middle point. This is the one to be removed. + B = poly[j + 2]; // End point of line segment + ax = A.X; + ay = A.Y; + bxax = B.X - ax; + byay = B.Y - ay; + if (bxax !== 0 || byay !== 0) // To avoid Nan, when A==P && P==B. And to avoid peaks (A==B && A!=P), which have lenght, but not area. + { + l = ((P.X - ax) * bxax + (P.Y - ay) * byay) / (bxax * bxax + byay * byay); + if (l > 1) + { + ax = B.X; + ay = B.Y; + } + else if (l > 0) + { + ax += bxax * l; + ay += byay * l; + } + } + bxax = P.X - ax; + byay = P.Y - ay; + d = bxax * bxax + byay * byay; + if (d <= toleranceSq) + { + rem[j + 1] = 1; + j++; // when removed, transfer the pointer to the next one + } + } + // add all unremoved points to poly2 + poly2.push( + { + X: poly[0].X, + Y: poly[0].Y + }); + for (j = 1; j < plen - 1; j++) + if (!rem[j]) poly2.push( + { + X: poly[j].X, + Y: poly[j].Y + }); + poly2.push( + { + X: poly[plen - 1].X, + Y: poly[plen - 1].Y + }); + // if the first point was added to the end, remove it + if (addlast) poly.pop(); + // break, if there was not anymore removed points + if (!rem.length) break; + // else continue looping using poly2, to check if there are points to remove + else poly = poly2; + } + plen = poly2.length; + // remove duplicate from end, if needed + if (poly2[plen - 1].X == poly2[0].X && poly2[plen - 1].Y == poly2[0].Y) + { + poly2.pop(); + } + if (poly2.length > 2) // to avoid two-point-polygons + results.push(poly2); + } + if (!(polygon[0] instanceof Array)) results = results[0]; + if (typeof (results) == "undefined") results = [ + [] + ]; + return results; + } + ClipperLib.JS.PerimeterOfPath = function (path, closed, scale) + { + if (typeof (path) == "undefined") return 0; + var sqrt = Math.sqrt; + var perimeter = 0.0; + var p1, p2, p1x = 0.0, + p1y = 0.0, + p2x = 0.0, + p2y = 0.0; + var j = path.length; + if (j < 2) return 0; + if (closed) + { + path[j] = path[0]; + j++; + } + while (--j) + { + p1 = path[j]; + p1x = p1.X; + p1y = p1.Y; + p2 = path[j - 1]; + p2x = p2.X; + p2y = p2.Y; + perimeter += sqrt((p1x - p2x) * (p1x - p2x) + (p1y - p2y) * (p1y - p2y)); + } + if (closed) path.pop(); + return perimeter / scale; + }; + ClipperLib.JS.PerimeterOfPaths = function (paths, closed, scale) + { + if (!scale) scale = 1; + var perimeter = 0; + for (var i = 0; i < paths.length; i++) + { + perimeter += ClipperLib.JS.PerimeterOfPath(paths[i], closed, scale); + } + return perimeter; + }; + ClipperLib.JS.ScaleDownPath = function (path, scale) + { + var i, p; + if (!scale) scale = 1; + i = path.length; + while (i--) + { + p = path[i]; + p.X = p.X / scale; + p.Y = p.Y / scale; + } + }; + ClipperLib.JS.ScaleDownPaths = function (paths, scale) + { + var i, j, p, round = Math.round; + if (!scale) scale = 1; + i = paths.length; + while (i--) + { + j = paths[i].length; + while (j--) + { + p = paths[i][j]; + p.X = p.X / scale; + p.Y = p.Y / scale; + } + } + }; + ClipperLib.JS.ScaleUpPath = function (path, scale) + { + var i, p, round = Math.round; + if (!scale) scale = 1; + i = path.length; + while (i--) + { + p = path[i]; + p.X = round(p.X * scale); + p.Y = round(p.Y * scale); + } + }; + ClipperLib.JS.ScaleUpPaths = function (paths, scale) + { + var i, j, p, round = Math.round; + if (!scale) scale = 1; + i = paths.length; + while (i--) + { + j = paths[i].length; + while (j--) + { + p = paths[i][j]; + p.X = round(p.X * scale); + p.Y = round(p.Y * scale); + } + } + }; + ClipperLib.ExPolygons = function () + { + return []; + } + ClipperLib.ExPolygon = function () + { + this.outer = null; + this.holes = null; + }; + ClipperLib.JS.AddOuterPolyNodeToExPolygons = function (polynode, expolygons) + { + var ep = new ClipperLib.ExPolygon(); + ep.outer = polynode.Contour(); + var childs = polynode.Childs(); + var ilen = childs.length; + ep.holes = new Array(ilen); + var node, n, i, j, childs2, jlen; + for (i = 0; i < ilen; i++) + { + node = childs[i]; + ep.holes[i] = node.Contour(); + //Add outer polygons contained by (nested within) holes ... + for (j = 0, childs2 = node.Childs(), jlen = childs2.length; j < jlen; j++) + { + n = childs2[j]; + ClipperLib.JS.AddOuterPolyNodeToExPolygons(n, expolygons); + } + } + expolygons.push(ep); + }; + ClipperLib.JS.ExPolygonsToPaths = function (expolygons) + { + var a, i, alen, ilen; + var paths = new ClipperLib.Paths(); + for (a = 0, alen = expolygons.length; a < alen; a++) + { + paths.push(expolygons[a].outer); + for (i = 0, ilen = expolygons[a].holes.length; i < ilen; i++) + { + paths.push(expolygons[a].holes[i]); + } + } + return paths; + } + ClipperLib.JS.PolyTreeToExPolygons = function (polytree) + { + var expolygons = new ClipperLib.ExPolygons(); + var node, i, childs, ilen; + for (i = 0, childs = polytree.Childs(), ilen = childs.length; i < ilen; i++) + { + node = childs[i]; + ClipperLib.JS.AddOuterPolyNodeToExPolygons(node, expolygons); + } + return expolygons; + }; +})(); diff --git a/js/ext-fsave.js b/js/ext-fsave.js new file mode 100644 index 00000000..75d71ab0 --- /dev/null +++ b/js/ext-fsave.js @@ -0,0 +1,270 @@ +/* FileSaver.js + * A saveAs() FileSaver implementation. + * 1.1.20151003 + * + * By Eli Grey, http://eligrey.com + * License: MIT + * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md + */ + +/*global self */ +/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */ + +/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ + +var saveAs = saveAs || (function(view) { + "use strict"; + // IE <10 is explicitly unsupported + if (typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) { + return; + } + var + doc = view.document + // only get URL when necessary in case Blob.js hasn't overridden it yet + , get_URL = function() { + return view.URL || view.webkitURL || view; + } + , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") + , can_use_save_link = "download" in save_link + , click = function(node) { + var event = new MouseEvent("click"); + node.dispatchEvent(event); + } + , is_safari = /Version\/[\d\.]+.*Safari/.test(navigator.userAgent) + , webkit_req_fs = view.webkitRequestFileSystem + , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem + , throw_outside = function(ex) { + (view.setImmediate || view.setTimeout)(function() { + throw ex; + }, 0); + } + , force_saveable_type = "application/octet-stream" + , fs_min_size = 0 + // See https://code.google.com/p/chromium/issues/detail?id=375297#c7 and + // https://github.com/eligrey/FileSaver.js/commit/485930a#commitcomment-8768047 + // for the reasoning behind the timeout and revocation flow + , arbitrary_revoke_timeout = 500 // in ms + , revoke = function(file) { + var revoker = function() { + if (typeof file === "string") { // file is an object URL + get_URL().revokeObjectURL(file); + } else { // file is a File + file.remove(); + } + }; + if (view.chrome) { + revoker(); + } else { + setTimeout(revoker, arbitrary_revoke_timeout); + } + } + , dispatch = function(filesaver, event_types, event) { + event_types = [].concat(event_types); + var i = event_types.length; + while (i--) { + var listener = filesaver["on" + event_types[i]]; + if (typeof listener === "function") { + try { + listener.call(filesaver, event || filesaver); + } catch (ex) { + throw_outside(ex); + } + } + } + } + , auto_bom = function(blob) { + // prepend BOM for UTF-8 XML and text/* types (including HTML) + if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { + return new Blob(["\ufeff", blob], {type: blob.type}); + } + return blob; + } + , FileSaver = function(blob, name, no_auto_bom) { + if (!no_auto_bom) { + blob = auto_bom(blob); + } + // First try a.download, then web filesystem, then object URLs + var + filesaver = this + , type = blob.type + , blob_changed = false + , object_url + , target_view + , dispatch_all = function() { + dispatch(filesaver, "writestart progress write writeend".split(" ")); + } + // on any filesys errors revert to saving with object URLs + , fs_error = function() { + if (target_view && is_safari && typeof FileReader !== "undefined") { + // Safari doesn't allow downloading of blob urls + var reader = new FileReader(); + reader.onloadend = function() { + var base64Data = reader.result; + target_view.location.href = "data:attachment/file" + base64Data.slice(base64Data.search(/[,;]/)); + filesaver.readyState = filesaver.DONE; + dispatch_all(); + }; + reader.readAsDataURL(blob); + filesaver.readyState = filesaver.INIT; + return; + } + // don't create more object URLs than needed + if (blob_changed || !object_url) { + object_url = get_URL().createObjectURL(blob); + } + if (target_view) { + target_view.location.href = object_url; + } else { + var new_tab = view.open(object_url, "_blank"); + if (new_tab == undefined && is_safari) { + //Apple do not allow window.open, see http://bit.ly/1kZffRI + view.location.href = object_url + } + } + filesaver.readyState = filesaver.DONE; + dispatch_all(); + revoke(object_url); + } + , abortable = function(func) { + return function() { + if (filesaver.readyState !== filesaver.DONE) { + return func.apply(this, arguments); + } + }; + } + , create_if_not_found = {create: true, exclusive: false} + , slice + ; + filesaver.readyState = filesaver.INIT; + if (!name) { + name = "download"; + } + if (can_use_save_link) { + object_url = get_URL().createObjectURL(blob); + setTimeout(function() { + save_link.href = object_url; + save_link.download = name; + click(save_link); + dispatch_all(); + revoke(object_url); + filesaver.readyState = filesaver.DONE; + }); + return; + } + // Object and web filesystem URLs have a problem saving in Google Chrome when + // viewed in a tab, so I force save with application/octet-stream + // http://code.google.com/p/chromium/issues/detail?id=91158 + // Update: Google errantly closed 91158, I submitted it again: + // https://code.google.com/p/chromium/issues/detail?id=389642 + if (view.chrome && type && type !== force_saveable_type) { + slice = blob.slice || blob.webkitSlice; + blob = slice.call(blob, 0, blob.size, force_saveable_type); + blob_changed = true; + } + // Since I can't be sure that the guessed media type will trigger a download + // in WebKit, I append .download to the filename. + // https://bugs.webkit.org/show_bug.cgi?id=65440 + if (webkit_req_fs && name !== "download") { + name += ".download"; + } + if (type === force_saveable_type || webkit_req_fs) { + target_view = view; + } + if (!req_fs) { + fs_error(); + return; + } + fs_min_size += blob.size; + req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) { + fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) { + var save = function() { + dir.getFile(name, create_if_not_found, abortable(function(file) { + file.createWriter(abortable(function(writer) { + writer.onwriteend = function(event) { + target_view.location.href = file.toURL(); + filesaver.readyState = filesaver.DONE; + dispatch(filesaver, "writeend", event); + revoke(file); + }; + writer.onerror = function() { + var error = writer.error; + if (error.code !== error.ABORT_ERR) { + fs_error(); + } + }; + "writestart progress write abort".split(" ").forEach(function(event) { + writer["on" + event] = filesaver["on" + event]; + }); + writer.write(blob); + filesaver.abort = function() { + writer.abort(); + filesaver.readyState = filesaver.DONE; + }; + filesaver.readyState = filesaver.WRITING; + }), fs_error); + }), fs_error); + }; + dir.getFile(name, {create: false}, abortable(function(file) { + // delete file if it already exists + file.remove(); + save(); + }), abortable(function(ex) { + if (ex.code === ex.NOT_FOUND_ERR) { + save(); + } else { + fs_error(); + } + })); + }), fs_error); + }), fs_error); + } + , FS_proto = FileSaver.prototype + , saveAs = function(blob, name, no_auto_bom) { + return new FileSaver(blob, name, no_auto_bom); + } + ; + // IE 10+ (native saveAs) + if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) { + return function(blob, name, no_auto_bom) { + if (!no_auto_bom) { + blob = auto_bom(blob); + } + return navigator.msSaveOrOpenBlob(blob, name || "download"); + }; + } + + FS_proto.abort = function() { + var filesaver = this; + filesaver.readyState = filesaver.DONE; + dispatch(filesaver, "abort"); + }; + FS_proto.readyState = FS_proto.INIT = 0; + FS_proto.WRITING = 1; + FS_proto.DONE = 2; + + FS_proto.error = + FS_proto.onwritestart = + FS_proto.onprogress = + FS_proto.onwrite = + FS_proto.onabort = + FS_proto.onerror = + FS_proto.onwriteend = + null; + + return saveAs; + }( + typeof self !== "undefined" && self + || typeof window !== "undefined" && window + || this.content + )); +// `self` is undefined in Firefox for Android content script context +// while `this` is nsIContentFrameMessageManager +// with an attribute `content` that corresponds to the window + +if (typeof module !== "undefined" && module.exports) { + module.exports.saveAs = saveAs; +} else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) { + define([], function() { + return saveAs; + }); +} diff --git a/js/ext-n3d.js b/js/ext-n3d.js new file mode 120000 index 00000000..8b0d2721 --- /dev/null +++ b/js/ext-n3d.js @@ -0,0 +1 @@ +../node_modules/n3d-threejs/index.js \ No newline at end of file diff --git a/js/ext-pixi.js b/js/ext-pixi.js new file mode 120000 index 00000000..f220158c --- /dev/null +++ b/js/ext-pixi.js @@ -0,0 +1 @@ +../node_modules/pixi.js/bin/pixi.js \ No newline at end of file diff --git a/js/ext-tween.js b/js/ext-tween.js new file mode 120000 index 00000000..641b1dea --- /dev/null +++ b/js/ext-tween.js @@ -0,0 +1 @@ +../node_modules/tween.js/index.js \ No newline at end of file diff --git a/js/geo-bounds.js b/js/geo-bounds.js new file mode 100644 index 00000000..9098d819 --- /dev/null +++ b/js/geo-bounds.js @@ -0,0 +1,141 @@ +"use strict"; + +var gs_base_bounds = { + copyright:"stewart allen -- all rights reserved" +}; + +(function() { + + if (!self.base) self.base = {}; + if (self.base.Bounds) return; + + /** + * + * @constructor + */ + function Bounds() { + this.minx = 10e7; + this.miny = 10e7; + this.maxx = -10e7; + this.maxy = -10e7; + this.leftMost = null; + } + + var BASE = self.base, + UTIL = BASE.util, + CONF = BASE.config, + ABS = Math.abs, + MIN = Math.min, + MAX = Math.max, + BoP = Bounds.prototype; + + BASE.Bounds = Bounds; + BASE.newBounds = newBounds; + + /** ****************************************************************** + * Bounds Prototype Functions + ******************************************************************* */ + + /** + * @returns {Bounds} + */ + BoP.clone = function() { + var b = new Bounds(); + b.minx = this.minx; + b.miny = this.miny; + b.maxx = this.maxx; + b.maxy = this.maxy; + return b; + }; + + BoP.equals = function(bounds, margin) { + if (!margin) margin = BASE.config.precision_offset; + return UTIL.isCloseTo(this.minx, bounds.minx, margin) && + UTIL.isCloseTo(this.miny, bounds.miny, margin) && + UTIL.isCloseTo(this.maxx, bounds.maxx, margin) && + UTIL.isCloseTo(this.maxy, bounds.maxy, margin); + }; + + /** + * @param {Bounds} b + */ + BoP.merge = function(b) { + this.minx = MIN(this.minx, b.minx); + this.maxx = MAX(this.maxx, b.maxx); + this.miny = MIN(this.miny, b.miny); + this.maxy = MAX(this.maxy, b.maxy); + }; + + /** + * @param {Point} p + */ + BoP.update = function(p) { + this.minx = MIN(this.minx, p.x); + this.maxx = MAX(this.maxx, p.x); + this.miny = MIN(this.miny, p.y); + this.maxy = MAX(this.maxy, p.y); + if (this.minx === p.x) this.leftMost = p; + }; + + BoP.contains = function(bounds) { + return bounds.isNested(this); + }; + + BoP.containsXY = function(x,y) { + return x >= this.minx && x <= this.maxx && y >= this.miny && y <= this.maxy; + }; + + BoP.containsOffsetXY = function(x,y,offset) { + return x >= this.minx-offset && x <= this.maxx+offset && y >= this.miny-offset && y <= this.maxy+offset; + }; + + /** + * @param {Bounds} parent + * @returns {boolean} true if fully inside parent bounds + */ + BoP.isNested = function(parent) { + return ( + this.minx >= parent.minx - CONF.precision_bounds && // min-x + this.maxx <= parent.maxx + CONF.precision_bounds && // max-x + this.miny >= parent.miny - CONF.precision_bounds && // min-y + this.maxy <= parent.maxy + CONF.precision_bounds // max-y + ); + }; + + /** + * @param {Bounds} b + * @param {number} precision + * @returns {boolean} + */ + BoP.overlaps = function(b, precision) { + return ( + ABS(this.centerx() - b.centerx()) * 2 - precision < this.width() + b.width() && + ABS(this.centery() - b.centery()) * 2 - precision < this.height() + b.height() + ); + }; + + BoP.width = function() { + return this.maxx - this.minx; + }; + + BoP.height = function() { + return this.maxy - this.miny; + }; + + BoP.centerx = function() { + return this.minx + this.width() / 2; + }; + + BoP.centery = function() { + return this.miny + this.height() / 2; + }; + + /** ****************************************************************** + * Connect to base and Helpers + ******************************************************************* */ + + function newBounds() { + return new Bounds(); + } + +})(); diff --git a/js/geo-debug.js b/js/geo-debug.js new file mode 100644 index 00000000..5595dfd5 --- /dev/null +++ b/js/geo-debug.js @@ -0,0 +1,221 @@ +"use strict"; + +var gs_base_debug = { + copyright:"stewart allen -- all rights reserved" +}; + +(function() { + + if (!self.base) self.base = {}; + if (self.base.debug) return; + + var base = self.base, + enabled = false, + flags = {}, + stash = [], + size = 20, + next_debug_color = 0, + debug_colors = [0xff0000, 0x00ff00, 0x0000ff, 0x00ffff, 0xff00ff, 0xffff00]; + + /** ****************************************************************** + * Debug Functions + ******************************************************************* */ + + function log(o) { + console.log(o); + if (enabled) { + stash.push(o); + while (stash.length > size) stash.shift(); + } + } + + function enable(history) { + enabled = true; + if (history) size = Math.abs(history); + } + + function disable() { + enabled = false; + } + + function on() { + return enabled; + } + + function history() { + return stash; + } + + function last() { + return stash[stash.length-1]; + } + + function trace(msg) { + if (msg && typeof msg != 'string') msg = JSON.stringify(msg); + log(new Error(msg).stack); + } + + function view() { + return base.debug.view; + } + + function setView(view) { + base.debug.view = view; + } + + function get(f) { + return flags[f]; + } + + function set(f, value) { + if (Array.isArray(f)) { + for (var i=0; i -- all rights reserved" +}; + +(function() { + + if (!self.base) self.base = {}; + if (self.base.Line) return; + + /** + * + * @param {Point} p1 + * @param {Point} p2 + * @param {String} [key] + * @constructor + */ + function Line(p1, p2, key) { + if (!key) key = [p1.key, p2.key].join('-'); + this.p1 = p1; + this.p2 = p2; + this.key = key; + this.coplanar = false; + this.edge = false; + this.del = false; + } + + var BASE = self.base, + LiP = Line.prototype; + + BASE.Line = Line; + BASE.newLine = newLine; + BASE.newOrderedLine = newOrderedLine; + + /** ****************************************************************** + * Line Prototype Functions + ******************************************************************* */ + + /** + * @returns {number} + */ + LiP.length = function() { + return Math.sqrt(this.length2()); + }; + + /** + * @returns {number} square of length + */ + LiP.length2 = function() { + return this.p1.distToSq2D(this.p2); + }; + + /** + * @returns {Slope} + */ + LiP.slope = function() { + return BASE.newSlope(this.p1.slopeTo(this.p2)); + }; + + /** + * @returns {Line} + */ + LiP.reverse = function() { + var t = this.p1; + this.p1 = this.p2; + this.p2 = t; + return this; + }; + + /** + * @returns {Point} + */ + LiP.midpoint = function() { + return this.p1.midPointTo(this.p2); + }; + + /** + * @param {Line} line + * @returns {boolean} + */ + LiP.isCollinear = function(line) { + var p1 = this.p1, + p2 = this.p2, + p3 = line.p1, + p4 = line.p2, + d1x = (p2.x - p1.x), + d1y = (p2.y - p1.y), + d2x = (p4.x - p3.x), + d2y = (p4.y - p3.y); + + return Math.abs( (d2y * d1x) - (d2x * d1y) ) < 0.000001; + }; + + /** ****************************************************************** + * Connect to base and Helpers + ******************************************************************* */ + + /** + * + * @param {Point} p1 + * @param {Point} p2 + * @param {String} [key] + * @returns {Line} + */ + function newLine(p1, p2, key) { + return new Line(p1, p2, key); + } + + function newOrderedLine(p1, p2, key) { + return p1.key < p2.key ? newLine(p1,p2,key) : newLine(p2,p1,key); + } + +})(); diff --git a/js/geo-point.js b/js/geo-point.js new file mode 100644 index 00000000..8f699f66 --- /dev/null +++ b/js/geo-point.js @@ -0,0 +1,645 @@ +"use strict"; + +var gs_base_point = { + copyright:"stewart allen -- all rights reserved" +}; + +(function() { + + if (!self.base) self.base = {}; + if (self.base.Point) return; + + /** + * + * @param {number} x + * @param {number} y + * @param {number} z + * @param {String} [key] + * @constructor + */ + function Point(x,y,z,key,CP) { + // todo make more efficient for cloning when all 5 params are passed + if (CP) { + this.x = x || (CP.X / CONF.clipper) || 0; + this.y = y || (CP.Y / CONF.clipper) || 0; + this.z = z || 0; + this.X = CP.X; + this.Y = CP.Y; + this.key = null; + } else { + this.x = x; + this.y = y; + this.z = z || 0; + this.X = (x * CONF.clipper); + this.Y = (y * CONF.clipper); + this.key = key || [x, y, z].toString(); + } + this.poly = null; // parent polygon + this.dist = 0.0; // for group intersection sorting and tests + this.p1 = null; // used in sliceIntersect(), connectLines() and intersect() + this.p2 = null; // used in sliceIntersect(), connectLines() and intersect() + this.pos = 0; // position in group + this.mod = 0; // group length (for modulus of pos) + this.del = false; // for culling + this.group = null; // for grouping in slice intersect, offset lines in trace + } + + var BASE = self.base, + UTIL = BASE.util, + CONF = BASE.config, + KEYS = BASE.key, + ROUND = UTIL.round, + PoP = Point.prototype; + + BASE.Point = Point; + BASE.newPoint = newPoint; + + /** ****************************************************************** + * Point Prototype Functions + ******************************************************************* */ + + PoP.setZ = function(z) { + this.z = z; + return this; + } + + PoP.swapXZ = function() { + var p = this, + t = p.x; + p.x = p.z; + p.z = t; + }; + + PoP.swapYZ = function() { + var p = this, + t = p.y; + p.y = p.z; + p.z = t; + }; + + PoP.round = function(precision) { + return newPoint(ROUND(this.x,precision), ROUND(this.y,precision), ROUND(this.z,precision)); + }; + + PoP.addFacet = function(facet) { + if (!this.group) this.group = []; + this.group.push(facet); + return this; + }; + + PoP.rekey = function() { + this.key = [this.x,this.y,this.z].join(','); + }; + + PoP.toString = function() { + return this.key; + }; + + /** + * @returns {Point} + */ + PoP.clone = function() { + return newPoint(this.x, this.y, this.z, this.key); + }; + + /** + * @param {Point} p + * @returns {Slope} + */ + PoP.slopeTo = function(p) { + return BASE.newSlope(this, p); + }; + + /** + * + * @param {Point} p + * @param {String} [k] + * @returns {Line} + */ + PoP.lineTo = function(p, k) { + return BASE.newLine(this, p, k); + }; + + /** + * @param {Point} p + * @param {number} [dist] + * @returns {boolean} + */ + PoP.isNear = function(p, dist) { + return UTIL.isCloseTo(this.x, p.x, dist) && UTIL.isCloseTo(this.y, p.y, dist); + }; + + /** + * return distance to line connecting points p1, p2 + * distance is calculated on the perpendicular (normal) to line + * + * @param {Point} p1 + * @param {Point} p2 + * @returns {number} + */ + PoP.distToLine = function(p1, p2) { + return Math.sqrt(this.distToLineSq(p1, p2)); + }; + + /** + * return square of distance to line connecting points p1, p2 + * distance is calculated on the perpendicular (normal) to line + * + * @param {Point} p1 + * @param {Point} p2 + * @returns {number} + */ + PoP.distToLineSq = function(p1, p2) { + var p = this, + d = UTIL.distSq(p1, p2); + + var t = ((p.x - p1.x) * (p2.x - p1.x) + (p.y - p1.y) * (p2.y - p1.y)) / d; + + if (t < 0) return UTIL.distSq(p, p1); + if (t > 1) return UTIL.distSq(p, p2); + + return UTIL.distSqv2(p.x, p.y, p1.x + t * (p2.x - p1.x), p1.y + t * (p2.y - p1.y)); + }; + + /** + * + * @param {Point} p1 + * @param {Point} p2 + * @param {number} dist2 + * @returns {boolean} + */ + PoP.withinDist2 = function(p1, p2, dist2) { + var ll2 = p1.distToSq2D(p2), + dp1 = this.distToSq2D(p1), + dp2 = this.distToSq2D(p2); + // if the line segment described is less than dist2 + // then add dist2 to ll2. if this point is not closer + // than newll2 to either point, then it can't be closer + // to the described segment than dist2 + if (ll2 < dist2) { + ll2 += dist2; + if (dp1 > ll2 && dp2 > ll2) return false; + } + // if point is farther from each point that the distance + // between the points and that distance is greater than dist2 + // then it's not possible for the point to be closer than + // dist2 to the described line segment. + if (dp1 > ll2 && dp2 > ll2) return false; + return this.distToLineSq(p1, p2) < dist2; + }; + + /** + * @param {Point} p2 + * @returns {Point} + */ + PoP.midPointTo = function(p2) { + return newPoint((this.x + p2.x)/2, (this.y + p2.y)/2, this.z); + }; + + /** + * @param {Point} p2 + * @returns {Point} + */ + PoP.midPointTo3D = function(p2) { + return newPoint( + (this.x + p2.x)/2, + (this.y + p2.y)/2, + (this.z + p2.z)/2 + ); + }; + + /** + * non-scale corrected version of follow() + * + * @param slope + * @param mult + * @returns {Point} + */ + PoP.projectOnSlope = function(slope, mult) { + return newPoint( + this.x + slope.dx * mult, + this.y + slope.dy * mult, + this.z); + }; + + PoP.followTo = function(point, mult) { + return this.follow(this.slopeTo(point), mult); + }; + + /** + * return a point along the line this from point to p2 + * but offset by a distance. positive distances are + * closer to this point. + * + * @param p2 + * @param dist + */ + PoP.offsetPointFrom = function(p2, dist) { + var p1 = this, + dx = p2.x - p1.x, + dy = p2.y - p1.y, + ls = dist / Math.sqrt(dx * dx + dy * dy), + ox = dx * ls, + oy = dy * ls; + return newPoint(p2.x - ox, p2.y - oy, p2.z, KEYS.NONE); + }; + + /** + * @param {Point} p2 + * @param {number} offset + * @returns {Line} + */ + PoP.offsetLineTo = function(p2, offset) { + var p1 = this, + dx = p2.x - p1.x, + dy = p2.y - p1.y, + ls = offset / Math.sqrt(dx * dx + dy * dy), + ox = dx * ls, + oy = dy * ls, + np1 = newPoint(p1.x - oy, p1.y + ox, p1.z, KEYS.NONE), + np2 = newPoint(p2.x - oy, p2.y + ox, p2.z, KEYS.NONE); + np1.op = p1; + np2.op = p2; + return BASE.newLine(np1, np2, KEYS.NONE); + }; + + + /** + * checks if a point is inside of a polygon + * does not check children/holes + * + * @param {Polygon} poly + * @returns {boolean} + */ + PoP.inPolygon = function(poly) { + if (!poly.bounds.containsXY(this.x, this.y)) return false; + + var p = poly.points, pl = p.length, p1, p2, i, inside = false; + + for (i=0; i= this.y) != (p2.y >= this.y) && + (this.x <= (p2.x - p1.x) * (this.y - p1.y) / (p2.y - p1.y) + p1.x)) + { + inside = !inside; + } + } + + return inside; + }; + + /** + * returns true if the point is inside of a polygon but + * not inside any of it's children + * + * @param {Polygon | Polygon[]} poly + * @return {boolean} true if inside outer but not inner + */ + PoP.isInPolygon = function(poly) { + var point = this, i; + if (Array.isArray(poly)) { + for (i=0; i 0) && !this.nearPolygon(poly, mindist2); + }; + + /** + * returns a new point following given slope for given distance + * same as projectOnSlope() but scaled + * + * @param {Slope} slope + * @param {number} distance + * @returns {Point} + */ + PoP.follow = function(slope, distance) { + var ls = distance / Math.sqrt(slope.dx * slope.dx + slope.dy * slope.dy); + return newPoint(this.x + slope.dx * ls, this.y + slope.dy * ls, this.z); + }; + + /** + * for point, return z-plane intersecting point on line to next point + * + * @param {Point} p + * @param {number} z + * @returns {Point} + */ + PoP.intersectZ = function(p, z) { + var dx = p.x - this.x, + dy = p.y - this.y, + dz = p.z - this.z, + pct = 1 - ((p.z - z) / dz); + return newPoint(this.x + dx * pct, this.y + dy * pct, this.z + dz * pct); + }; + + /** + * @param {Point} p + * @returns {boolean} + */ + PoP.isEqual2D = function(p) { + return this === p || (this.x === p.x && this.y === p.y); + }; + + /** + * returns true if points are close enough to be considered equivalent + * + * @param {Point} p + * @returns {boolean} + */ + PoP.isMergable2D = function(p) { + return this.isEqual2D(p) || (this.distToSq2D(p) < CONF.precision_merge_sq); + }; + + /** + * compares 3D point + * + * @param {Point} p + * @returns {boolean} + */ + PoP.isEqual = function(p) { + return this === p || (this.x === p.x && this.y === p.y && this.z === p.z); + }; + + /** + * returns true if points are close enough to be considered equivalent + * + * @param {Point} p + * @returns {boolean} + */ + PoP.isMergable3D = function(p) { + return this.isEqual(p) || (this.distToSq3D(p) < CONF.precision_merge_sq); + }; + + /** + * return true if point is inside 2D square size dist*2 around p + * + * @param {Point} p + * @param {number} dist + * @returns {boolean} + */ + PoP.isInBox = function(p, dist) { + return Math.abs(this.x - p.x) < dist && Math.abs(this.y - p.y) < dist; + }; + + /** + * return min distance from point to a polygon + * stops searching if any point is closer than threshold + * + * @param {Polygon} poly + * @param {number} [threshold] stop looking if under threshold + */ + PoP.distToPolySegments = function(poly, threshold) { + var point = this, + mindist = Infinity; + poly.forEachSegment(function(p1, p2) { + const nextdist = Math.min(mindist, point.distToLine(p1, p2)); + mindist = Math.min(nextdist, mindist); + // returning true terminates forEachSegment() + if (mindist <= threshold) return true; + }); + return mindist; + }; + + /** + * @param {Polygon} poly + * @param {number} [threshold] stop looking if under threshold + */ + PoP.distToPolyPoints = function(poly, threshold) { + var point = this, mindist = Infinity; + poly.forEachPoint(function(pp) { + mindist = Math.min(mindist, point.distTo2D(pp)); + if (mindist < threshold) return true; + }); + return mindist; + }; + + /** + * @param {Point[]} points + * @param {number} max + * @returns {Point} nearest point (less than max) from array to this point + */ + PoP.nearestTo = function(points, max) { + if (!max) throw "missing max"; + var mind = Infinity, + minp = null, + i, p, d; + for (i=0; i 0; + if ((c.x - a.x) * as_y - (c.y - a.y) * as_x > 0 == s_ab) return false; + if ((c.x - b.x) * (this.y - b.y) - (c.y - b.y) * (this.x - b.x) > 0 != s_ab) return false; + return true; + }; + + /** + * returns true if point is on a line described by two points. + * test sum of distances p1->this + this->p2 ~= p1->p2 whens + * slopes from p1->this same as this->p2 + * + * @param {Point} p1 + * @param {Point} p2 + * @returns {boolean} + */ + PoP.onLine = function(p1, p2) { + return this.distToLine(p1, p2) < CONF.precision_point_on_line; + }; + + /** + * + * @param {THREE.Vector3} delta + * @return {Point} new offset point + */ + PoP.add = function(delta) { + return newPoint(this.x + delta.x, this.y + delta.y, this.z + delta.z); + }; + + /** + * + * @param {THREE.Vector3} delta + * @return {Point} new offset point + */ + PoP.sub = function(delta) { + return newPoint(this.x - delta.x, this.y - delta.y, this.z - delta.z); + }; + + /** + * + * @param {THREE.Vector3} delta + */ + PoP.move = function(delta) { + this.x += delta.x; + this.y += delta.y; + this.z += delta.z; + this.X += delta.x * CONF.clipper; + this.Y += delta.y * CONF.clipper; + return this; + }; + + /** ****************************************************************** + * Connect to base and Helpers + ******************************************************************* */ + + /** + * + * @param {number} x + * @param {number} y + * @param {number} z + * @param {String} [key] + * @param {Object} [CP] clipper point + * @returns {Point} + */ + function newPoint(x, y, z, key, CP) { + return new Point(x, y, z, key, CP); + } + +})(); diff --git a/js/geo-polygon.js b/js/geo-polygon.js new file mode 100644 index 00000000..3bcaca7b --- /dev/null +++ b/js/geo-polygon.js @@ -0,0 +1,1192 @@ +"use strict"; + +var gs_base_polygon = { + copyright:"stewart allen -- all rights reserved" +}; + +(function() { + + if (!self.base) self.base = {}; + if (self.base.Polygon) return; + + var BASE = self.base, + CONF = BASE.config, + UTIL = BASE.util, + DBUG = BASE.debug, + KEYS = BASE.key, + SQRT = Math.sqrt, + POLY = function() { return BASE.polygons }, + ABS = Math.abs, + MIN = Math.min, + MAX = Math.max, + PI = Math.PI, + DEG2RAD = PI / 180, + Bounds = BASE.Bounds, + newPoint = BASE.newPoint, + PlP = Polygon.prototype, + nest_test_slope = BASE.newSlope(newPoint(0,0,0,KEYS.NONE), newPoint(1,0,0,KEYS.NONE)), + min_area_mult = 2.0, + seqid = 1; + + BASE.Polygon = Polygon; + BASE.newPolygon = newPolygon; + + /** ****************************************************************** + * Constructors + ******************************************************************* */ + + /** + * @param {Points[]} [points] to seed poly + * @constructor + */ + function Polygon(points) { + this.id = seqid++; // polygon unique id + this.open = false; + this.length = 0; // number of points + this.points = []; // ordered array of points + this.area2 = 0.0; // computed as 2x area (sign = direction) + this.bounds = new Bounds(); + this.inner = null; // array of enclosed polygons (if any) + this.parent = null; // enclosing parent polygon + this.depth = 0; // depth nested from top parent (density for support fill) + this.delete = false; // for culling during tracing + this.fillang = null; // hinted fill angle + this.fills = null; // fill lines (only used by supports currently) + if (points) this.addPoints(points); + } + + /** ****************************************************************** + * Polygon Filter/Chain Functions + ******************************************************************* */ + + Polygon.filterTooSmall = function(p) { + return p.length < 3 ? null : p; + }; + + Polygon.filterTooSkinny = function(p) { + return (p.circularityDeep() * p.areaDeep()) < 0.25 ? null : p; + }; + + Polygon.filterArea = function(area) { + return function(p) { + return p.area() >= area ? p : null; + }; + }; + + Polygon.filterDeleted = function(p) { + return p.delete ? null : p; + }; + + Polygon.filterInside = function(pin) { + return function(p) { + return pin.contains(p); + } + }; + + Polygon.filterCollect = function(out) { + return function(p) { + out.push(p); + return p; + } + }; + + Polygon.filterEvolve = function(p) { + return p.evolve(); + }; + + Polygon.filterChain = function(filters) { + return function() { + var f = filters; + return function(p) { + var len = f.length, + idx = 0; + while (idx < len && (p = f[idx++](p))) + ; + return p; + } + }(); + }; + + /** ****************************************************************** + * Polygon Prototype Functions + ******************************************************************* */ + + PlP.createConvexHull = function(points) { + function removeMiddle(a, b, c) { + var cross = (a.x - b.x) * (c.y - b.y) - (a.y - b.y) * (c.x - b.x); + var dot = (a.x - b.x) * (c.x - b.x) + (a.y - b.y) * (c.y - b.y); + return cross < 0 || cross == 0 && dot <= 0; + } + + points.sort(function (a, b) { + return a.x != b.x ? a.x - b.x : a.y - b.y; + }); + + var n = points.length; + var hull = []; + + for (var i = 0; i < 2 * n; i++) { + var j = i < n ? i : 2 * n - 1 - i; + while (hull.length >= 2 && removeMiddle(hull[hull.length - 2], hull[hull.length - 1], points[j])) + hull.pop(); + hull.push(points[j]); + } + + hull.pop(); + + this.addPoints(hull); + return this; + }; + + PlP.stepsFromRoot = function() { + var p = this.parent, steps = 0; + while (p) { + if (p.inner && p.inner.length > 1) steps++; + p = p.parent; + } + return steps; + }; + + PlP.first = function() { + return this.points[0]; + }; + + PlP.last = function() { + return this.points[this.length-1]; + }; + + PlP.swap = function(x,y) { + var poly = this, + points = poly.points, + length = points.length; + poly.bounds = new Bounds(); + for (var i=0; i 0) { + this.push(newPoint( + UTIL.round(Math.cos(angle * DEG2RAD) * radius, 7) + center.x, + UTIL.round(Math.sin(angle * DEG2RAD) * radius, 7) + center.y, + center.z + )); + angle += add; + } + return this; + }; + + /** + * offset all points + * @param {THREE.Vector3} offset + * @returns {Polygon} + */ + PlP.move = function(offset) { + var scope = this, + bounds = scope.bounds = new Bounds(); + scope.points.forEach(function(p) { + p.move(offset); + bounds.update(p); + }); + if (scope.inner) scope.inner.forEach(function(p) { p.move(offset) }); + return scope; + }; + + PlP.scale = function(scale, round) { + var scope = this, + bounds = scope.bounds = new Bounds(); + scope.points.forEach(function(p) { + p.x *= scale; + p.y *= scale; + p.z *= scale; + if (round) { + p.x = UTIL.round(p.x, round); + p.y = UTIL.round(p.y, round); + p.z = UTIL.round(p.z, round); + } + bounds.update(p); + }); + if (scope.inner) scope.inner.forEach(function(i) { i.scale(scale,round) }); + }; + + /** + * add fill angle hinting from longest segment + */ + PlP.hintFillAngle = function() { + var index = 0, + points = this.points, + length = points.length, + prev, + next, + dist2, + longest, + mincir = CONF.hint_min_circ, + minlen = CONF.hint_len_min, + maxlen = CONF.hint_len_max || Infinity; + + while (index < length) { + prev = points[index]; + next = points[++index % length]; + dist2 = prev.distToSq2D(next); + if (dist2 >= minlen && dist2 <= maxlen && (!longest || dist2 > longest.len)) { + longest = {p1:prev, p2:next, len:dist2}; + } + } + + if (longest && this.circularity() >= mincir) { + this.fillang = longest.p1.slopeTo(longest.p2).normal(); + // console.log([this.fillang,this.getZ(),this.id]); + } + + return this.fillang; + }; + + /** + * todo make more efficient + * + * @param {Boolean} deep + * @returns {Polygon} + */ + PlP.clone = function(deep) { + var np = newPolygon(), + ln = this.length, + i = 0; + + while (i < ln) np.push(this.points[i++]); + + np.fillang = this.fillang; + np.depth = this.depth; + np.open = this.open; + + if (deep && this.inner) { + np.inner = this.inner.clone(); + } + + return np; + }; + + /** + * set all points' z value + * + * @param {number} z + * @returns {Polygon} this + */ + PlP.setZ = function(z) { + var ar = this.points, + ln = ar.length, + i = 0; + while (i < ln) ar[i++].z = z; + if (this.inner) this.inner.forEach(function(c) {c.setZ(z)}); + return this; + }; + + /** + * @returns {number} z value of first point + */ + PlP.getZ = function() { + return this.points[0].z; + }; + + /** + * + * @param {Layer} layer + * @param {number} color + * @param {boolean} [recursive] + * @param {boolean} [open] + */ + PlP.render = function(layer, color, recursive, open) { + layer.poly(this, color, recursive, open); + }; + + /** + * add new point and return polygon reference for chaining + * + * @param {number} x + * @param {number} y + * @param {number} [z] + * @returns {Polygon} + */ + PlP.add = function(x,y,z) { + this.push(newPoint(x,y,z)); + return this; + }; + + /** + * append array of points to polygon and return polygon + * + * @param {Point[]} points + * @returns {Polygon} + */ + PlP.addPoints = function(points) { + var poly = this, + length = points.length, + i = 0; + while (i < length) { + poly.push(points[i++]); + } + return this; + }; + + /** + * append point to polygon and return point + * + * @param {Point} p + * @returns {Point} + */ + PlP.push = function(p) { + // clone any point belonging to another polygon + if (p.poly) p = p.clone(); + p.poly = this; + this.length++; + this.points.push(p); + this.bounds.update(p); + return p; + }; + + /** + * append point to polygon and return polygon + * + * @param {Point} p + * @returns {Polygon} + */ + PlP.append = function(p) { + this.push(p); + return this; + }; + + /** close polygon */ + PlP.setClosed = function() { + this.open = false; + return this; + }; + + /** open polygon */ + PlP.setOpen = function() { + this.open = true; + return this; + }; + + PlP.isOpen = function() { + return this.open; + }; + + PlP.isClosed = function() { + return !this.open; + }; + + PlP.setClockwise = function() { + if (!this.isClockwise()) this.reverse(); + return this; + }; + + PlP.setCounterClockwise = function() { + if (this.isClockwise()) this.reverse(); + return this; + }; + + PlP.isClockwise = function() { + return this.area(true) > 0; + }; + + PlP.showKey = function() { + return [this.first().key,this.last().key,this.length].join('~~'); + }; + + /** + * set this polygon's winding in alignment with the supplied polygon + * + * @param {Polygon} poly + * @param [boolean] toLongest + * @returns {Polygon} self + */ + PlP.alignWinding = function(poly, toLongest) { + if (toLongest && this.length > poly.length) { + poly.alignWinding(this, false); + } else if (this.isClockwise() !== poly.isClockwise()) { + this.reverse(); + } + }; + + /** + * set this polygon's winding in opposition to supplied polygon + * + * @param {Polygon} poly + * @param [boolean] toLongest + * @returns {Polygon} self + */ + PlP.opposeWinding = function(poly, toLongest) { + if (toLongest && this.length > poly.length) { + poly.opposeWinding(this, false); + } else if (this.isClockwise() === poly.isClockwise()) { + this.reverse(); + } + }; + + /** + * reverse direction of polygon points. + * @returns {Polygon} self + */ + PlP.reverse = function() { + this.area2 = -this.area2; + this.points = this.points.reverse(); + return this; + }; + + /** + * return true if this polygon is (likely) nested inside parent + * + * @param {Polygon} parent + * @returns {boolean} + */ + PlP.isNested = function(parent) { + if (parent.bounds.contains(this.bounds)) { + //var int = POLY().rayIntersect(this.bounds.leftMost, nest_test_slope, [parent], false); + //return int.length % 2 === 1 || this.isInside(parent, CONF.precision_nested_sq); + return this.isInside(parent, CONF.precision_nested_sq); + } + return false; + }; + + PlP.forEachPointEaseDown = function(fn, fromPoint) { + var index = this.findClosestPointTo(fromPoint).index, + fromZ = fromPoint.z, + offset = 0, + points = this.points, + length = points.length, + touch = -1, // first point to touch target z + targetZ = points[0].z, + dist2next, + last, + next, + done; + + while (true) { + next = points[index % length]; + if (last && next.z < fromZ) { + var deltaZ = fromZ - next.z; + dist2next = last.distTo2D(next); + if (dist2next > deltaZ * 2) { + // too long: synth intermediate + fn(last.followTo(next, deltaZ).setZ(next.z), offset++); + } else if (dist2next >= deltaZ) { + // ease down on this segment + } else { + // too short: clone n move z + next = next.clone().setZ(fromZ - dist2next/2); + } + fromZ = next.z; + } else if (offset === 0 && next.z < fromZ) { + next = next.clone().setZ(fromZ); + } + last = next; + fn(next, offset++); + if ((index % length) === touch) break; + if (touch < 0 && next.z <= targetZ) touch = (index % length); + index++; + } + + return last; + }; + + PlP.forEachPoint = function(fn, close, start) { + var index = start || 0, + points = this.points, + length = points.length, + count = close ? length + 1 : length, + offset = 0, + pos; + + while (count-- > 0) { + pos = index % length; + if (fn(points[pos], pos, points, offset++)) return; + index++; + } + }; + + PlP.forEachSegment = function(fn, open, start) { + var index = start || 0, + points = this.points, + length = points.length, + count = open ? length - 1 : length, + pos1, pos2; + + while (count-- > 0) { + pos1 = index % length; + pos2 = (index+1) % length; + if (fn(points[pos1], points[pos2], pos1, pos2)) return; + index++; + } + }; + + /** + * returns intersections sorted by closest to lp1 + */ + PlP.intersections = function(lp1, lp2) { + var list = []; + this.forEachSegment(function(pp1, pp2, ip1, ip2) { + var int = UTIL.intersect(lp1, lp2, pp1, pp2, BASE.key.SEGINT, false); + if (int) { + list.push(int); + pp1.pos = ip1; + pp2.pos = ip2; + } + }); + list.sort(function(p1, p2) { + return UTIL.distSq(lp1, p1) - UTIL.distSq(lp1, p2); + }); + return list; + }; + + /** + * emit new open poly between two intersection points + */ + PlP.emitSegment = function(i1, i2) { + var poly = newPolygon(), + start = i1.p2.pos, + end = i2.p1.pos, + delta = end - start; + while (delta < 0) { + delta += this.length; + } + poly.setOpen(); + poly.push(i1); + if (delta < this.length / 2) { + this.forEachPoint(function(p, pos) { + poly.push(p); + if (p === i2.p1) return true; + }, true, start); + } + poly.push(i2); + return poly; + }; + + /** + * @param {Polygon} poly + * @param {number} [tolerance] + * @returns {boolean} any points inside OR on edge + */ + PlP.hasPointsInside = function(poly, tolerance) { + if (!poly.overlaps(this)) return false; + + var mid, exit = false; + + this.forEachSegment(function(prev, next) { + // check midpoint on long lines + if (prev.distTo2D(next) > CONF.precision_midpoint_check_dist) { + mid = prev.midPointTo(next); + if (mid.inPolygon(poly) || mid.nearPolygon(poly, tolerance || CONF.precision_close_to_poly_sq)) { + return exit = true; + } + } + if (next.inPolygon(poly) || next.nearPolygon(poly, tolerance || CONF.precision_close_to_poly_sq)) { + return exit = true; + } + }); + + return exit; + }; + + /** + * TODO replace isNested() with isInside() ? + * + * @param {Polygon} poly + * @param {number} [tolerance] + * @returns {boolean} all points inside OR on edge + */ + PlP.isInside = function(poly, tolerance) { + // throw new Error("isInside"); + if (!( + // poly.overlaps(this) && + this.bounds.isNested(poly.bounds) + )) return false; + + var mid, + midcheck, + exit = true; + + this.forEachSegment(function(prev, next) { + // check midpoint on long lines + if (prev.distTo2D(next) > CONF.precision_midpoint_check_dist) { + mid = prev.midPointTo(next); + if (!(mid.inPolygon(poly) || mid.nearPolygon(poly, tolerance || CONF.precision_close_to_poly_sq))) { + exit = false; + return true; + } + } + if (!(next.inPolygon(poly) || next.nearPolygon(poly, tolerance || CONF.precision_close_to_poly_sq))) { + exit = false; + return true; + } + }); + + return exit; + }; + + /** + * @param {Polygon} poly + * @param {number} [tolerance] + * @returns {boolean} all points inside poly AND not inside children + */ + PlP.contains = function(poly, tolerance) { + return (poly && poly.isInside(this, tolerance) && poly.isOutsideAll(this.inner, tolerance)); + }; + + /** + * + * @param polys + * @returns {boolean} + */ + PlP.containedBySet = function(polys) { + if (!polys) return false; + for (var i=0; i 0; + }; + + /** + * remove all inner polygons + * @returns {Polygon} self + */ + PlP.clearInner = function() { + this.inner = null; + return this; + }; + + PlP.newUndeleted = function() { + var poly = newPolygon(); + this.forEachPoint(function(p) { + if (!p.del) poly.push(p); + }); + return poly; + }; + + /** + * http://www.ehow.com/how_5138742_calculate-circularity.html + * @returns {number} 0.0 - 1.0 from flat to perfectly circular + */ + PlP.circularity = function() { + return (4 * PI * this.area()) / UTIL.sqr(this.perimeter()); + }; + + PlP.circularityDeep = function() { + return (4 * PI * this.areaDeep()) / UTIL.sqr(this.perimeter()); + }; + + /** + * @returns {number} perimeter length (sum of all segment lengths) + */ + PlP.perimeter = function() { + var len = 0.0; + + this.forEachSegment(function(prev,next) { + len += SQRT(prev.distToSq2D(next)); + }, this.open); + + return len; + }; + + PlP.perimeterDeep = function() { + var len = this.perimeter(); + if (this.inner) this.inner.forEach(function(p) { len += p.perimeter() }); + return len; + }; + + /** + * calculate and return the area enclosed by the polygon. + * if raw is true, return a signed area equal to 2x the + * enclosed area which also indicates winding direction. + * + * @param {boolean} [raw] + * @returns {number} area + */ + PlP.area = function(raw) { + if (this.length < 3) return 0; + if (this.area2 === 0.0) { + for (var p=this.points,pl=p.length,pi=0,p1,p2; pi -- all rights reserved" +}; + +(function() { + + if (!self.base) self.base = {}; + if (self.base.polygons) return; + + var BASE = self.base, + UTIL = BASE.util, + CONF = BASE.config, + DBUG = BASE.debug, + DEG2RAD = Math.PI / 180, + ABS = Math.abs, + SQRT = Math.sqrt, + SQR = UTIL.sqr, + NOKEY = BASE.key.NONE, + newPoint = BASE.newPoint; + + BASE.polygons = { + trace2count : trace2count, + rayIntersect : rayIntersect, + alignWindings : alignWindings, + setWinding : setWinding, + fillArea : fillArea, + subtract : subtract, + flatten : flatten, + trimTo : trimTo, + expand : expand, + union : union, + nest : nest, + dump : dump, + diff : doDiff, + filter : filter, + toClipper : toClipper, + fromClipperNode : fromClipperNode, + fromClipperTree : fromClipperTree, + cleanClipperTree : cleanClipperTree + }; + + /** ****************************************************************** + * Polygon array utility functions + ******************************************************************* */ + + function toClipper(polys,debug) { + var out = []; + polys.forEach(function(poly) { poly.toClipper(out,debug) }); + return out; + } + + function fromClipperNode(tnode, z) { + var poly = BASE.newPolygon(); + tnode.m_polygon.forEach(function(p) { + poly.push(newPoint(null, null, z, null, p)); + }); + poly.open = tnode.IsOpen; + return poly; + }; + + function fromClipperTree(tnode, z, tops, parent) { + var polys = tops || [], + poly; + + tnode.m_Childs.forEach(function(child) { + poly = fromClipperNode(child, z); + // throw out all tiny polygons + if (poly.area() < 0.1) return; + if (parent) { + parent.addInner(poly); + } else { + polys.push(poly); + } + if (child.m_Childs) { + fromClipperTree(child, z, polys, parent ? null : poly); + } + }); + + return polys; + }; + + function cleanClipperTree(tree) { + var clib = self.ClipperLib, + clip = clib.Clipper; + + if (tree.m_Childs) tree.m_Childs.forEach(function(child) { + child.m_polygon = clip.CleanPolygon(child.m_polygon, CONF.clipperClean); + cleanClipperTree(child.m_Childs); + }); + + return tree; + }; + + function filter(array, output, fn) { + array.forEach(function(poly) { + poly = fn(poly); + if (poly) { + if (Array.isArray(poly)) { + output.appendAll(poly); + } else { + output.push(poly); + } + } + }); + return output; + } + + function dump(poly) { + if (Array.isArray(poly)) { + poly.forEach(function(p) { dump(p) }); + } else { + console.group({id:poly.id, area:poly.area(), depth:poly.depth, inner:(poly.inner ? poly.inner.length : null)}); + if (poly.inner) dump(poly.inner); + console.groupEnd(); + }; + } + + /** + * todo use clipper polytree? + * + * use bounding boxes and sliceIntersection + * to determine parent/child nesting. returns a + * array of trees. + * + * @param {Polygon[]} polygon soup + * @param {boolean} deep allow nesting beyond 2 levels + * @param {boolean} opentop prevent open polygons from having inners + * @returns {Polygon[]} top level parent polygons + */ + function nest(polygons, deep, opentop) { + if (!polygons) return polygons; + // sort groups by size + polygons.sort(function (a, b) { + return a.area() - b.area(); + }); + var i, poly; + // clear parent/child links if they exist + for (i = 0; i < polygons.length; i++) { + poly = polygons[i]; + poly.parent = null; + poly.inner = null; + } + // nest groups if fully contained by a parent + for (i = 0; i < polygons.length - 1; i++) { + poly = polygons[i]; + // find the smallest suitable parent + for (var j = i + 1; j < polygons.length; j++) { + var parent = polygons[j]; + // prevent open polys from having inners + if (opentop && parent.isOpen()) continue; + if (poly.isNested(parent)) { + parent.addInner(poly); + break; + } + } + } + // tops have an even # depth + var tops = [], + p; + // assign a depth level to each group + for (i = 0; i < polygons.length; i++) { + p = polygons[i]; + poly = p; + poly.depth = 0; + while (p.parent) { + poly.depth++; + p = p.parent; + } + if (deep) { + if (poly.depth === 0) tops.push(poly); + } else { + if (poly.depth % 2 === 0) { + tops.push(poly); + } else { + poly.inner = null; + } + } + } + return tops; + } + + /** + * sets windings for parents one way + * and children in opposition + * + * @param {Polygon[]} array + * @param {boolean} CW + * @param {boolean} [recurse] + */ + function setWinding(array, CW, recurse) { + if (!array) return; + var poly, i = 0; + while (i < array.length) { + poly = array[i++]; + if (poly.isClockwise() !== CW) poly.reverse(); + if (recurse && poly.inner) setWinding(poly.inner, !CW, false); + } + } + + /** + * ensure all polygons have the same winding direction. + * try to use reversals that touch the fewest nodes. + * + * @param {Polygon[]} polys + * @return {boolean} true if aligned clockwise + */ + function alignWindings(polys) { + var len = polys.length, + fwd = 0, + pts = 0, + i = 0, + setCW, + poly; + while (i < len) { + poly = polys[i++]; + pts += poly.length; + if (poly.isClockwise()) fwd += poly.length; + } + i = 0; + setCW = fwd > (pts/2); + while (i < len) { + poly = polys[i++]; + if (poly.isClockwise() != setCW) poly.reverse(); + } + return setCW; + } + + function setContains(setA, poly) { + for (var i=0; i= min && poly.circularity() > 0.01) { + to.push(poly); + out.push(poly); + } + }); + } + + // 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), outA); + } + } + + 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), outB); + } + } + + return out; + } + + /** + * Slice.doProjectedFills() + * Print.init w/ brims + * + * @param {Polygon[]} polys + * @param {number} [z] + * @returns {Polygon[]} + */ + function union(polys) { + if (polys.length < 2) return polys; + + var out = polys.slice(), i, j, union, uset = []; + + outer: for (i=0; i 1) && polys.length > 0) expand(polys, distance2 || distance, z, out, count > 0 ? count-1 : 0, distance2, collector); + + return polys; + } + + /** + * @param {Polygon} poly + * @param {Polygon[]} traces + * @param {number} offset + * @param {number} count + * @param {number} depth + * @param {Polygon[]} [last] + * @param {Polygon[]} [first] + */ + function trace2count(poly, traces, offset, count, depth, last, first) { + if (DBUG) DBUG.set('last', count === 1); + + if (count === 0) { + if (last) last.append(poly); + return; + } + + // offset polygon to outer traces array + var calcoff = depth === 0 && last ? offset / 2 : offset, + outer = poly.offset(calcoff, []), + inner = [], + j; + + // outer offset failed + if (outer.length === 0) { + if (last) last.append(poly); + return; + } + + // offset poly children to inner traces array + if (poly.inner) { + poly.inner.forEach(function(ic) { + ic.offset(-calcoff, inner); + }); + } + + var newouter = [], newinner = []; + subtract(outer, inner, newouter, newinner, poly.getZ()); + + if (newouter.length > 0) { + traces.appendAll(newouter); + if (depth === 0 && first) { + first.appendAll(newouter); + } + // recurse for multiple shells + if (count > 0) { + for (j=0; j 90) angle -= 180; + + // X,Y ray slope derived from angle + raySlope = BASE.newSlope(0,0, + UTIL.round(Math.cos(angle * DEG2RAD) * spacing, 7), + UTIL.round(Math.sin(angle * DEG2RAD) * spacing, 7) + ); + + // compute union of top boundaries + while (i < polys.length) bounds.merge(polys[i++].bounds); + + // ray stepping is an axis from the line perpendicular to the ray + var rayint = output || [], + stepX = -raySlope.dy, + stepY = raySlope.dx, + iterX = ABS(ABS(stepX) > 0 ? bounds.width() / stepX : 0), + iterY = ABS(ABS(stepY) > 0 ? bounds.height() / stepY : 0), + dist = SQRT(SQR(iterX * stepX) + SQR(iterY * stepY)), + step = SQRT(SQR(stepX) + SQR(stepY)), + steps = dist / step, + start = angle < 0 ? { x:bounds.minx, y:bounds.miny, z:zpos } : { x:bounds.maxx, y:bounds.miny, z:zpos }, + clib = self.ClipperLib, + ctyp = clib.ClipType, + ptyp = clib.PolyType, + cfil = clib.PolyFillType, + clip = new clib.Clipper(), + ctre = new clib.PolyTree(), + minlen = BASE.config.clipper * (minLen || 0.25), + lines = []; + + for (i = 0; i < steps; i++) { + var p1 = newPoint(start.x - raySlope.dx * 1000, start.y - raySlope.dy * 1000, zpos, NOKEY), + p2 = newPoint(start.x + raySlope.dx * 1000, start.y + raySlope.dy * 1000, zpos, NOKEY); + + lines.push([p1,p2]); + + start.x += stepX; + start.y += stepY; + } + + clip.AddPaths(lines, ptyp.ptSubject, false); + clip.AddPaths(toClipper(polys), ptyp.ptClip, true); + + if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftNonZero, cfil.pftEvenOdd)) { + ctre.m_AllPolys.forEach(function(poly) { + // filter out polygons under min length (0.5mm) + if (clib.JS.PerimeterOfPath(poly.m_polygon, false, 1) < minlen) return; + poly.m_polygon.forEach(function(point) { + rayint.push(newPoint(null,null,zpos,null,point)); + }); + }); + } + + return rayint; + } + + /** + * tracing a ray through a slice's polygons, find and return + * a sorted list of all intersecting points. + * + * @param {Point} start + * @param {Slope} slope + * @param {Polygon[]} polygons + * @param {boolean} [for_fill] + * @returns {Point[]} + */ + function rayIntersect(start, slope, polygons, for_fill) { + var i = 0, + flat = [], + points = [], + conf = BASE.config, + merge_dist = for_fill ? conf.precision_fill_merge : conf.precision_merge; + // todo use new flatten() function above + polygons.forEach(function(p) { + p.flattenTo(flat); + }); + polygons = flat; + while (i < polygons.length) { + var polygon = polygons[i++], + pp = polygon.points, + pl = pp.length, + dbug = BASE.debug, + debug = false; + for (var j = 0; j < pl; j++) { + var j2 = (j + 1) % pl, + ip = UTIL.intersectRayLine(start, slope, pp[j], pp[j2]); + if (ip) { + // add group object to point for cull detection + ip.group = polygon; + // add point to point list + points.push(ip); + // if point is near a group endpoint, add position marker for culling + if (ip.isNear(pp[j], merge_dist)) { + ip.pos = j; + ip.mod = pl; + if (debug) dbug.points([ip], 0x0000ff, 0.5, 1.0); + } else if (ip.isNear(pp[j2], merge_dist)) { + ip.pos = j2; + ip.mod = pl; + if (debug) dbug.points([ip], 0x00ffff, 0.5, 0.85); + } else { + if (debug) dbug.points([ip], 0xff00ff, 0.5, 0.5); + } + } + } + } + if (points.length > 0) { + var del = false; + // sort on distance from ray origin + points.sort(function (p1, p2) { + // handle passing through line-common end points + if (!(p1.del || p2.del) && p1.isNear(p2, merge_dist)) { + var line = []; + if (!p1.isNear(p1.p1, merge_dist)) line.push(p1.p1); + if (!p1.isNear(p1.p2, merge_dist)) line.push(p1.p2); + if (!p2.isNear(p2.p1, merge_dist)) line.push(p2.p1); + if (!p2.isNear(p2.p2, merge_dist)) line.push(p2.p2); + /** + * when true, points are coincident on collinear lines but + * not passing through endpoints on each. kill them. this case + * was added later. see below for what else can happen. + */ + if (line.length < 2) { + dbug.log("sliceInt: line common ep fail: "+line.length); + } else + if (line.length > 2) { + p1.del = true; + p2.del = true; + } else + /** + * when a ray intersects two equal points, they are either inside or outside. + * to determine which, we create a line from the two points connected to them + * and test intersect the ray with that line. if it intersects, the points are + * inside and we keep one of them. otherwise, they are outside and we drop both. + */ + if (!UTIL.intersectRayLine(start, slope, line[0], line[1])) { + del = true; + p1.del = true; + p2.del = true; + if (debug) dbug.points([p1, p2], 0xffffff, 0.2, 1); + } else { + del = true; + p1.del = true; + if (debug) dbug.points([p1], 0xffff00, 0.2, 0.85); + } + } + return p1.dist - p2.dist; // sort on 'a' dist from ray origin + }); + /** + * cull invalid lines between groups on same/different levels depending + * ok = same level (even), same group + * ok = same level (odd), diff group + * ok = diff level (even-odd) + */ + if (for_fill) { + var p1, p2; + i = 0; + pl = points.length; + while (i < pl) { + p1 = points[i++]; + while (p1 && p1.del && i < pl) p1 = points[i++]; + p2 = points[i++]; + while (p2 && p2.del && i < pl) p2 = points[i++]; + if (p1 && p2 && p1.group && p1.group) { + var p1g = p1.group, + p2g = p2.group, + even = (p1g.depth % 2 === 0), // point is on an even depth group + same = (p1g === p2g); // points intersect same group + if (p1g.depth === p2g.depth) { + // TODO this works sometimes and not others + //if ((even && !same) || (same && !even)) { + // p1.del = true; + // p2.del = true; + // del = true; + // if (debug) dbug.points([p1, p2], 0xfff000, 0.2, 2); + //} + // check cull co-linear with group edge + if (same && p1.mod && p2.mod) { + var diff = ABS(p1.pos - p2.pos); + if (diff === 1 || diff === p1.mod - 1) { + p1.del = true; + p2.del = true; + del = true; + if (debug) dbug.points([p1, p2], 0xffffff, 0.2, 1.85); + } + } + } + } + } + } + // handle deletions, if found + if (del) { + var np = []; + for (i = 0; i < points.length; i++) { + var p = points[i]; + if (!p.del) { + np.push(p); + } + } + points = np; + } + } + return points; + } + +})(); diff --git a/js/geo-render.js b/js/geo-render.js new file mode 100644 index 00000000..74e4a3e4 --- /dev/null +++ b/js/geo-render.js @@ -0,0 +1,99 @@ +"use strict"; + +var gs_base_render = { + copyright:"stewart allen -- all rights reserved" +}; + +(function() { + + if (!self.base) self.base = {}; + if (self.base.render) return; + + var BASE = self.base, + line_width = 1, + materialCache = {}; + + BASE.render = { + wireframe : wireframe + }; + + /** ****************************************************************** + * Render Functions + ******************************************************************* */ + + /** + * render triangle array as line segments. use of {@link newOrderedLine} + * allows lines to be cached by normalizing key order. + * + * @param {THREE.Group} group + * @param {Point[]} points + * @param {number} color + * @returns {THREE.Line} + */ + function wireframe(group, points, color) { + if (points.length % 3 != 0) throw "invalid line : "+points.length; + var lines = new THREE.Geometry(), + hash = {}, + added = 0; + for (var i = 0; i < points.length; i += 3) { + var p1 = points[i], + p2 = points[i + 1], + p3 = points[i + 2], + l1 = newOrderedLine(p1, p2), + l2 = newOrderedLine(p2, p3), + l3 = newOrderedLine(p3, p1); + if (!hash[l1.key]) { + lines.vertices.push(new THREE.Vector3(p1.x, p1.y, p1.z)); + lines.vertices.push(new THREE.Vector3(p2.x, p2.y, p2.z)); + hash[l1.key] = ++added; + } + if (!hash[l2.key]) { + lines.vertices.push(new THREE.Vector3(p2.x, p2.y, p2.z)); + lines.vertices.push(new THREE.Vector3(p3.x, p3.y, p3.z)); + hash[l2.key] = ++added; + } + if (!hash[l3.key]) { + lines.vertices.push(new THREE.Vector3(p3.x, p3.y, p3.z)); + lines.vertices.push(new THREE.Vector3(p1.x, p1.y, p1.z)); + hash[l3.key] = ++added; + } + } + lines.verticesNeedUpdate = true; + var mesh = new THREE.LineSegments(lines, getMaterial(color)); + group.add(mesh); + return mesh; + } + + /** ****************************************************************** + * Connect to base and Helpers + ******************************************************************* */ + + /** + * @param {number} color + * @returns {THREE.LineBasicMaterial} + */ + function getMaterial(color) { + var material = materialCache[color]; + if (!material) { + material = new THREE.LineBasicMaterial({ + fog:false, + color: color, + linewidth: line_width + }); + materialCache[color] = material; + } + return material; + } + + /** + * required for line caching in {@link base.render.wireframe} + * + * @param {Point} p1 + * @param {Point} p2 + * @returns {Line} + */ + function newOrderedLine(p1, p2) { + return p1.key < p2.key ? BASE.newLine(p1, p2) : BASE.newLine(p2, p1); + } + +})(); diff --git a/js/geo-slope.js b/js/geo-slope.js new file mode 100644 index 00000000..ef82fbfb --- /dev/null +++ b/js/geo-slope.js @@ -0,0 +1,134 @@ +"use strict"; + +var gs_base_slope = { + copyright:"stewart allen -- all rights reserved" +}; + +(function() { + + if (!self.base) self.base = {}; + if (self.base.Slope) return; + + /** + * + * @param p1 + * @param p2 + * @param dx + * @param dy + * @constructor + */ + function Slope(p1, p2, dx, dy) { + this.dx = p1 && p2 ? p2.x - p1.x : dx; + this.dy = p1 && p2 ? p2.y - p1.y : dy; + this.angle = Math.atan2(this.dy, this.dx) * RAD2DEG; + } + + var BASE = self.base, + CONF = BASE.config, + ABS = Math.abs, + SlP = Slope.prototype, + DEG2RAD = Math.PI / 180, + RAD2DEG = 180 / Math.PI; + + BASE.Slope = Slope; + BASE.newSlope = newSlope; + + BASE.newSlopeFromAngle = function(angle) { + return newSlope(0,0, + Math.cos(angle * DEG2RAD), + Math.sin(angle * DEG2RAD) + ); + }; + + /** ****************************************************************** + * Slope Prototype Functions + ******************************************************************* */ + + SlP.toString = function() { + return [this.dx, this.dy, this.angle].join(','); + }; + + /** + * @param {Slope} s + * @returns {boolean} + */ + SlP.isSame = function(s) { + // if very close to vertical or horizontal, they're the same + if (ABS(this.dx) <= CONF.precision_merge && ABS(s.dx) <= CONF.precision_merge) return true; + if (ABS(this.dy) <= CONF.precision_merge && ABS(s.dy) <= CONF.precision_merge) return true; + // check angle within a range + var prec = Math.min(1/Math.sqrt(this.dx * this.dx + this.dy * this.dy), CONF.precision_slope_merge); + return angleWithinDelta(this.angle, s.angle, prec || CONF.precision_slope); + }; + + /** + * turn slope 90 degrees + * + * @returns {Slope} + */ + SlP.normal = function() { + var t = this.dx; + this.dx = -this.dy; + this.dy = t; + this.angle = Math.atan2(this.dy, this.dx) * RAD2DEG; + return this; + }; + + SlP.toUnit = function() { + var max = Math.max(ABS(this.dx), ABS(this.dy)); + this.dx = this.dx / max; + this.dy = this.dy / max; + return this; + }; + + SlP.factor = function(f) { + this.dx *= f; + this.dy *= f; + return this; + }; + + /** + * reverse (180 degree) slope + * + * @returns {Slope} + */ + SlP.invert = function() { + this.dx = -this.dx; + this.dy = -this.dy; + this.angle = 360 - this.angle;//Math.atan2(this.dy, this.dx) * RAD2DEG; + return this; + }; + + /** ****************************************************************** + * Connect to base and Helpers + ******************************************************************* */ + + /** + * returns true if the difference between a & b is less than v + * + * @param {number} a + * @param {number} b + * @param {number} v + * @returns {boolean} + */ + function minDeltaABS(a,b,v) { + return ABS(a-b) < v; + } + + function angleWithinDelta(a1, a2, delta) { + return (ABS(a1-a2) <= delta || 360-ABS(a1-a2) <= delta); + } + + /** + * + * @param p1 + * @param p2 + * @param dx + * @param dy + * @returns {Slope} + */ + function newSlope(p1, p2, dx, dy) { + return new Slope(p1, p2, dx, dy); + } + +})(); diff --git a/js/geo.js b/js/geo.js new file mode 100644 index 00000000..81b88c2f --- /dev/null +++ b/js/geo.js @@ -0,0 +1,394 @@ +"use strict"; + +var gs_base = { + copyright:"stewart allen -- all rights reserved" +}; + +(function() { + + if (!self.base) self.base = {}; + if (self.base.util) return; + + var BASE = self.base, + ABS = Math.abs, + round_decimal_precision = 8; + + /** ****************************************************************** + * Utility Functions + ******************************************************************* */ + + function time() { return new Date().getTime() } + + /** + * call function with all combinations of a1, a2 + * and passing in the supplied arg object. + * + * @param {Array} a1 + * @param {Array} a2 + * @param {Object} arg + * @param {Function} fn + * @returns {Object} + */ + function doCombinations(a1, a2, arg, fn) { + var i, j; + for (i = 0; i < a1.length; i++) { + for (j = (a1 === a2 ? i + 1 : 0); j < a2.length; j++) { + fn(a1[i], a2[j], arg); + } + } + return arg; + } + + /** + * @param {Point} p1 + * @param {Point} p2 + * @param {Point} p3 + * @returns {boolean} + */ + function isClockwise(p1, p2, p3) { + return area2(p1, p2, p3) > 0; + } + + /** + * @param {Point} p1 + * @param {Point} p2 + * @param {Point} p3 + * @returns {boolean} + */ + function isCounterClockwise(p1, p2, p3) { + return area2(p1, p2, p3) < 0; + } + + /** + * @param {Point} p1 + * @param {Point} p2 + * @param {Point} p3 + * @returns {boolean} + */ + function isCollinear(p1, p2, p3) { + return inCloseRange(area2(p1, p2, p3), -0.00001, 0.00001); + } + + function pac(p1, p2) { + return (p2.x - p1.x) * (p2.y + p1.y); + } + + /** + * returns 2x area for a triangle with sign indicating handedness + * + * @param {Point} p1 + * @param {Point} p2 + * @param {Point} p3 + * @returns {number} negative for CCW progression, positive for CW progression + */ + function area2(p1, p2, p3) { + return pac(p1,p2) + pac(p2,p3) + pac(p3,p1); + } + + /** + * + * @param v1 + * @param v2 + * @param [dist] + * @returns {boolean} + */ + function isCloseTo(v1,v2,dist) { + return ABS(v1-v2) <= (dist || BASE.config.precision_merge); + } + + /** + * + * @param val + * @param min + * @param max + * @returns {boolean} + */ + function inCloseRange(val, min, max) { + return (isCloseTo(val,min) || val >= min) && (isCloseTo(val,max) || val <= max); + } + + /** + * return square of value + * @param v + * @returns {number} + */ + function sqr(v) { return v * v } + + /** + * return distance squared between two points + * @param p1 + * @param p2 + * @returns {number} + */ + function dist2(p1,p2) { return sqr(p2.x - p1.x) + sqr(p2.y - p1.y) } + + /** + * return distance squared between two points + * enables faster Point.nearPolygon() + * + * @param {number} x1 + * @param {number} y1 + * @param {number} x2 + * @param {number} y2 + * @returns {number} + */ + function dist2v2(x1,y1,x2,y2) { return sqr(x2 - x1) + sqr(y2 - y1) } + + /** + * + * @param offset + * @param precision + * @returns {number} + */ + function offsetPrecision(offset, precision) { + return ABS(offset) - precision; + } + + /** + * + * @param value + * @param min + * @param max + * @returns {boolean} + */ + function inRange(value, min, max) { + var val = parseFloat(value); + return val >= min && val <= max; + } + + /** + * + * @param v + * @param zeros + * @returns {number} + */ + function round(v, zeros) { + var pow = Math.pow(10,zeros || round_decimal_precision); + return Math.round(v * pow) / pow; + } + + /** + * used by {@link Polygon.trace} and {@link Polygon.intersect} + * + * @param {Point} p1 + * @param {Point} p2 + * @param {Point} p3 + * @param {Point} p4 + * @param {String} [test] + * @param {boolean} [parallelok] + * @returns {?Point | String} + */ + function intersect(p1, p2, p3, p4, test, parallelok) { + var keys = BASE.key, + p1x = p1.x, + p1y = p1.y, + p2x = p2.x, + p2y = p2.y, + p3x = p3.x, + p3y = p3.y, + p4x = p4.x, + p4y = p4.y, + d1x = (p2x - p1x), // ad.x + d1y = (p2y - p1y), // ad.y + d2x = (p4x - p3x), // bd.x + d2y = (p4y - p3y), // bd.y + d = (d2y * d1x) - (d2x * d1y); // det + + //if (ABS(d) < 0.0000000001) { + if (ABS(d) < 0.0001) { + // lines are parallel or collinear + return test && !parallelok ? null : keys.PARALLEL; + } + + var a = p1y - p3y, // origin dy + b = p1x - p3x, // origin dx + n1 = (d2x * a) - (d2y * b), + n2 = (d1x * a) - (d1y * b); + + a = n1 / d; // roughly distance from l1 origin to l2 intersection + b = n2 / d; // roughly distance from l2 origin to l1 intersection + + var ia = a >= -0.0001 && a <= 1.0001, + ib = b >= -0.0001 && b <= 1.0001, + segint = (ia && ib), + rayint = (a >= 0 && b >= 0); + + if (test === keys.SEGINT && !segint) return null; + if (test === keys.RAYINT && !rayint) return null; + + var ip = BASE.newPoint( + p1x + (a * d1x), // x + p1y + (a * d1y), // y + p3.z || p4.z, // z + segint ? keys.SEGINT : rayint ? keys.RAYINT : keys.PROJECT + ); + + ip.dist = a; + ip.p1 = p3; + ip.p2 = p4; + + return ip; + } + + /** + * used by {@link rayIntersect} and {@link Polygon.trace} + * + * @param {Point} ro + * @param {Slope} s1 + * @param {Point} p1 + * @param {Point} p2 + * @param {boolean} [infinite] + * @returns {?Point} + */ + function intersectRayLine(ro, s1, p1, p2, infinite) { + var keys = BASE.key, + p1x = ro.x, + p1y = ro.y, + s1x = s1.dx, + s1y = s1.dy, + p3x = p1.x, + p3y = p1.y, + p4x = p2.x, + p4y = p2.y, + s2x = p4x - p3x, + s2y = p4y - p3y, + d = (s2y * s1x) - (s2x * s1y); + + var a = p1y - p3y, + b = p1x - p3x, + n1 = (s2x * a) - (s2y * b), + n2 = (s1x * a) - (s1y * b); + + if (ABS(d) < 0.000000000001) { + // lines are parallel or collinear + return null; + } + + a = n1 / d; + b = n2 / d; + + if (infinite || (inCloseRange(b,0,1) && a >= 0)) { + var ip = BASE.newPoint( + p1x + (a * s1x), + p1y + (a * s1y), + p2.z || ro.z, + keys.NONE + ); + ip.dist = a; + ip.p1 = p1; + ip.p2 = p2; + return ip; + } + return null; + } + + /** + * @param {Point} p1 + * @param {Point} p2 + * @param {Point} p3 + * @param {Point} p4 + * @returns {number} + */ + function determinant(p1, p2, p3, p4) { + var d1x = (p2.x - p1.x), + d1y = (p2.y - p1.y), + d2x = (p4.x - p3.x), + d2y = (p4.y - p3.y); + + return (d2y * d1x) - (d2x * d1y); + } + + /** ****************************************************************** + * Connect to base + ******************************************************************* */ + + BASE.key = { + NONE : "", + PROJECT : "project", + SEGINT : "segint", + RAYINT : "rayint", + PARALLEL : "parallel" + }; + + BASE.config = { + // heal disjoint polygons in slicing (experimental) + bridgeLineGapDistance : 0, + // Bounds default margin nearTo + // Polygon.offset mindist2 offset precision + precision_offset : 0.05, + // Polygon.isEquivalent area() isCloseTo + precision_poly_area : 0.05, + // Polygon.isEquivalent bounds() equals value + precision_poly_bounds: 0.01, + // Polygon.isEquivalent point distance to other poly line + precision_poly_merge: 0.05, + // Polygon.traceIntersects mindist2 + // Polygon.overlaps (bounds overlaps test precision) + // Polygon.isEquivalent circularity (is circle if 1-this < merge) + // Slope.isSame (vert/horiz w/in this value) + // isCloseTo() default for dist + // sliceIntersects point merge dist for non-fill + precision_merge : 0.0001, + precision_slice_z : 0.0001, + // Point.isInPolygon nearPolygon value + // Point.isInPolygonNotNear nearPolygon value + // Point.isMergable2D distToSq2D value + // Point.isMergable3D distToSq2D value + // Polygon.isInside nearPolygon value + // Polygon.isOutside nearPolygon value + precision_merge_sq : sqr(0.0001), + // Bound.isNested inflation value for potential parent + precision_bounds : 0.0001, + // Slope.isSame default precision + precision_slope : 0.02, + // Slope.isSame use to calculate precision + precision_slope_merge : 0.25, + // sliceIntersect point merge distance for fill + precision_fill_merge : 0.001, + // convertPoints point merge distance + // other values break cube-s9 (wtf) + precision_decimate : 0.05, + // decimate test over this many points + decimate_threshold : 100000, + // Point.onLine precision distance (endpoints in Polygon.intersect) + precision_point_on_line : 0.01, + // Polygon.isEquivalent value for determining similar enough to test + precision_circularity : 0.001, + // polygon fill hinting (settings override) + hint_len_min : sqr(3), + hint_len_max : sqr(20), + hint_min_circ : 0.15, + // tolerances to determine if a point is near a masking polygon + precision_mask_tolerance : 0.001, + // Polygon isInside,isOutside tolerance (accounts for midpoint skew) + precision_close_to_poly_sq : sqr(0.001), + // how long a segment has to be to trigger a midpoint check (inner/outer) + precision_midpoint_check_dist : 1, + precision_nested_sq : sqr(0.01), + // clipper multiplier + clipper : 100000, + // clipper poly clean + clipperClean : 1000 + }; + + BASE.util = { + sqr : sqr, + time : time, + round : round, + area2: area2, + distSq : dist2, + distSqv2 : dist2v2, + inRange : inRange, + isCloseTo : isCloseTo, + inCloseRange : inCloseRange, + isCollinear : isCollinear, + isClockwise : isClockwise, + isCounterClockwise : isCounterClockwise, + doCombinations : doCombinations, + offsetPrecision : offsetPrecision, + intersectRayLine : intersectRayLine, + intersect : intersect, + determinant : determinant + }; + +})(); diff --git a/js/kiri-codec.js b/js/kiri-codec.js new file mode 100644 index 00000000..738996ac --- /dev/null +++ b/js/kiri-codec.js @@ -0,0 +1,223 @@ +"use strict"; + +var gs_kiri_codec = { + copyright:"stewart allen -- all rights reserved" +}; + +(function() { + + if (!self.kiri) self.kiri = {}; + if (self.kiri.codec) return; + + var base = self.base, + kiri = self.kiri, + handlers = {}; + + kiri.codec = { + encode: encode, + decode: decode, + registerDecoder: registerDecoder + }; + + function encode(o, state) { + state = state || {}; + if (o === null) return null; + if (o === undefined) return undefined; + if (Array.isArray(o)) { + var arr = new Array(o.length), i=0; + while (i < o.length) arr[i] = encode(o[i++], state); + return arr; + } + switch (typeof(o)) { + case 'string': + case 'number': + return o; + case 'object': + if (o.encode) return o.encode(state); + return genOEncode(o, state); + } + return null; + } + + function decode(o, state) { + state = state || {}; + if (o === null) return null; + if (o === undefined) return undefined; + if (Array.isArray(o)) { + for (var i=0; i < o.length; i++) o[i] = decode(o[i],state); + return o; + } + switch (typeof(o)) { + case 'string': + case 'number': + return o; + case 'object': + if (o.type && handlers[o.type]) return handlers[o.type](o,state); + return genODecode(o, state); + } + return null; + } + + function registerDecoder(type, handler) { + handlers[type] = handler; + } + + function genOEncode(o, state) { + if (o instanceof Float32Array) return o; + var out = {}; + for (var k in o) { + if (o.hasOwnProperty(k)) out[k] = encode(o[k], state); + } + return out; + } + + function genODecode(o, state) { + if (o instanceof Float32Array) return o; + var out = {}; + for (var k in o) { + if (o.hasOwnProperty(k)) out[k] = decode(o[k], state); + } + return out; + } + + /** ****************************************************************** + * Object CODEC Functions + ******************************************************************* */ + + kiri.Slice.prototype.encode = function(state) { + return { + type: 'slice', + z: this.z, + index: this.index, + camMode: this.camMode, + tops: encode(this.tops, state), + bridges: encode(this.bridges, state), + flats: encode(this.flats, state), + solids: encode(this.solids, state), + supports: encode(this.supports, state) + //groups: encode(this.groups, state) + }; + }; + + registerDecoder('slice', function(v, state) { + var slice = kiri.newSlice(v.z, state.mesh ? state.mesh.newGroup() : null); + + slice.index = v.index; + slice.camMode = v.camMode; + slice.tops = decode(v.tops, state); + slice.bridges = decode(v.bridges, state); + slice.flats = decode(v.flats, state); + slice.solids = decode(v.solids, state); + slice.supports = decode(v.supports, state); + //slice.groups = decode(v.groups, state); + + return slice; + }); + + kiri.Top.prototype.encode = function(state) { + return { + type: 'top', + poly: encode(this.poly, state), + traces: encode(this.traces, state), + inner: encode(this.inner, state), + solids: encode(this.solids, state), + fill_lines: encodePointArray(this.fill_lines), + fill_sparse: encode(this.fill_sparse, state) + }; + }; + + registerDecoder('top', function(v, state) { + var top = kiri.newTop(decode(v.poly, state)); + + top.traces = decode(v.traces, state); + top.inner = decode(v.inner, state); + top.solids = decode(v.solids, state); + top.fill_lines = decodePointArray(v.fill_lines,state); + top.fill_sparse = decode(v.fill_sparse, state); + + return top; + }); + + function encodePointArray(points) { + if (!points) return null; + + var array = new Float32Array(points.length * 3), + pos = 0; + + points.forEach(function(point) { + array[pos++] = point.x; + array[pos++] = point.y; + array[pos++] = point.z; + }); + + return array; + } + + function decodePointArray(array) { + if (!array) return null; + + var vid = 0, + pid = 0, + points = new Array(array.length/3); + + while (vid < array.length) { + points[pid++] = base.newPoint(array[vid++], array[vid++], array[vid++]); + } + + return points; + } + + base.Polygon.prototype.encode = function(state) { + if (!state.poly) state.poly = {}; + + var cached = state.poly[this.id]; + + if (cached) { + return { + type: 'poly', + ref: this.id + }; + } + + state.poly[this.id] = this; + + return { + type: 'poly', + id: this.id, + array: encodePointArray(this.points), + open: this.isOpen(), + inner: encode(this.inner, state), + parent: encode(this.parent, state), + fills: encodePointArray(this.fills), + depth: this.depth + }; + }; + + registerDecoder('poly', function(v, state) { + if (!state.poly) state.poly = {}; + + if (v.ref) return state.poly[v.ref]; + + var poly = base.newPolygon(), + vid = 0; + + while (vid < v.array.length) { + poly.push(base.newPoint(v.array[vid++], v.array[vid++], v.array[vid++])); + } + + poly.id = v.id; + poly.open = v.open; + // if (v.open) poly.setOpen(); else poly.setClosed(); + + state.poly[v.id] = poly; + + poly.inner = decode(v.inner, state); + poly.parent = decode(v.parent, state); + poly.fills = decodePointArray(v.fills); + poly.depth = v.depth; + + return poly; + }); + + +})(); diff --git a/js/kiri-db.js b/js/kiri-db.js new file mode 100644 index 00000000..de433fbe --- /dev/null +++ b/js/kiri-db.js @@ -0,0 +1,164 @@ +"use strict"; + +(function() { + + if (!self.kiri) self.kiri = { }; + + /** + * @constructor + */ + function Catalog(motodb, decimate) { + var store = this; + store.db = motodb; + store.files = {}; + store.listeners = []; + store.autodec = decimate; + store.deferredHandler = null; + store.refresh(); + } + + var kiri = self.kiri, + DP = Catalog.prototype; + + kiri.openCatalog = function(motodb,decimate) { + return new Catalog(motodb,decimate); + }; + + DP.refresh = function() { + var store = this; + store.db.get('files', function(files) { + if (files) { + store.files = files; + notifyFileListeners(store); + } + }); + }; + + DP.wipe = function() { + var key, files = this.files; + for (key in files) { + if (files.hasOwnProperty(key)) this.deleteFile(key); + } + }; + + DP.fileList = function() { + return this.files; + }; + + DP.addFileListener = function(listener) { + if (!this.listeners.contains(listener)) { + this.listeners.push(listener); + listener(this.files); + } + }; + + DP.removeFileListener = function(listener) { + this.listeners.remove(listener); + }; + + function saveFileList(store) { + store.db.put('files', store.files); + notifyFileListeners(store); + } + + function notifyFileListeners(store) { + for (var i=0; i 0.001; + + // console.log({id:tool.id, diam:flute_diameter, pix:flute_pix_float, tocks:profile_pix_iter}); + + // for each pixel in tool profile, check inside radius + for (var x = 0; x < profile_pix_iter; x++) { + for (var y = 0; y < profile_pix_iter; y++) { + var dx = x - toolCenter, + dy = y - toolCenter, + dist_from_center = Math.sqrt(dx * dx + dy * dy); + + if (dist_from_center <= flute_radius_pix_float) { + var z_offset = ball ? (1 - Math.cos((dist_from_center / flute_radius_pix_float) * HPI)) * -flute_radius : 0; + toolOffset.push([dx, dy, z_offset]); + } else if (larger_shaft && dist_from_center <= shaft_radius_pix_float) { + toolOffset.push([dx, dy, -shaft_offset]); + } + } + } + + return toolOffset; + }; + + /** + * find highest z on a line segment + * x,y are in platform coodinates + */ + function getTopoZPathMax(widget, profile, x1, y1, x2, y2) { + + var topo = widget.topo, + rez = topo.resolution, + bounds = widget.getBoundingBox(), + dx = x2-x1, + dy = y2-y1, + md = Math.max(Math.abs(dx),Math.abs(dy)), + mi = md / rez, + ix = dx / mi, + iy = dy / mi, + zmax = 0; + + while (mi-- > 0) { + var tx1 = Math.round((x1 - bounds.min.x) / rez), + ty1 = Math.round((y1 - bounds.min.y) / rez); + zmax = Math.max(zmax, getMaxTopoToolZ(topo, profile, tx1, ty1, true)); + x1 += ix; + y1 += iy; + } + + return zmax; + }; + + /** + * x,y are in topo grid int coordinates + */ + function getMaxTopoToolZ(topo, profile, x, y, floormax) { + var tv, tx, ty, tz, gv, + i = 0, mz = -1, + sx = topo.stepsx, sy = topo.stepsy, + data = topo.data; + + while (i < profile.length) { + tv = profile[i++]; + tx = tv[0] + x; + ty = tv[1] + y; + if (tx < 0 || tx >= sx || ty < 0 || ty >= sy) continue; + gv = data[tx * sy + ty]; + if (gv === undefined) { + continue; + } + if (floormax && gv === 0) { + return topo.bounds.max.z; + } + tz = tv[2] + gv; + mz = Math.max(tz, mz); + } + + return mz >= 0.0 ? mz : topo.bounds.max.z; + }; + + /** + * call out to slicer + */ + function doSlicing(widget, options, ondone, onupdate) { + SLICER.sliceWidget(widget, options, ondone, onupdate); + } + + /** + * top down progressive union for CAM + */ + function pancake(slices, onupdate) { + var union, tops, last; + + slices.forEach(function(slice,index) { + tops = slice.gatherTopPolys([]).clone(true); + if (!union) { + union = tops; + } else { + tops.appendAll(union); + union = POLY.union(tops); + slice.tops = []; + union.forEach(function(poly) { + slice.addTop(poly); + poly.setZ(slice.z); + }); + } + last = slice; + if (onupdate) onupdate(index/slices.length); + }); + + return last.clone(false); + } + + /** + * @param {Slice[]} slices + * @param {number} z position + * + * return slice closest to specified z + */ + function closestSliceToZ(slices, z) { + var selected = null, + distance = Infinity, + nextdist; + slices.forEach(function(slice) { + nextdist = Math.abs(slice.z - z); + if (nextdist < distance) { + selected = slice; + distance = nextdist; + } + }); + // DBUG.log({z:selected.z, dist:distance, off:selected.z - z}); + return selected; + } + + /** + * select from pancaked layers + */ + function selectSlices(slices, step, mode, output) { + var last, zlastout, emitted = []; + + function emit(slice) { + // prevent double emit at end + if (last === slice) return; + last = slice; + if (slice.camMode) { + // clone to prevent double emit + var nuslice = newSlice(slice.z); + slice.tops.forEach(function(top) { + nuslice.addTop(top.poly.clone(true)); + }); + slice = nuslice; + } + slice.camMode = mode; + zlastout = slice.z; + emitted.push(slice); + } + + // - find mandatory slices + // - divide space between by step + // - select closes spaces for divisible gap + var forced = []; + slices.forEach(function(slice) { + if (slice.hasFlats) forced.push(slice); + }) + // DBUG.log({forced:forced}); + + var mid = []; + forced.forEachPair(function(s1, s2) { + // skip last to first pair + if (s2.z > s1.z) return; + var delta = Math.abs(s2.z - s1.z), + inc = delta / step, + nstep = step, + dec = inc - Math.floor(inc), + slop = step * 0.02; // allow 2% over/under on step alignment + // skip if delta close to step + if (Math.abs(delta - step) < slop) return; + // add another step if decimal too high + if (dec > slop) nstep = delta / Math.ceil(inc); + // DBUG.log(['f',delta,step,nstep,delta / nstep]); + // find closest slices in-between + for (var zv = s1.z - nstep; zv >= s2.z + nstep/2; zv -= nstep) { + mid.push(closestSliceToZ(slices, zv)); + } + }, 1); + // DBUG.log({mid:mid}); + + forced.appendAll(mid); + forced.sort(function(s1, s2) { return s2.z - s1.z; }); + // drop first/top slice (because it's not an actual cut) + forced = forced.slice(1); + forced.forEach(function(slice) { + emit(slice); + // DBUG.log(slice.z); + }); + + // add to output array + emitted.forEach(function(slice) { + output.push(slice); + }); + } + + /** + * merge collinear points + * remove floor lead-in, lead-out + * remove collinear points (todo) + */ + function cleanupTopoSlice(slice, diameter, curvesOnly) { + var poly = slice.tops[0].traces[0], + nupoly = newPolygon().setOpen(), + points = poly.points, + start = 0, + end = points.length - 1, + latent, point, last, i; + + // find start + for (i = start; i < points.length; i++) + if (points[i].z > 0) { start = i; break; } + // find end + for (i = end; i > 0; i--) + if (points[i].z > 0) { end = i; break; } + // merge collinear + for (i = start; i <= end; i++) { + point = points[i]; + if (last) { + if (last.z === point.z && (last.x === point.x || last.y === point.y)) { + latent = point; + continue + } else { + if (latent) { + nupoly.push(latent); + latent = null; + } + } + } + nupoly.push(point); + last = point; + } + if (latent) nupoly.push(latent); + + if (nupoly.length < 2) return false; + + // limit cleanup to curved features + var traces = [], + trace = newPolygon().setOpen(); + + slice.tops[0].traces = traces; + points = nupoly.points; + last = points[0]; + latent = null; + for (i = 1; i < points.length; i++) { + point = points[i]; + var use = curvesOnly ? + (last.z != point.z && (last.x !== point.x || last.y !== point.y)) : + last.z && point.z; + if (use) { + // join to previous trace if not too far away + if (trace.length === 0 && traces.length > 0 && point.distTo3D(traces.peek().last()) <= diameter) { + trace = traces.peek(); + } + if (latent !== last) trace.push(last); + trace.push(point); + latent = point; + } else { + if (trace.length > 1) { + // don't re-push a joined trace + if (traces.length === 0 || traces.peek() !== trace) traces.push(trace); + trace = newPolygon().setOpen(); + latent = null; + } else if (trace.length === 1) { + trace = newPolygon().setOpen(); + } + } + last = point; + } + + if (trace.length > 1) traces.push(trace); + + return traces.length > 0; + } + + /** + * @param {Widget} widget + * @param {Object} settings + * @param {Function} ondone + * @param {Function} onupdate + */ + function generateTopoMap(widget, settings, ondone, onupdate) { + var mesh = widget.mesh, + proc = settings.process, + outp = settings.process, + resolution = outp.camTolerance, + diameter = getToolDiameter(settings, proc.finishingTool), + tool = getToolById(settings, proc.finishingTool), + toolStep = diameter * proc.finishingOver, + traceJoin = diameter / 2, + bounds = widget.getBoundingBox().clone(), + boundsX = bounds.max.x - bounds.min.x, + boundsY = bounds.max.y - bounds.min.y, + maxangle = proc.finishingAngle, + curvesOnly = proc.finishCurvesOnly, + R2A = 180 / Math.PI, + stepsx = Math.ceil(boundsX / resolution), + stepsy = Math.ceil(boundsY / resolution), + data = new Float32Array(stepsx * stepsy), + topo = widget.topo = { + data: data, + stepsx: stepsx, + stepsy: stepsy, + bounds: bounds, + diameter: diameter, + resolution: resolution + }, + toolOffset = createToolProfile(settings, proc.finishingTool, topo), + newslices = [], + newlines, + newtop, + newtrace, + slice, lx, ly, lv, + startTime = time(); + + // return highest z within tools radius + function maxzat(x,y) { + return getMaxTopoToolZ(topo, toolOffset, x, y); + } + + function topoSlicesDone(slices) { + var gridx = 0, + gridy, + gridi, // index + gridv, // value + miny = bounds.min.y, + maxy = bounds.max.y, + maxx = bounds.max.x, + zMin = MAX(bounds.min.z, outp.camZBottom) + 0.0001, + x, y, tv, ltv; + + // for each Y slice, find z grid value (x/z swapped) + for (var j=0; j zMin || p2.z > zMin) && // one endpoint above 0 + (p1.z > gridv || p2.z > gridv) && // one endpoint above gridv + ((p1.y <= y && p2.y >= y) || // one endpoint left + (p2.y <= y && p1.y >= y)) // one endpoint right + ) { + var dy = p1.y - p2.y, + dz = p1.z - p2.z, + pct = (p1.y - y) / dy, + nz = p1.z - (dz * pct); + if (nz > gridv) gridv = lodata[gridi] = Math.max(nz, zMin); + } + } + gridy++; + } + gridx++; + onupdate(0.20 + (gridx/stepsx) * 0.50, "topo tracing"); + } + + // do x linear finishing + if (proc.finishingXOn) { + startTime = time(); + + // emit slice per X + gridx = 0; + for (x = bounds.min.x; x <= bounds.max.x; x += toolStep) { + // for (x = bounds.min.x; x <= bounds.max.x; x += resolution) { + ly = gridy = 0; + slice = newSlice(gridx, mesh.newGroup ? mesh.newGroup() : null); + slice.camMode = CPRO.FINISH_X; + slice.lines = newlines = []; + newtop = slice.addTop(newPolygon().setOpen()).poly; + newtrace = newPolygon().setOpen(); + slice.tops[0].traces = [ newtrace ]; + for (y = bounds.min.y; y < bounds.max.y; y += resolution) { + gridv = data[gridx * stepsy + gridy]; + tv = maxzat(gridx, gridy); + if (ly) { + if (mesh) newlines.push(newLine( + newPoint(x,ly,lv), + newPoint(x,y,gridv) + )); + var ang = Math.abs((Math.atan2(ltv - tv, resolution) * R2A) % 90); + // over max angle, turn into square edge (up or down) + if (ang > maxangle) { + if (ltv > tv) { + // down = forward,down + newtrace.push(newPoint(x,y,ltv)); + } else { + // up = up,forward + newtrace.push(newPoint(x,ly,tv)); + } + } + } + if (DEBUG) newtop.push(newPoint(x,y,gridv)); + newtrace.push(newPoint(x,y,tv)); + ly = y; + lv = gridv; + ltv = tv; + gridy++; + } + if (cleanupTopoSlice(slice,diameter,curvesOnly)) newslices.push(slice); + gridx = Math.round(((x - bounds.min.x + toolStep) / boundsX) * stepsx); + onupdate(0.70 + (gridx/stepsx) * 0.15, "linear x"); + } + } + + // do y linear finishing + if (proc.finishingYOn) { + startTime = time(); + + // emit slice per Y + gridy = 0; + for (y = bounds.min.y; y < bounds.max.y; y += toolStep) { + lx = gridx = 0; + slice = newSlice(gridy, mesh.newGroup ? mesh.newGroup() : null); + slice.camMode = CPRO.FINISH_Y; + slice.lines = newlines = []; + newtop = slice.addTop(newPolygon().setOpen()).poly; + newtrace = newPolygon().setOpen(); + slice.tops[0].traces = [ newtrace ]; + for (x = bounds.min.x; x <= bounds.max.x; x += resolution) { + gridv = data[gridx * stepsy + gridy]; + tv = maxzat(gridx, gridy); + if (lx) { + if (mesh) newlines.push(newLine( + newPoint(lx,y,lv), + newPoint(x,y,gridv) + )); + var ang = Math.abs((Math.atan2(ltv - tv, resolution) * R2A) % 90); + // over max angle, turn into square edge (up or down) + if (ang > maxangle) { + if (ltv > tv) { + // down = forward,down + newtrace.push(newPoint(x,y,ltv)); + } else { + // up = up,forward + newtrace.push(newPoint(lx,y,tv)); + } + } + } + if (DEBUG) newtop.push(newPoint(x,y,gridv)); + newtrace.push(newPoint(x,y,tv)); + lx = x; + lv = gridv; + ltv = tv; + gridx++; + } + if (cleanupTopoSlice(slice,diameter,curvesOnly)) newslices.push(slice); + gridy = Math.round(((y - bounds.min.y + toolStep) / boundsY) * stepsy); + onupdate(0.85 + (gridy/stepsy) * 0.15, "linear y"); + } + } + + ondone(newslices); + } + + // slices progress left-to-right along the X axis + doSlicing(widget, {height:resolution, swapX:true, topo:true}, topoSlicesDone, function(update) { + onupdate(0.0 + update * 0.20, "topo slicing"); + }); + } + + /** + * Create facing passes in CAM mode + * + * @param {Slice} slice target + * @param {Polygon[]} shell enclosing slice tops + * @param {number} diameter of tool + * @param {number} overlap % on each pass + * @param {boolean} true for pocket only mode + * @param {number} bounds shell offset + * @returns {Object} shell + */ + function createFacingSlices(slice, shell, diameter, overlap, pocket) { + var outer = [], + offset = []; + + // clone and flatten the shell with tops to offset array + shell.clone(true).forEach(function(poly) { poly.setZ(slice.z).flattenTo(offset) }); + + // re-nest offset array + offset = POLY.nest(offset); + + // inset offset array by 1/2 diameter then by tool overlap % + POLY.expand(offset, - (diameter / 2), slice.z, outer, 0, -diameter * overlap); + + if (!pocket) { + // re-flatten offset polys + offset = POLY.flatten(outer.slice(), []); + // re-clone shell to offset polys (because it was lost in the offset) + shell.clone(true).forEach(function(poly) { poly.setZ(slice.z).flattenTo(offset) }); + // re-nest offset polys + outer = POLY.nest(offset); + } + + if (!slice.tops.length) { console.log({no_top: slice.z}); slice.addTop() } + + slice.tops[0].traces = outer; + }; + + /** + * Create roughing offsets in CAM mode + * + * @param {Slice} slice target + * @param {Polygon[]} shell enclosing slice tops + * @param {number} diameter of tool (mm) + * @param {number} stock to leave (mm) + * @param {number} percent overlap on each pass + * @param {boolean} true for pocket only mode + * @returns {Object} shell or newly generated shell + */ + function createRoughingSlices(slice, shell, diameter, stock, overlap, pocket) { + var tops = slice.gatherTopPolys([]).clone(true), + outer = [], + offset = []; + + // clone and flatten the shell with tops to offset array + shell.clone(true).forEach(function(poly) { poly.setZ(slice.z).flattenTo(offset) }); + POLY.flatten(tops, offset, true); + + // only tab cut polys should be open + offset.forEach(function(trace) { + trace.setClosed(); + }); + + // re-nest offset array + offset = POLY.nest(offset); + + // inset offset array by 1/2 diameter then by tool overlap % + POLY.expand(offset, - (diameter / 2 + stock), slice.z, outer, 0, -diameter * overlap); + + if (!pocket) { + // re-flatten offset polys + offset = POLY.flatten(outer.slice(), [], true); + // re-clone shell to offset polys (because it was lost in the offset) + shell.clone(true).forEach(function(poly) { + poly.setZ(slice.z).flattenTo(offset); + poly.setClosed(); + }); + // re-nest offset polys + // outer = POLY.nest(offset); + outer = offset; + } + + slice.tops[0].traces = outer; + }; + + /** + * Create CAM finishing offsets + * + * @param {Slice} slice target + * @param {Polygon[]} outermost pancacked shells for fill + * @param {number} tool diameter + * @param {boolean} pocket only + */ + function createFinishingSlices(slice, shell, diameter, pocket) { + if (slice.tops.length === 0) return shell; + + var tops = slice.gatherTopPolys([]).clone(true), + offset = POLY.expand(tops, diameter / 2, slice.z); + + // when pocket only, drop first outer poly + // if it matches the shell and promote inner polys + if (pocket) { + offset = POLY.filter(POLY.diff(shell, offset, slice.z), [], function(poly) { + if (poly.area() < 1) return null; + for (var sp=0; sp zMin + tabHeight) return; + // no tops / traces + if (slice.tops.length === 0) return; + + var trace, index, maxArea = 0, tmpArea; + + // find trace with greatest area + slice.tops[0].traces.forEach(function(trc, idx) { + if ((tmpArea = trc.area()) > maxArea) { + maxArea = tmpArea; + index = idx; + trace = trc; + } + }); + + // for tracing out intersections + trace.setClockwise(); + + const outside = 10000, + width = (tabWidth + toolDiam) / 2, + // horizontal top cut + htl = { x: -outside, y: tabWidth / 2 }, + htr = { x: -htl.x, y: htl.y }, + // horizontal bottom cut + hbl = { x: htl.x, y: -htl.y }, + hbr = { x: -htl.x, y: -htl.y }, + // vertical left cut + vtl = { x: -width, y: outside }, + vbl = { x: vtl.x, y: -vtl.y }, + // vertical right cut + vtr = { x: -vtl.x, y: vtl.y }, + vbr = { x: -vtl.x, y: -vtl.y }, + nutraces = []; + + var lrtop = trace.intersections(htl, htr), + lrbot = trace.intersections(hbl, hbr), + tblt = trace.intersections(vtl, vbl), + tbrt = trace.intersections(vtr, vbr), + tr1 = trace.emitSegment(lrtop[0], tblt[0]), + tr2 = trace.emitSegment(tbrt[0], lrtop[lrtop.length-1]), + tr3 = trace.emitSegment(lrbot[lrbot.length-1], tbrt[tbrt.length-1]), + tr4 = trace.emitSegment(tblt[tblt.length-1], lrbot[0]); + + // remove cut trace and replace with open polys + slice.tops[0].traces.splice(index, 1, tr1, tr2, tr3, tr4); + } + + // called when horizontal slicing complete + const camSlicesDone = function(slices) { + + const camShell = pancake(slices, function(update) { + onupdate(0.25 + update * 0.15, "shelling"); + }); + + const camShellPolys = shellRough = camShell.gatherTopPolys([]); + + if (procRough && !pocketOnly) { + // expand shell by half tool diameter + stock to leave + shellRough = POLY.expand(shellRough, (roughToolDiam / 2) + proc.roughingStock, 0); + } + + if (procFinish && pocketOnly) { + shellFinish = POLY.expand(camShellPolys, -finishToolDiam/2, 0); + } + + // hollow area from top of stock to top of part + if (procFacing) { + var ztop = bounds.max.z, + zpos = ztop + outp.camZTopOffset, + zstep = proc.roughingDown; + + while (zpos >= ztop) { + zpos = zpos - MIN(zstep, zpos - ztop); + + const slice = newSlice(zpos, mesh.newGroup ? mesh.newGroup() : null); + slice.camMode = CPRO.FACING; + sliceAll.append(slice); + + shellRough.clone().forEach(function(poly) { + slice.addTop(poly); + }) + + if (Math.abs(zpos - ztop) < 0.001) break; + } + } + + if (procRough) { + var selected = []; + selectSlices(slices, proc.roughingDown, CPRO.ROUGH, selected); + sliceAll.appendAll(selected); + } + + if (procFinish) { + var selected = []; + selectSlices(slices, proc.finishingDown, CPRO.FINISH, selected); + sliceAll.appendAll(selected); + } + + if (procDrill) { + var drills = [], + centerDiff = drillToolDiam * 0.1, + area = (drillToolDiam/2) * (drillToolDiam/2) * Math.PI, + areaDelta = area * 0.05; + + slices.forEach(function(slice) { + var inner = slice.gatherTopPolyInners([]); + inner.forEach(function(poly) { + if (poly.circularity() >= 0.995 && Math.abs(poly.area() - area) <= areaDelta) { + var center = poly.circleCenter(), + merged = false, + closest = Infinity, + dist; + // TODO reject if inside camShellPolys (means there is material above) + // if (center.isInPolygon(camShellPolys)) return; + drills.forEach(function(drill) { + if (merged) return; + if ((dist = drill.last().distTo2D(center)) <= centerDiff) { + merged = true; + drill.push(center); + } + closest = Math.min(closest,dist); + }); + if (!merged) { + drills.push(newPolygon().append(center)); + } + } + }); + }); + + // force all drill poly points to use center (average) point + drills.forEach(function(drill) { + var center = drill.center(true), + slice = newSlice(0,null); + drill.points.forEach(function(point) { + point.x = center.x; + point.y = center.y; + }); + slice.camMode = CPRO.DRILL; + slice.addTop(null).traces = [ drill ]; + sliceAll.append(slice); + }); + } + } + + // horizontal slices for rough/finish + doSlicing(widget, {height: sliceDepth, cam:true, zmin:outp.camZBottom}, camSlicesDone, function(update) { + onupdate(0.0 + update * 0.25, "slicing"); + }); + + // for each final slice, do post-processing + sliceAll.forEach(function(slice, index) { + // re-index + slice.index = index; + switch (slice.camMode) { + case CPRO.FACING: + createFacingSlices(slice, shellRough, roughToolDiam, proc.roughingOver, pocketOnly); + break; + case CPRO.ROUGH: + createRoughingSlices(slice, shellRough, roughToolDiam, proc.roughingStock, proc.roughingOver, pocketOnly); + if (addTabs) addCutoutTabs(slice, roughToolDiam); + break; + case CPRO.FINISH: + createFinishingSlices(slice, shellFinish, finishToolDiam, pocketOnly); + if (addTabs) addCutoutTabs(slice, finishToolDiam); + break; + } + onupdate(0.40 + (index / sliceAll.length) * 0.10, "finishing") + }, "cam post"); + + // we need topo for safe travel moves + generateTopoMap(widget, settings, function(slices) { + sliceAll.appendAll(slices); + // todo union rough / finish shells + // todo union rough / finish tabs + // todo append to generated topo map + }, function(update, msg) { + onupdate(0.50 + update * 0.50, msg || "create topo"); + }); + + ondone(); + }; + + /** + * DRIVER PRINT CONTRACT + * + * @param {Object} print state object + * @param {Function} update incremental callback + * @param {Number} [index] into widget array + * @param {Object} [firstPoint] starting point + */ + function printSetup(print, update, index, firstPoint) { + var getTool = getToolById, + settings = print.settings, + device = settings.device, + process = settings.process, + widgetIndex = index || 0, + widgetArray = print.widgets, + widgetCount = widgetArray.length, + widget = widgetArray[widgetIndex]; + + if (widgetIndex >= widgetCount || !widget) return; + + var slices = widget.slices, + bounds = widget.getCamBounds(settings), + hasStock = process.camStockZ && process.camStockX && process.camStockY, + startCenter = process.outputOriginCenter, + zclear = process.camZClearance || 1, + zadd = hasStock ? process.camStockZ - bounds.max.z : 0, + zmax = hasStock ? process.camStockZ + zclear : bounds.max.z + zclear, + originx = startCenter ? 0 : hasStock ? -process.camStockX / 2 : bounds.min.x, + originy = startCenter ? 0 : hasStock ? -process.camStockY / 2 : bounds.min.y, + origin = hasStock ? newPoint(originx, originy, process.camStockZ) : newPoint(originx, originy, bounds.max.z + zclear), + output = print.output, + modes = CPRO, + depthFirst = process.camDepthFirst, + easeDown = false && process.camEaseDown, + tolerance = process.camTolerance, + drillDown = process.drillDown, + drillLift = process.drillLift, + drillDwell = process.drillDwell, + newOutput = widgetIndex === 0 ? [] : print.output, + layerOut = [], + printPoint, + isNewMode, + tool, + toolDiam, + toolDiamMove, + toolProfile, + feedRate, + plungeRate, + lastTool, + lastMode, + lastPoint, + nextIsMove = true, + spindle = 0, + spindleMax = device.spindleMax, + newOut = print.newOut, + tip2tipEmit = print.tip2tipEmit, + poly2polyEmit = print.poly2polyEmit, + poly2polyDepthFirstEmit = print.poly2polyDepthFirstEmit; + + function newLayer() { + if (layerOut.length < 2) return; + newOutput.push(layerOut); + layerOut = []; + } + + /** + * @param {Point} point + * @param {number} emit (0=move, !0=filament emit/laser on/cut mode) + * @param {number} [speed] speed + * @param {number} [tool] tool + */ + function layerPush(point, emit, speed, tool) { + layerOut.push(newOut(point, emit, speed, tool)); + } + + function setTool(toolID, feed, plunge) { + if (toolID !== lastTool) { + tool = getToolById(settings, toolID); + toolDiam = getToolDiameter(settings, toolID); + toolDiamMove = toolDiam, // TODO validate w/ multiple models + toolProfile = createToolProfile(settings, toolID, widget.topo); + lastTool = toolID; + } + feedRate = feed; + plungeRate = plunge; + } + + function emitDrills(polys) { + polys = polys.slice(); + for (;;) { + var closestDist = Infinity, + closestI, + closest = null, + dist; + + for (var i=0; i down * 2) { + points.push(point.clone()); + point.z -= down; + remain -= down; + } else if (remain < down) { + points.push(point.clone()); + point.z -= remain; + points.push(point.clone()); + break; + } else { + points.push(point.clone()); + point.z -= remain / 2; + points.push(point.clone()); + point.z -= remain / 2; + points.push(point.clone()); + break; + } + } + points.forEach(function(point, index) { + camOut(point, 1); + if (index < points.length - 1) { + if (dwell) camDwell(dwell); + if (lift) camOut(point.clone().setZ(point.z + lift), 0); + } + }) + camOut(point.clone().setZ(zmax)); + newLayer(); + } + + function camDwell(time) { + layerPush( + null, + 0, + time, + tool.number + ); + } + + function camOut(point, cut) { + point = point.clone(); + point.x += widget.mesh.position.x; + point.y += widget.mesh.position.y; + point.z += zadd; + if (nextIsMove) { + cut = 0; + nextIsMove = false; + } + var rate = feedRate; + // only when we have a previous point to compare to + if (lastPoint) { + var deltaXY = lastPoint.distTo2D(point), + deltaZ = point.z - lastPoint.z, + absDeltaZ = Math.abs(deltaZ), + isMove = !cut; + // drop points too close together + if (deltaXY < 0.001 && point.z === lastPoint.z) { + // console.trace(["drop dup",lastPoint,point]); + return; + } + if (isMove && deltaXY <= toolDiamMove) { + // convert short planar moves to cuts + if (absDeltaZ <= tolerance) { + cut = 1; + isMove = false; + } else if (deltaZ <= -tolerance) { + // move over before descending + layerPush(point.clone().setZ(lastPoint.z), 0, 0, tool.number); + // new pos for plunge calc + deltaXY = 0; + } + } //else (TODO verify no else here b/c above could change isMove) + // move over things + if ((deltaXY > toolDiam || (deltaZ > toolDiam && deltaXY > tolerance)) && (isMove || absDeltaZ >= tolerance)) { + var maxz = MAX( + getTopoZPathMax( + widget, + toolProfile, + lastPoint.x, + lastPoint.y, + point.x, + point.y) + zadd, + point.z, + lastPoint.z), + mustGoUp = MAX(maxz - point.z, maxz - lastPoint.z) >= tolerance, + clearz = maxz; + // up if any point between higher than start/finish + if (mustGoUp) { + clearz = maxz + zclear; + layerPush(lastPoint.clone().setZ(clearz), 0, 0, tool.number); + } + // over to point above where we descend to + if (mustGoUp || point.z < maxz) { + layerPush(point.clone().setZ(clearz), 0, 0, tool.number); + // new pos for plunge calc + deltaXY = 0; + } + } + // synth new plunge rate + if (deltaZ <= -tolerance) { + var threshold = MIN(deltaXY / 2, absDeltaZ), + modifier = threshold / absDeltaZ; + if (threshold && modifier && deltaXY > tolerance) { + // use modifier to speed up long XY move plunge rates + rate = Math.round(plungeRate + ((feedRate - plungeRate) * modifier)); + } else { + rate = plungeRate; + } + // console.log({deltaZ: deltaZ, deltaXY: deltaXY, threshold:threshold, modifier:modifier, rate:rate, plungeRate:plungeRate}); + } + } else { + // before first point, move cutting head to point above it + layerPush(point.clone().setZ(bounds.max.z + zclear), 0, 0, tool.number); + } + // todo synthesize move speed from feed / plunge accordingly + layerPush( + point, + cut ? 1 : 0, + rate, + tool.number + ); + lastPoint = point; + layerOut.spindle = spindle; + } + + // make top start offset configurable + printPoint = firstPoint || origin; + + // accumulated data for depth-first optimiztions + var depthData = { + rough: [], + finish: [], + roughDiam: 0, + finishDiam: 0, + linearx: [], + lineary: [], + layer: 0, + drill: [] + }; + + // todo first move into positon + slices.forEach(function(slice, sliceIndex) { + depthData.layer++; + isNewMode = slice.camMode != lastMode; + lastMode = slice.camMode; + nextIsMove = true; + if (isNewMode) depthData.layer = 0; + + switch (slice.camMode) { + case modes.FACING: + setTool(process.roughingTool, process.roughingSpeed, 0); + spindle = Math.min(spindleMax, process.roughingSpindle); + slice.tops.forEach(function(top) { + if (!top.traces) return; + var polys = []; + top.traces.forEach(function (poly) { + polys.push(poly); + if (poly.inner) { + poly.inner.forEach(function(inner) { + polys.push(inner); + }) + } + }); + // set winding specified in output + POLY.setWinding(polys, process.outputClockwise, true); + printPoint = poly2polyEmit(polys, printPoint, function(poly, index, count) { + poly.forEachPoint(function(point, pidx, points, offset) { + camOut(point.clone(), offset !== 0); + }, true, index); + }); + newLayer(); + }); + break; + case modes.ROUGH: + case modes.FINISH: + if (slice.camMode === modes.ROUGH) { + setTool(process.roughingTool, process.roughingSpeed, process.roughingPlunge); + spindle = Math.min(spindleMax, process.roughingSpindle); + depthData.roughDiam = toolDiam; + } else { + setTool(process.finishingTool, process.finishingSpeed, process.finishingPlunge); + spindle = Math.min(spindleMax, process.finishingSpindle); + depthData.finishDiam = toolDiam; + } + // todo find closest next trace/trace-point + slice.tops.forEach(function(top) { + if (!top.poly) return; + if (!top.traces) return; + var polys = []; + top.traces.forEach(function (poly) { + if (depthFirst) poly = poly.clone(true); + poly.layer = depthData.layer; + polys.push(poly); + }); + // set winding specified in output + POLY.setWinding(polys, process.outputClockwise, true); + if (depthFirst) { + (slice.camMode === modes.ROUGH ? depthData.rough : depthData.finish).append(polys); + } else { + printPoint = poly2polyEmit(polys, printPoint, function(poly, index, count) { + poly.forEachPoint(function(point, pidx, points, offset) { + camOut(point.clone(), offset !== 0); + }, poly.isClosed(), index); + }); + newLayer(); + } + }); + break; + case modes.FINISH_X: + case modes.FINISH_Y: + setTool(process.finishingTool, process.finishingSpeed, process.finishingPlunge); + spindle = Math.min(spindleMax, process.finishingSpindle); + depthData.finishDiam = toolDiam; + // todo find closest next trace/trace-point + slice.tops.forEach(function(top) { + if (!top.traces) return; + var polys = [], poly, emit; + top.traces.forEach(function (poly) { + if (depthFirst) poly = poly.clone(true); + polys.push({first:poly.first(), last:poly.last(), poly:poly}); + }); + if (depthFirst) { + (slice.camMode === modes.FINISH_X ? depthData.linearx : depthData.lineary).appendAll(polys); + } else { + printPoint = tip2tipEmit(polys, printPoint, function(el, point, count) { + poly = el.poly; + if (poly.last() === point) poly.reverse(); + poly.forEachPoint(function(point, pidx) { + camOut(point.clone(), pidx > 0); + }, false); + }); + newLayer(); + } + }); + break; + case modes.DRILL: + setTool(process.drillTool, process.drillDownSpeed, process.drillDownSpeed); + // drilling is always depth-first + slice.tops.forEach(function(top) { + if (!top.traces) return; + depthData.drill.appendAll(top.traces); + }); + break; + } + update(sliceIndex / slices.length); + }); + + // act on accumulated layer data + if (depthFirst) { + // roughing depth first + if (depthData.rough.length > 0) { + setTool(process.roughingTool, process.roughingSpeed, process.roughingPlunge); + spindle = Math.min(spindleMax, process.roughingSpindle); + printPoint = poly2polyDepthFirstEmit(depthData.rough, printPoint, function(poly, index, count, fromPoint) { + var last = null; + if (easeDown && poly.isClosed()) { + last = poly.forEachPointEaseDown(function(point, offset) { + camOut(point.clone(), offset > 0); + }, fromPoint); + } else { + poly.forEachPoint(function(point, pidx, points, offset) { + camOut(point.clone(), offset !== 0); + }, poly.isClosed(), index); + } + newLayer(); + return last; + }, depthData.roughDiam * process.roughingOver * 1.01); + } + // finishing depth first + if (depthData.finish.length > 0) { + setTool(process.finishingTool, process.finishingSpeed, process.finishingPlunge); + spindle = Math.min(spindleMax, process.finishingSpindle); + printPoint = poly2polyDepthFirstEmit(depthData.finish, printPoint, function(poly, index, count, fromPoint) { + var last = null; + if (easeDown && poly.isClosed()) { + last = poly.forEachPointEaseDown(function(point, offset) { + camOut(point.clone(), offset > 0); + }, fromPoint); + } else { + poly.forEachPoint(function(point, pidx, points, offset) { + camOut(point.clone(), offset !== 0); + }, poly.isClosed(), index); + } + newLayer(); + return last; + }, depthData.finishDiam * 0.01); + } + // two modes for deferred finishing: x then y or combined + if (process.finishCurvesOnly) { + setTool(process.finishingTool, process.finishingSpeed, process.finishingPlunge); + spindle = Math.min(spindleMax, process.finishingSpindle); + // combined deferred linear x and y finishing + var linearxy = [].appendAll(depthData.linearx).appendAll(depthData.lineary); + printPoint = tip2tipEmit(linearxy, printPoint, function(el, point, count) { + var poly = el.poly; + if (poly.last() === point) poly.reverse(); + poly.forEachPoint(function(point, pidx) { + camOut(point.clone(), pidx > 0); + }, false); + newLayer(); + }); + } else { + setTool(process.finishingTool, process.finishingSpeed, process.finishingPlunge); + spindle = Math.min(spindleMax, process.finishingSpindle); + // deferred linear x finishing + if (depthData.linearx.length > 0) + printPoint = tip2tipEmit(depthData.linearx, printPoint, function(el, point, count) { + var poly = el.poly; + if (poly.last() === point) poly.reverse(); + poly.forEachPoint(function(point, pidx) { + camOut(point.clone(), pidx > 0); + }, false); + newLayer(); + }); + // deferred linear y finishing + if (depthData.lineary.length > 0) + printPoint = tip2tipEmit(depthData.lineary, printPoint, function(el, point, count) { + var poly = el.poly; + if (poly.last() === point) poly.reverse(); + poly.forEachPoint(function(point, pidx) { + camOut(point.clone(), pidx > 0); + }, false); + newLayer(); + }); + } + } + + // drilling is always depth first + if (depthData.drill.length > 0) { + setTool(process.drillTool, process.drillDownSpeed, process.drillDownSpeed); + emitDrills(depthData.drill); + } + + // last layer/move is to zmax + camOut(printPoint.clone().setZ(bounds.max.z + zclear), false); + newOutput.push(layerOut); + + // replace output single flattened layer with all points + print.output = newOutput; + + if (widgetIndex + 1 < widgetCount) printSetup(print, update, widgetIndex + 1, printPoint); + }; + + /** + * @returns {Array} gcode lines + */ + function printExport(print, online) { + var i, + time = 0, + lines = 0, + bytes = 0, + output = [], + spindle = 0, + modes = CPRO, + settings = print.settings, + device = settings.device, + gcodes = settings.device || {}, + space = gcodes.gcodeSpace, + stripComments = gcodes.gcodeStrip, + cmdToolChange = gcodes.gcodeChange || [ "M6 T{tool}" ], + cmdDwell = gcodes.gcodeDwell || [ "G4 P{time}" ], + widget = print.widgets[0], + bounds = widget.getCamBounds(settings), + bed = settings.device, + spro = settings.process, + sout = settings.process, + decimals = 4, + pos = { x:null, y:null, z:null, f:null, t:null }, + line, + cidx, + point, + points = 0, + hasStock = spro.camStockZ && spro.camStockX && spro.camStockY, + zmax = hasStock ? spro.camStockZ : bounds.max.z, + runbox = { + max: { x:-Infinity, y:-Infinity, z:-Infinity}, + min: { x:Infinity, y:Infinity, z:Infinity} + }, + offset = sout.outputOriginCenter ? null : { + x: bounds.max.x, //bed.bedWidth/2, + y: bounds.max.y //bed.bedDepth/2 + }, + consts = { + tool: 0, + tool_name: "unknown", + top: offset ? bed.bedDepth : bed.bedDepth/2, + left: offset ? 0 : -bed.bedWidth/2, + right: offset ? bed.bedWidth : bed.bedWidth/2, + bottom: offset ? 0 : -bed.bedDepth/2, + time: 0 + }, + append; + + if (online) { + append = function(line) { + if (line) { + lines++; + bytes += line.length; + output.append(line); + } + if (!line || output.length > 1000) { + online(output.join("\n")); + output = []; + } + }; + } else { + append = function(line) { + if (!line) return; + output.append(line); + lines++; + bytes += line.length; + } + } + + function filterEmit(array, consts) { + if (!array) return; + for (i=0; i= 0) { + line = line.substring(0, cidx).trim(); + if (line.length === 0) continue; + } + append(line); + } + } + + function add0(val) { + var s = val.toString(), + d = s.indexOf("."); + if (d < 0) { + return s + '.0'; + } else { + return s; + } + } + + function toolNameByNumber(number, tools) { + for (var i=0; i 0 ? "M3" : "M4") + " S" + Math.abs(spindle)); + } + layerout.forEach(function(out) { + moveTo(out); + }); + }); + + filterEmit(gcodes.gcodePost, consts); + + // force emit of buffer + append(); + + print.time = time; + print.lines = lines; + print.bytes = bytes + lines - 1; + print.bounds = runbox; + + return online ? null : output.join("\n"); + }; + +})(); diff --git a/js/kiri-driver-fdm.js b/js/kiri-driver-fdm.js new file mode 100644 index 00000000..2877025e --- /dev/null +++ b/js/kiri-driver-fdm.js @@ -0,0 +1,462 @@ +/** Copyright 2014-2017 Stewart Allen -- All Rights Reserved */ + +"use strict"; + +var gs_kiri_fdm = exports; + +(function() { + + if (!self.kiri) self.kiri = { }; + if (!self.kiri.driver) self.kiri.driver = { }; + if (self.kiri.driver.FDM) return; + + var KIRI = self.kiri, + BASE = self.base, + UTIL = BASE.util, + CONF = BASE.config, + FDM = KIRI.driver.FDM = { }, + POLY = BASE.polygons, + SLICER = KIRI.slicer, + newPoint = BASE.newPoint, + time = UTIL.time; + + /** + * 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) { + var spro = settings.process, + spri = settings.device, + sout = settings.process, + update_start = time(), + minSolid = spro.sliceSolidMinArea, + solidLayers = spro.sliceSolidLayers, + doSolidLayers = solidLayers && !spro.sliceVase, + firstOffset = spri.nozzleSize / 2, + shellOffset = spri.nozzleSize * spro.sliceShellSpacing, + fillOffset = shellOffset * settings.synth.fillOffsetMult, + sliceFillAngle = spro.sliceFillAngle, + view = widget.mesh && widget.mesh.newGroup ? widget.mesh.newGroup() : null; + + if (spro.sliceHeight <= 0.01) { + DBUG.log("invalid slice height"); + return ondone(null); + } + + SLICER.sliceWidget(widget, { + height: spro.sliceHeight, + view:view, + firstHeight: spro.sliceHeight * sout.firstLayerHeight + }, onSliceDone, onSliceUpdate); + + function onSliceUpdate(update) { + onupdate(0.0 + update * 0.5); + } + + function onSliceDone(slices) { + widget.slices = slices; + + if (!slices) return; + + // calculate % complete and call onupdate() + function doupdate(index, from, to, msg) { + onupdate(0.5 + (from + ((index/slices.length) * (to-from))) * 0.5, msg); + } + + // for each slice, performe a function and call doupdate() + function forSlices(from, to, fn, msg) { + slices.forEach(function(slice) { + fn(slice); + doupdate(slice.index, from, to, msg) + }); + } + + // do not hint polygin fill longer than a max span length + CONF.hint_len_max = UTIL.sqr(spro.sliceBridgeMax); + + // reset (if necessary) for solids and support projections + slices.forEach(function(slice) { + slice.invalidateFill(); + slice.invalidateSolids(); + slice.invalidateSupports(); + }); + + var supportEnabled = spro.sliceSupportEnable && spro.sliceSupportDensity > 0.0, + supportMinArea = spro.sliceSupportArea; + + // create shells and diff inner fillable areas + forSlices(0.0, 0.2, function(slice) { + var solid = ( + slice.index < spro.sliceBottomLayers || + slice.index > slices.length - spro.sliceTopLayers-1 || + spro.sliceFillSparse > 0.95 + ) && !spro.sliceVase; + slice.doShells(spro.sliceShells, firstOffset, shellOffset, fillOffset, spro.sliceVase); + if (solid) slice.doSolidLayerFill(spri.nozzleSize, sliceFillAngle, spro.sliceFillDensity); + sliceFillAngle += 90.0; + }, "offsets"); + + // calculations only relevant when solid layers are used + if (doSolidLayers) { + forSlices(0.2, 0.34, function(slice) { + slice.doDiff(minSolid); + }, "diff"); + forSlices(0.34, 0.35, function(slice) { + slice.projectFlats(solidLayers); + slice.projectBridges(solidLayers); + }, "solids"); + forSlices(0.35, 0.5, function(slice) { + slice.doSolidsFill(spri.nozzleSize, sliceFillAngle, spro.sliceFillDensity, minSolid); + sliceFillAngle += 90.0; + }, "solids"); + } + + // calculations only relevant when supports are enabled + if (supportEnabled) { + forSlices(0.5, 0.7, function(slice) { + slice.doSupport(spro.sliceSupportOffset, spro.sliceSupportSpan, spro.sliceSupportExtra, supportMinArea, spro.sliceSupportSize, spro.sliceSupportOffset); + }, "support"); + forSlices(0.7, 0.8, function(slice) { + slice.doSupportFill(spri.nozzleSize, spro.sliceSupportDensity, supportMinArea); + }, "support"); + } + + // sparse layers only present when non-vase mose and sparse % > 0 + if (!spro.sliceVase && spro.sliceFillSparse > 0.0) { + forSlices(0.8, 1.0, function(slice) { + slice.doSparseLayerFill(spri.nozzleSize, spro.sliceFillDensity, spro.sliceFillSparse, widget.getBoundingBox()); + }, "infill"); + } + + // report slicing complete + ondone(); + } + + }; + + /** + * DRIVER PRINT CONTRACT + * + * @param {Object} print state object + * @param {Function} update incremental callback + */ + FDM.printSetup = function(print, update) { + var widgets = print.widgets, + settings = print.settings, + device = settings.device, + process = settings.process, + mode = settings.mode, + output = print.output, + printPoint = newPoint(0,0,0), + maxLayers = 0, + layer = 0, + mesh, + meshIndex, + layerout, + closest, + mindist, + minidx, + find, + mslices, + slices, + sliceEntry; + + // find max layers (for updates) + widgets.forEach(function(widget) { + maxLayers = Math.max(maxLayers, widget.slices.length); + }); + + // for each layer until no layers are found + for (;;) { + slices = []; + layerout = []; + + // create list of mesh slice arrays with their platform offsets + for (meshIndex = 0; meshIndex < widgets.length; meshIndex++) { + mesh = widgets[meshIndex].mesh; + if (!mesh.widget) continue; + mslices = mesh.widget.slices; + if (mslices && mslices[layer]) { + slices.push({slice:mslices[layer], offset:mesh.position}); + } + } + + if (slices.length === 0) break; + + // create brim, if specificed in FDM mode (code shared by laser) + if (layer === 0 && process.outputBrimCount) { + var brims = [], + polys = [], + preout = [], + startPoint = printPoint; + + widgets.forEach(function(widget) { + var tops = []; + widget.slices[0].tops.forEach(function(top) { + tops.push(top.poly.clone()); + }); + POLY.nest(tops).forEach(function(poly) { + poly.offset(-process.outputBrimOffset).forEach(function(brim) { + brim.move(widget.mesh.position); + brims.push(brim); + }); + }); + }); + + POLY.union(brims).forEach(function(brim) { + POLY.trace2count(brim, polys, -device.nozzleSize, process.outputBrimCount, 0); + }); + + printPoint = print.poly2polyEmit(polys, printPoint, function(poly, index, count, startPoint) { + return print.polyPrintPath(poly, startPoint, preout); + }); + + print.addPrintPoints(preout, layerout, startPoint); + } + + // iterate over layer slices, find closest widget, print, eliminate + for (;;) { + closest = null; + mindist = Infinity; + for (meshIndex = 0; meshIndex < slices.length; meshIndex++) { + sliceEntry = slices[meshIndex]; + if (!sliceEntry) continue; + find = sliceEntry.slice.findClosestPointTo(printPoint.sub(sliceEntry.offset)); + if (find && (!closest || find.distance < mindist)) { + closest = sliceEntry; + mindist = find.distance; + minidx = meshIndex; + } + } + if (!closest) break; + slices[minidx] = null; + // output seek to start point between mesh slices if previous data + printPoint = print.slicePrintPath(closest.slice, printPoint.sub(closest.offset), closest.offset, layerout, true); + } + + if (layerout.length) output.append(layerout); + layer++; + update(layer / maxLayers); + } + }; + + /** + * @returns {Array} gcode lines + */ + FDM.printExport = function(print, online) { + var layers = print.output, + settings = print.settings, + device = settings.device, + process = settings.process, + time = 0, + layer = 0, + fan_power = device.gcodeFan, + trackProgress = device.gcodeTrack, + layer1speed = process.firstLayerSpeed, + decimals = 4, + output = [], + outputLength = 0, + lastProgress = 0, + progress = 0, + distance = 0, + emitted = 0, + pos = {x:0, y:0, z:0, f:0}, + zinc = process.sliceHeight, + zpos = zinc * process.firstLayerHeight, + offset = process.outputOriginCenter ? null : { + x: device.bedWidth/2, + y: device.bedDepth/2 + }, + consts = { + temp: process.outputTemp, + temp_bed: process.outputBedTemp, + bed_temp: process.outputBedTemp, + fan_speed: process.outputFanMax, + speed: process.outputFanMax, + top: offset ? device.bedDepth : device.bedDepth/2, + left: offset ? 0 : -device.bedWidth/2, + right: offset ? device.bedWidth : device.bedWidth/2, + bottom: offset ? 0 : -device.bedDepth/2, + z_max: device.maxHeight + }, + shortDist = process.outputShortDistance, + shortFact = process.outputShortFactor, + maxPrintMMM = process.outputFeedrate * 60, + seekMMM = process.outputSeekrate * 60, + retOver = process.outputRetractOver, + retDist = process.outputRetractDist, + retSpeed = process.outputRetractSpeed * 60, + // ratio of nozzle area to filament area times + // ratio of slice height to filament max noodle height + emitPerMM = print.extrudePerMM(device.nozzleSize, device.filamentSize, process.sliceHeight), + emitPerMMLayer1 = print.extrudePerMM(device.nozzleSize, device.filamentSize, process.sliceHeight * process.firstLayerHeight), + constReplace = print.constReplace, + pidx, path, out, lastp, dist, printMMM, shortMMM, + appendAll = function(arr) { + arr.forEach(function(line) { append(line) }); + }, + append, + lines = 0, + bytes = 0; + + if (online) { + append = function(line) { + if (line) { + lines++; + bytes += line.length; + output.append(line); + } + if (!line || output.length > 1000) { + online(output.join("\n")); + output = []; + } + }; + } else { + append = function(line) { + if (!line) return; + output.append(line); + lines++; + bytes += line.length; + } + } + + append("; Generated by KIRI:MOTO"); + append("; "+new Date().toString()); + append(constReplace("; Bed left:{left} right:{right} top:{top} bottom:{bottom}", consts)); + append("; --- startup ---"); + for (var i=0; i 0.0 && dist > retOver) moveTo({e:-retDist}, retSpeed, "ooze retract"); + moveTo({x:x, y:y}, seekMMM); + if (retOver > 0.0 && dist > retOver) moveTo({e:retDist}, retSpeed, "re-engage"); + time += (dist / seekMMM) * 60; // seek distance + time += (retDist / retSpeed) * 60 * 2; // retraction time + // approximate compensation for acceleration & deceleration + time += (shortDist * 2) / seekMMM / 10 * 60; + } + lastp = out.point; + // emit tracked progress + if (trackProgress && progress != lastProgress) { + append(constReplace(trackProgress, {progress:progress})); + lastProgress = progress; + } + } + layer++; + } + + append("; --- shutdown ---"); + for (var i=0; i mp[1] ? [(mp[0] / mp[1]) * 10, 10] : [10, (mp[1] / mp[1]) * 10], + // sort objects by size + c = output.sort(function (a, b) { return (b.w * b.h) - (a.w * a.h) }), + p = new MOTO.Pack(ms[0], ms[1], process.outputTileSpacing).fit(c); + + while (!p.packed) { + ms[0] += mi[0]; + ms[1] += mi[1]; + p = new MOTO.Pack(ms[0], ms[1], process.outputTileSpacing).fit(c); + } + + 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.forEach(function(o, i) { + // because first point emitted twice (begin/end) + e = o.point = o.point.clone(); + e.x += p.max.w / 2 - m.fit.x; + e.y += p.max.h / 2 - m.fit.y; + e.z = 0; + }); + } + }; + + /** + * + */ + function exportElements(print, onpre, onpoly, onpost, onpoint) { + + var process = print.settings.process, + output = print.output, + last, + point, + poly = [], + min = {x:0, y:0}, + max = {x:0, y:0}; + + output.forEach(function(layer) { + layer.forEach(function(out) { + point = out.point; + if (process.outputInvertX) point.x = -point.x; + if (process.outputInvertY) point.y = -point.y; + point.x *= process.outputTileScaling; + point.y *= process.outputTileScaling; + min.x = Math.min(min.x, point.x); + max.x = Math.max(max.x, point.x); + min.y = Math.min(min.y, point.y); + max.y = Math.max(max.y, point.y); + }); + }); + + // normalize against origin lower left + if (!process.outputOriginCenter) { + output.forEach(function(layer) { + layer.forEach(function(out) { + point = out.point; + point.x -= min.x; + point.y -= min.y; + }); + }); + max.x = max.x - min.x; + max.y = max.y - min.y; + min.x = 0; + min.y = 0; + } + + onpre(min, max, process.outputLaserPower, process.outputLaserSpeed); + + output.forEach(function(layer) { + layer.forEach(function(out) { + point = out.point; + if (out.emit) { + if (last && poly.length === 0) poly.push(onpoint(last)); + poly.push(onpoint(point)); + } else if (poly.length > 0) { + onpoly(poly); + poly = []; + } + last = point; + }); + if (poly.length > 0) { + onpoly(poly); + poly = []; + } + }); + + onpost(); + }; + + /** + * + */ + LASER.exportGCode = function(print) { + var lines = [], dx = 0, dy = 0, feedrate, laser_on; + + exportElements( + print, + function(min, max, power, speed) { + var width = (max.x - min.x), + height = (max.y - min.y); + dx = min.x; + dy = min.y; + feedrate = " F" + speed, + laser_on = "M106 S" + UTIL.round(256 * (power / 100), 3); + // pre + }, + function(poly) { + poly.forEach(function(point, index) { + if (index === 0) { + lines.push("G0 " + point); + } else if (index === 1) { + lines.push(laser_on); + lines.push("G1 " + point + feedrate); + } else { + lines.push("G1 " + point); + } + }); + lines.push("M107"); + }, + function() { + // post + }, + function(point) { + return "X" + UTIL.round(point.x - dx, 3) + " Y" + UTIL.round(point.y - dy, 3); + } + ); + + return lines.join('\n'); + }; + + /** + * + */ + LASER.exportSVG = function(print) { + var lines = [], dx = 0, dy = 0, my; + + exportElements( + print, + function(min, max) { + var width = (max.x - min.x), + height = (max.y - min.y); + dx = min.x; + dy = min.y; + my = max.y; + lines.push(''); + lines.push(''); + lines.push(''); + }, + function(poly) { + lines.push(''); + }, + function() { + lines.push(""); + }, + function(point) { + return UTIL.round(point.x - dx, 3) + "," + UTIL.round(my - point.y - dy, 3); + } + ); + + return lines.join('\n'); + }; + + /** + * + */ + LASER.exportDXF = function(print) { + var lines = []; + + exportElements( + print, + function(min, max) { + lines.appendAll([ + ' 0', + 'SECTION', + ' 2', + 'HEADER', + ' 9', + '$ACADVER', + '1', + 'AC1014', + ' 0', + 'ENDSEC', + ' 0', + 'SECTION', + ' 2', + 'ENTITIES', + ]); + }, + function(poly) { + lines.appendAll([ + ' 0', + 'LWPOLYLINE', + '100', // subgroup required + 'AcDbPolyline', + ' 90', // poly vertices + poly.length, + ' 70', // open + '0', + ' 43', // constant width line + '0.0' + ]); + + poly.forEach(function(point) { + lines.appendAll([ + ' 10', + point.x, + ' 20', + point.y + ]); + }); + + lines.appendAll([ + ' 0', + 'SEQEND', + ]); + }, + function() { + lines.appendAll([ + ' 0', + 'ENDSEC', + ' 0', + 'EOF', + ]); + }, + function(point) { + return {x:point.x,y:point.y}; + } + ); + + return lines.join('\n'); + }; + +})(); diff --git a/js/kiri-layer.js b/js/kiri-layer.js new file mode 100644 index 00000000..84fef6f5 --- /dev/null +++ b/js/kiri-layer.js @@ -0,0 +1,165 @@ +"use strict"; + +var gs_kiri_layer = { + copyright:"stewart allen -- all rights reserved" +}; + +(function() { + + if (!self.kiri) self.kiri = {}; + if (self.kiri.Layer) return; + + var KIRI = self.kiri, + LP = Layer.prototype, + mcache = {}; + + KIRI.Layer = Layer; + KIRI.newLayer = function(view) { return new Layer(view) }; + + function materialFor(color) { + var m = mcache[color]; + if (!m) { + m = mcache[color] = new THREE.LineBasicMaterial({ + fog: false, + color: color, + linewidth: 1 + }); + } + return m; + } + + function toVector(p) { + return new THREE.Vector3(p.x, p.y, p.z); + } + + function addPoly(arr, poly, deep, open) { + var points = poly.points, + len = points.length; + + if (len < 2) return; + + var doOpen = (open === undefined || open === null) ? poly.isOpen() : open, + end = doOpen ? len - 1 : len, + last = toVector(points[0]), + i = 0; + + while (i < end) { + arr.push(last); + arr.push(last = toVector(points[(++i) % len])); + } + + if (deep && poly.inner) { + poly.inner.forEach(function(p) { + addPoly(arr, p, false, open); + }) + } + } + + /** + * @param {THREE.Group} view + */ + function Layer(view) { + this.changed = false; + this.group = null; + this.view = view; + this.bycolor = {}; + }; + + LP.setVisible = function(vis) { + if (this.group) this.group.visible = vis; + }; + + LP.clear = function() { + this.changed = true; + this.bycolor = {}; + }; + + LP.add = function(color, obj) { + var ca = this.bycolor[color]; + if (!ca) ca = this.bycolor[color] = []; + ca.push(obj); + }; + + LP.poly = function(poly, color, deep, open) { + var layer = this; + if (Array.isArray(poly)) { + poly.forEach(function(p) { layer.poly(p, color, deep, open) }); + } else { + this.add(color, {poly:poly, deep:deep, open:open}); + this.changed = true; + } + }; + + LP.points = function(points, color, size, opacity) { + var layer = this, + sz = size/2; + points.forEach(function(p) { + layer.lines([ + toVector({x:p.x+sz, y:p.y+sz, z:p.z-sz}), + toVector({x:p.x+sz, y:p.y+sz, z:p.z+sz}), + toVector({x:p.x-sz, y:p.y+sz, z:p.z-sz}), + toVector({x:p.x-sz, y:p.y+sz, z:p.z+sz}), + toVector({x:p.x+sz, y:p.y-sz, z:p.z-sz}), + toVector({x:p.x+sz, y:p.y-sz, z:p.z+sz}), + toVector({x:p.x-sz, y:p.y-sz, z:p.z-sz}), + toVector({x:p.x-sz, y:p.y-sz, z:p.z+sz}), + + toVector({x:p.x+sz, y:p.y+sz, z:p.z+sz}), + toVector({x:p.x-sz, y:p.y+sz, z:p.z+sz}), + toVector({x:p.x+sz, y:p.y+sz, z:p.z-sz}), + toVector({x:p.x-sz, y:p.y+sz, z:p.z-sz}), + toVector({x:p.x+sz, y:p.y-sz, z:p.z+sz}), + toVector({x:p.x-sz, y:p.y-sz, z:p.z+sz}), + toVector({x:p.x+sz, y:p.y-sz, z:p.z-sz}), + toVector({x:p.x-sz, y:p.y-sz, z:p.z-sz}), + + toVector({x:p.x+sz, y:p.y+sz, z:p.z+sz}), + toVector({x:p.x+sz, y:p.y-sz, z:p.z+sz}), + toVector({x:p.x+sz, y:p.y+sz, z:p.z-sz}), + toVector({x:p.x+sz, y:p.y-sz, z:p.z-sz}), + toVector({x:p.x-sz, y:p.y+sz, z:p.z+sz}), + toVector({x:p.x-sz, y:p.y-sz, z:p.z+sz}), + toVector({x:p.x-sz, y:p.y+sz, z:p.z-sz}), + toVector({x:p.x-sz, y:p.y-sz, z:p.z-sz}), + ], color); + }); + }; + + LP.lines = function(points, color) { + this.add(color, {lines:points}); + this.changed = true; + }; + + LP.render = function() { + if (!(this.view && this.changed)) return; + if (this.group) this.view.remove(this.group); + this.group = this.view.newGroup(); + + var bycolor = this.bycolor, + key, added; + + for (key in bycolor) { + if (!bycolor.hasOwnProperty(key)) continue; + + var geo = new THREE.Geometry(), + arr = geo.vertices, + mat = materialFor(parseInt(key)); + added = bycolor[key]; + + added.forEach(function(obj) { + if (obj.poly) { + addPoly(arr, obj.poly, obj.deep, obj.open); + } else if (obj.lines) { + obj.lines.forEach(function(p) { + arr.push(toVector(p)); + }); + } + }); + + if (arr.length > 0) this.group.add(new THREE.LineSegments(geo, mat)); + } + + this.changed = false; + } + +})(); diff --git a/js/kiri-pack.js b/js/kiri-pack.js new file mode 100644 index 00000000..aca13042 --- /dev/null +++ b/js/kiri-pack.js @@ -0,0 +1,64 @@ +"use strict"; + +var gs_kiri_pack = { + copyright:"stewart allen -- all rights reserved" +}; + +/** + * adapted from + * http://codeincomplete.com/posts/2011/5/7/bin_packing/ + */ + +(function (){ + + function Packer (w, h, spacing) { + this.root = { x: 0, y: 0, w: w, h: h }; + this.max = { w: 0, h: 0 }; + this.packed = false; + this.spacing = typeof(spacing) === 'number' ? spacing : 1; + this.pad = this.spacing / 2; + } + + Packer.prototype = { + + fit: function (blocks) { + var n = 0, node, block, w, h; + while (n < blocks.length) { + block = blocks[n++]; + w = block.w + this.spacing; + h = block.h + this.spacing; + if (node = this.findNode(this.root, w, h)) { + block.fit = this.splitNode(node, w, h); + } else { + return this; + } + } + this.packed = true; + return this; + }, + + findNode: function (root, w, h) { + if (root.used) { + return this.findNode(root.right, w, h) || this.findNode(root.down, w, h); + } else if (w <= root.w && h <= root.h) { + return root; + } else { + return null; + } + }, + + splitNode: function (node, w, h) { + 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 }; + this.max.w = Math.max(this.max.w, node.x + w); + this.max.h = Math.max(this.max.h, node.y + h); + return node; + } + + }; + + if (!self.moto) self.moto = {}; + self.moto.Pack = Packer; + +})(); diff --git a/js/kiri-print.js b/js/kiri-print.js new file mode 100644 index 00000000..d90a3b4a --- /dev/null +++ b/js/kiri-print.js @@ -0,0 +1,765 @@ +/** Copyright 2014-2017 Stewart Allen -- All Rights Reserved */ + +"use strict"; + +var gs_kiri_print = exports; + +(function() { + + if (!self.kiri) self.kiri = {}; + + var KIRI = self.kiri, + DRIVERS = KIRI.driver, + CAM = DRIVERS.CAM, + FDM = DRIVERS.FDM, + LASER = DRIVERS.LASER, + BASE = self.base, + UTIL = BASE.util, + DBUG = BASE.debug, + POLY = BASE.polygons, + SQRT = Math.sqrt, + PI = Math.PI, + PROTO = Print.prototype, + Polygon = BASE.Polygon, + newPoint = BASE.newPoint; + + KIRI.newPrint = function(settings, widgets, id) { return new Print(settings, widgets, id) }; + + /** + * @param {Object} settings + * @param {Widget[]} widgets + * @constructor + */ + function Print(settings, widgets, id) { + this.id = id || new Date().getTime().toString(36); + this.settings = settings; + this.widgets = widgets; + + this.group = new THREE.Group(); + this.layerView = []; + + this.time = 0; + this.lines = 0; + this.bytes = 0; + this.output = []; + this.distance = 0; + this.bounds = null; + } + + PROTO.newOut = newOut; + PROTO.tip2tipEmit = tip2tipEmit; + PROTO.extrudePerMM = extrudePerMM; + PROTO.constReplace = constReplace; + PROTO.poly2polyEmit = poly2polyEmit; + PROTO.addPrintPoints = addPrintPoints; + PROTO.poly2polyDepthFirstEmit = poly2polyDepthFirstEmit; + + PROTO.parseGCode = function(gcode, offset) { + var lines = gcode + .toUpperCase() + .replace("X", " X") + .replace("Y", " Y") + .replace("Z", " Z") + .replace("E", " E") + .replace("F", " F") + .replace(" ", " ") + .split("\n"); + + var scope = this, + output = scope.output = [], + bounds = scope.bounds = { + max: { x:-Infinity, y:-Infinity, z:-Infinity}, + min: { x:Infinity, y:Infinity, z:Infinity} + }, + seq = [], + move = false, + E0G0 = false, + G0 = function() { + move = true; + if (seq.length > 0) { + output.push(seq); + seq = []; + } + }, + LZ = 0.0, + pos = { + X: 0.0, + Y: 0.0, + Z: 0.0, + F: 0.0, + E: 0.0 + }, + off = { + x: offset ? offset.x || 0 : 0, + y: offset ? offset.y || 0 : 0, + z: offset ? offset.z || 0 : 0 + }; + + lines.forEach(function(line) { + line = line.split(" "); + if (line.length < 2) return; + switch (line.shift()) { + case 'G0': + G0(); + case 'G1': + line.forEach(function(tok) { + pos[tok.charAt(0)] = parseFloat(tok.substring(1)); + }); + if (pos.X) bounds.min.x = Math.min(bounds.min.x, pos.X); + if (pos.X) bounds.max.x = Math.max(bounds.max.x, pos.X); + if (pos.Y) bounds.min.y = Math.min(bounds.min.y, pos.Y); + if (pos.Y) bounds.max.y = Math.max(bounds.max.y, pos.Y); + if (pos.Z) bounds.min.z = Math.min(bounds.min.z, pos.Z); + if (pos.Z) bounds.max.z = Math.max(bounds.max.z, pos.Z); + if (pos.E) E0G0 = true; + if (E0G0 && pos.E === 0.0) { + if (LZ != pos.Z) G0(); + else move = true; + } + seq.push(newOut( + {x:pos.X + off.x, y:pos.Y + off.y, z:pos.Z + off.z}, + !move, + pos.F + )); + break; + case 'M6': + break; + } + move = false; + pos.E = 0.0; + LZ = pos.Z; + }); + + G0(); + + scope.lines = lines.length; + scope.bytes = gcode.length; + }; + + PROTO.setup = function(remote, onupdate, ondone) { + var scope = this, + settings = scope.settings, + mode = settings.mode; + + if (remote) { + KIRI.work.printSetup(settings, function(reply) { + if (reply.done) { + scope.output = reply.output; + ondone(); + } else { + onupdate(reply.update, reply.updateStatus) + } + }); + } else { + var driver = KIRI.driver[mode]; + if (driver) driver.printSetup(scope, onupdate); + else console.log({missing_print_driver: mode}); + ondone(); + } + }; + + PROTO.exportGCode = function(remote, ondone, online) { + var scope = this, + settings = scope.settings, + mode = settings.mode; + + if (remote) { + KIRI.work.printGCode(function(reply) { + scope.lines = reply.lines; + scope.bytes = reply.bytes; + scope.bounds = reply.bounds; + scope.distance = reply.distance; + scope.time = reply.time; + ondone(reply.gcode); + }); + return; + } else { + var driver = KIRI.driver[mode]; + if (driver && driver.printExport) { + ondone(driver.printExport(scope, online)); + } else { + console.log({missing_export_driver: mode}); + ondone(null); + } + } + }; + + PROTO.exportLaserGCode = function() { + return KIRI.driver.LASER.exportGCode(this); + }; + + PROTO.exportSVG = function() { + return KIRI.driver.LASER.exportSVG(this); + }; + + PROTO.exportDXF = function() { + return KIRI.driver.LASER.exportDXF(this); + }; + + PROTO.encodeOutput = function() { + var newout = [], newlayer; + + this.output.forEach(function(layerout) { + newlayer = []; + newout.push(newlayer); + layerout.forEach(function(out) { + if (out.point) newlayer.push({emit:out.emit, point:{x:out.point.x, y:out.point.y, z:out.point.z}}); + }); + }); + return newout; + }; + + PROTO.render = function() { + var scope = this, + mode = scope.settings.mode; + + switch (mode) { + case 'CAM': + case 'FDM': + scope.renderMoves(true, 0x0088aa); + break; + case 'LASER': + scope.renderMoves(false, 0x0088aa); + break; + } + }; + + PROTO.renderMoves = function(showMoves, moveColor) { + var scope = this, last, view; + // render layered output + scope.lines = 0; + scope.output.forEach(function(layerout) { + var move = [], print = [], z; + layerout.forEach(function(out) { + if (last) { + if (UTIL.distSq(last, out.point) < 0.001 && out.point.z === last.z) { + return; + } + if (out.emit > 0) { + print.push(last); + print.push(out.point); + } else { + move.push(last); + move.push(out.point); + } + } else { + if (out.emit) DBUG.log("first point is emit"); + z = out.point.z; + } + last = out.point; + }); + view = KIRI.newLayer(scope.group); + scope.layerView.push(view); + if (showMoves) view.lines(move, moveColor); + view.lines(print, 0x5566aa); + view.render(); + scope.lines += print.length; + }); + } + + PROTO.getLayerCount = function() { + return this.output.length; + } + + PROTO.hide = function() { + this.layerView.forEach(function(layer) { + layer.setVisible(false); + }) + }; + + PROTO.showLayer = function(index, show) { + if (this.layerView[index]) this.layerView[index].setVisible(show); + }; + + /** + * @constructor + */ + function Output(point, emit, speed, tool) { + this.point = point; // point to emit + this.emit = emit; // emit (feed for printers, power for lasers, cut for cam) + this.speed = speed; + this.tool = tool; + } + + /** + * @param {Point} point + * @param {number} emit (0=move, !0=filament emit/laser on/cut mode) + * @param {number} [speed] speed + * @param {number} [tool] tool + */ + function newOut(point, emit, speed, tool) { + return new Output(point, emit, speed, tool); + } + + /** + * + * @param {Polygon} poly + * @param {Point} startPoint + * @param {Array} output + * @param {number} [extrude] multiplier + * @param {number} [extrude] wipe distance + * @return {Point} last output point + */ + PROTO.polyPrintPath = function(poly, startPoint, output, extrude, wipe) { + // poly.setClosed(); + poly.setClockwise(); + + var mindist = Infinity, + closest = poly.findClosestPointTo(startPoint), + dist, + first = true, + settings = this.settings, + shellMult = extrude || settings.process.outputShellMult; + + poly.forEachPoint(function(point) { + if (first) { + // move from startPoint to point + output.push(newOut(point, 0)); + first = false; + } else { + output.push(newOut(point, shellMult)); + } + }, true, closest.index); + + if (wipe) { + wipe = Math.min(wipe, poly.perimeter()); + poly.forEachSegment(function(point, next) { + // exit loop when no more wipe + if (!(wipe && wipe > 0.1)) return true; + dist = point.distTo2D(next); + if (dist <= wipe) { + output.push(newOut(next, 0)); + wipe -= dist; + } else { + output.push(newOut(point.followTo(next, wipe), 0)); + return true; + } + }, false, closest.index); + } + + return output[output.length - 1].point; + }; + + /** + * create 3d print output path for this slice + * + * @parma {Slice} slice + * @param {Point} startPoint start as close as possible to startPoint + * @param {THREE.Vector3} offset + * @param {Point[]} output points + * @param {boolean} isFDM controls whether we emit wipe or not + * @return {Point} last output point + */ + PROTO.slicePrintPath = function(slice, startPoint, offset, output, isFDM) { + var i, + preout = [], + scope = this, + settings = this.settings, + process = settings.process, + minSeek = settings.device.nozzleSize * 1.5, + wipeDist = process.outputWipeDistance, // todo disable for laser mode + fillMult = process.outputFillMult, + seekMult = fillMult * 0.5, + origin = startPoint.add(offset), + extrude = process.outputShellMult || (process.laserSliceHeight >= 0 ? 1 : 0), + z = slice.z; + + function outputTraces(poly, extrude, last) { + if (!poly) return; + if (Array.isArray(poly)) { + outputOrderClosest(poly, function(next) { + outputTraces(next, extrude, last); + }); + } else { + startPoint = scope.polyPrintPath(poly, startPoint, preout, extrude, isFDM && last ? wipeDist : 0); + } + } + + /** + * @param {Polygon[]} polys + */ + function outputSparse(polys) { + if (!polys) return; + var lines = [], p1, p2, iter; + polys.forEach(function(poly) { + poly.forEachSegment(function(p1,p2) { + lines.push(p1.clone()); + lines.push(p2.clone()); + }, true); + }); + outputFills(lines); + } + + function outputFills(lines, extrude) { + var mindist, p1, p2, dist, point, find, + printMult = extrude || fillMult; + while (lines) { + find = null; + mindist = Infinity; + for (i=0; i 0) { + output.push(newOut(startPoint, 0)); + } + output.appendAll(input); + } + + /** + * emit each element in an array based on + * the next closest endpoint. + * todo replace outputFills() with this + */ + function tip2tipEmit(array, startPoint, emitter) { + var mindist, dist, found, count = 0; + + for (;;) { + found = null; + mindist = Infinity; + array.forEach(function(el) { + if (el.delete) return; + dist = startPoint.distTo3D(el.first); + if (dist < mindist) { + found = {el:el, first:el.first, last:el.last}; + mindist = dist; + } + dist = startPoint.distTo3D(el.last); + if (dist < mindist) { + found = {el:el, first:el.last, last:el.first}; + mindist = dist; + } + }); + if (found) { + found.el.delete = true; + startPoint = found.last; + emitter(found.el, found.first, ++count); + } else { + break; + } + } + + return startPoint; + } + + /** + * like tip2tipEmit but accepts an array of + * polygons and the next closest point can + * be anywhere in the adjacent polygon + */ + function poly2polyEmit(array, startPoint, emitter) { + var mindist, dist, found, count = 0; + for (;;) { + found = null; + mindist = Infinity; + array.forEach(function(poly) { + if (poly.delete) return; + if (poly.isOpen()) { + const d2f = startPoint.distTo2D(poly.first()); + const d2l = startPoint.distTo2D(poly.first()); + if (d2f > mindist && d2l > mindist) return; + if (d2l < mindist && d2l < d2f) { + poly.reverse(); + found = {poly:poly, index:0, point:poly.first()}; + } else if (d2f < mindist) { + found = {poly:poly, index:0, point:poly.first()}; + } + return; + } + poly.forEachPoint(function(point, index) { + dist = startPoint.distTo3D(point); + if (dist < mindist) { + found = {poly:poly, index:index, point:point}; + mindist = dist; + } + }); + }); + if (found) { + found.poly.delete = true; + startPoint = emitter(found.poly, found.index, ++count, startPoint) || found.point; + } else { + break; + } + } + + // undo delete marks + array.forEach(function(poly) { poly.delete = false }); + + return startPoint; + } + + /** + * @param {Polygon[][]} array of array of polygons representing each layer (top down) + * @param {Point} startPoint entry point for algorithm + * @param {Function} emitter called to emit each polygon + * @param {number} offset tool diameter used for this depth-first cut + * + * used for CAM depth first layer output + */ + function poly2polyDepthFirstEmit(array, startPoint, emitter, offset) { + var layers = [], + pools; + + array.forEach(function(layerPolys, layerIndex) { + pools = []; + layers.push(pools); + + // flattening but preserving inner relationships + // allows iterating over all layer polys to determine + // if they deserve their own pool + flattenPolygons(POLY.nest(layerPolys, true, true)).sort(function(p1,p2) { + // sort by area descending + return p2.area() - p1.area(); + }).forEach(function (poly) { + // a polygon should be made into a pool if: + // - it is open + // - it has more than one sibling + // - it has no parent (top/outer most) + // - it is offset from its parent by more than diameter + if (poly.isOpen() || !poly.parent || poly.parent.innerCount() > 1 || !polygonWithinOffset(poly, poly.parent, offset)) { + pools.push(poly); + poly.pool = []; + poly.poolsDown = []; + } else { + // otherwise walk up the parent tree to find a pool to join + var search = poly.parent; + // walk up until pool found + while (search && !search.pool) { + search = search.parent; + } + // open polygons can be unparented and without a pool + if (!search) { + console.log({orphan:poly}); + return; + } + // add to pool + search.pool.push(poly); + } + }); + + // sort pools increasing in size to aid fitting from below + pools.sort(function (p1, p2) { + return p1.area() - p2.area(); + }); + + // add add pools to smallest enclosing pool in layer above + const poolsAbove = layers[layerIndex - 1]; + + if (layerIndex > 0) + pools.forEach(function(pool) { + for (var i=0; i 0) { + v1 = consts[tok.substring(0,pos)] || 0; + v2 = parseInt(tok.substring(pos+1)) || 0; + return op(v1,v2); + } else { + return null; + } + } + + function constReplace(str, consts, start) { + var cs = str.indexOf("{", start || 0), + ce = str.indexOf("}", cs), + tok, nutok, nustr; + if (cs >=0 && ce > cs) { + tok = str.substring(cs+1,ce); + nutok = + constOp(tok, consts, "-", function(v1,v2) { return v1-v2 }) || + constOp(tok, consts, "+", function(v1,v2) { return v1+v2 }) || + constOp(tok, consts, "/", function(v1,v2) { return v1/v2 }) || + constOp(tok, consts, "*", function(v1,v2) { return v1*v2 }) || + consts[tok] || 0; + nustr = str.replace("{"+tok+"}",nutok); + return constReplace(nustr, consts, ce+1+(nustr.length-str.length)); + } else { + return str; + } + } + +})(); diff --git a/js/kiri-serial.js b/js/kiri-serial.js new file mode 100644 index 00000000..2abd5456 --- /dev/null +++ b/js/kiri-serial.js @@ -0,0 +1,608 @@ +(function() { + if (!self.kiri) return; + if (self.kiri.serial) return; + + // tinyg motor (en/dis)able $me / $md + // tinyg '%' to flush queue + // grbl motor (en/dis)able $1= (free after ms) $1=255 (always energized) + + var SELF = self, + KIRI = SELF.kiri, + SPACE = KIRI.space, + LOC = SELF.location, + SDB = moto.KV, + API = kiri.api, + initDone = false, + nextID = new Date().getTime(), + localQueue = [], // buffered command queue + localQueueMax = 1500, // max local buffer + bounds = null, // bounding box of gcode + gcode = null, // gcode buffer + gcodeIndex = 0, // gcode buffer send index + gcodeAbort = false, // send abort + logBuffer = [], // on screen log buffer + sendBatchMax = 1000, // max to queue to waiting + gcLinesSent, // status of local lines sent + gcRemoteQueue, // status of remote queue + gcPaused = false, // true if user paused + status = null, // machine status + socket, // connection to serial sender + ports, // list of available ports + senderTimeout, // repeating sender call + waitingForAck = [], // ids sent to spjs awaiting ack + spjsQueueCount = 0, // spjs mem queue size + spjsQueueCountMax = 1500, // do not send to spjs over this queue size + selectedPort, + selectedMode, + commandInput, + logDiv, + jogInput, + hostInput, + queueInput, + connect, // button + senderStatus, // machine status + senderDialog, + senderGcPause, // pause button + senderPortClose, // button + senderSelectMode, // select dropdown + senderSelectPort; // select dropdown + + var init_tinyg = [ + '{"ej":""}', // enable json + '{"js":1}', // json syntax strict + '{"sr":n}', // status request + '{"sv":1}', // status verbosity filtered + '{"si":250}', // status interval 250ms + '{"qr":n}', // request queue report + '{"qv":1}', // set queue report verbosity + '{"ec":0}', // disable LF (from CRLF) + '{"jv":4}', // json verbosity 4 + '{"hp":n}', // get hardware platform + '{"fb":n}', // get firmware version + '{"mt":n}', // get motor timeout + '{"sr":n}', + '{"pos":n}' // request position info + ], + init_grbl = [ + '*init*', // init port + '*status*' // request position info + ]; + + KIRI.serial = { + setGCode: setGCode, + show: show, + hide: hide, + toggle: toggle, + init: init + }; + + function init() { + if (initDone) return; + initDone = true; + + var pre = LOC.protocol === 'https:' ? 'wss://' : 'ws://', + rpx = $('srxi'), + rpy = $('sryi'), + rpz = $('srzi'), + wpx = $('saxi'), + wpy = $('sayi'), + wpz = $('sazi'); + + senderDialog = $('sender'); + senderStatus = $('sender-status'); + commandInput = $('sender-command'); + logDiv = $('sender-log'); + jogInput = $('sender-jog'); + hostInput = $('sender-host'); + connect = $('sender-connect'); + gcLinesSent = $('sender-gc-sent'); + gcRemoteQueue = $('sender-gc-queue'); + senderSelectMode = $('sender-mode'); + senderSelectPort = $('sender-port'); + senderPortClose = $('sender-port-close'); + senderGcPause = $('sender-gc-pause'); + + senderSelectMode.onchange = selectMode; + senderSelectPort.onchange = selectPort; + senderPortClose.onclick = closePort; + senderPortClose.disabled = true; + + $('sender-close').onclick = hide; + $('sender-spjs').onclick = function() { window.open("https://wiki.grid.space/wiki/GCode-Sender-in-CAM-Mode", "_help")}; + $('sjx-').onclick = function() { jog('X',-1) }; + $('sjx+').onclick = function() { jog('X',1) }; + $('sjy-').onclick = function() { jog('Y',-1) }; + $('sjy+').onclick = function() { jog('Y',1) }; + $('sjz-').onclick = function() { jog('Z',-1) }; + $('sjz+').onclick = function() { jog('Z',1) }; + $('sender-set-zero').onclick = function() { sendNow("G92 X0Y0Z0") }; + $('sender-goto-zero').onclick = function() { sendNow("G90 G0X0Y0Z0") }; + $('sender-ctrlx').onclick = softReset; + $('sender-hold').onclick = feedHold; + $('sender-resume').onclick = feedResume; + + $('sender-pad').onmouseover = function() { + jogInput.focus(); + }; + $('sender-pad').addEventListener('keydown', function(ev) { + var dist = (ev.cmdKey ? 0.1 : 1); + switch (ev.keyCode) { + case 37: // left arrow + jog('X', -dist); + ev.preventDefault(); + break; + case 39: // right arrow + jog('X', dist); + ev.preventDefault(); + break; + case 38: // up arrow + jog(ev.shiftKey ? 'Z' : 'Y', dist); + ev.preventDefault(); + break; + case 40: // down arrow + jog(ev.shiftKey ? 'Z' : 'Y', -dist); + ev.preventDefault(); + break; + } + }); + + $('sender-gc-runbox').onclick = runbox; + + $('sender-gc-send').onclick = programStart; + senderGcPause.onclick = programPause; + $('sender-gc-abort').onclick = programAbort; + + hostInput.value = SDB['kiri-serial'] || ''; + + SPACE.onEnterKey([ + commandInput, function() { + if (selectedPort) { + sendNow(commandInput.value); + emit("» "+commandInput.value); + } + commandInput.value = ''; + } + ]); + + var handleMessageData = function(dm) { + dm = dm.trim(); + if (dm.charAt(0) === '<') { + // grbl status update + dm = dm.substring(1,dm.length-2); + dm = dm.split(','); + // console.log(dm); + status = dm[0]; + senderStatus.value = status; + switch (status) { + case 'Idle': + case 'Run': + case 'Home': + case 'Check': + senderStatus.style.color = '#080'; + break; + case 'Hold': + case 'Door': + senderStatus.style.color = '#800'; + break; + case 'Alarm': + senderStatus.style.color = '#880'; + break; + } + rpx.value = dm[1].split(':')[1]; + rpy.value = dm[2]; + rpz.value = dm[3]; + wpx.value = dm[4].split(':')[1]; + wpy.value = dm[5]; + wpz.value = dm[6]; + } else + if (dm.charAt(0) === '{') { + // tinyg status update + dm = js2o(dm); + var ro = dm.sr || (dm.r && dm.r.sr ? dm.r.sr : null); + if (ro) { + if (ro.posx !== undefined) rpx.value = ro.posx; + if (ro.posy !== undefined) rpy.value = ro.posy; + if (ro.posz !== undefined) rpz.value = ro.posz; + } + var stat = dm.stat || (dm.r && dm.r.stat ? dm.r.stat : null) || 1; + status = ['Unknown','Ready','Alarm','Stop','End','Run','Hold','Probe','Homing'][stat]; + senderStatus.style.color = ['#000','#080','#880','#000','#000','#000','#800','#000','#000'][stat]; + senderStatus.value = status; + // console.log(dm); + } else + if (dm && dm !== 'ok') emit("« "+dm); + } + + var handleSocketData = function(msg) { + var data = msg.data.trim(); + if (data.charAt(0) === '{') { + data = js2o(data); + // if (data.Cmd) console.log(data); + if (data.Cmd && data.Cmd === 'Queued' && data.Data) queueAck(data.Data); + if (data.QCnt >= 0) { + spjsQueueCount = data.QCnt; + gcRemoteQueue.value = spjsQueueCount; + } + if (data.SerialPorts) updatePortList(data.SerialPorts); + if (selectedPort && data.P === selectedPort.Name) { + // console.log(data); + if (data.D) handleMessageData(data.D); + } + } else if (data.length > 0) { + console.log("** "+data.trim().replace('\n',' ... ')); + } + } + + var onSocketClose = function() { + socket = null; + connect.innerHTML = 'connect'; + senderSelectMode.disabled = true; + senderSelectPort.disabled = true; + }; + + connect.onclick = function() { + if (socket) { + // connect.innerHTML = 'connect'; + socket.close(); + socket = null; + return; + } + + if (!hostInput.value) { + return alert('please enter the host:port of your SPJS server'); + } + + SDB['kiri-serial'] = hostInput.value; + + try { + socket = new WebSocket(pre + hostInput.value + "/ws"); + } catch (e) { + emit("** unable to connect to "+hostInput.value); + return; + } + socket.onopen = function() { + emit("** websocket open to "+hostInput.value); + socket.send('list'); + }; + socket.onerror = function(e) { + emit("** socket error with SPJS server @ "+hostInput.value); + connect.disabled = false; + socket.close(); + // console.log(e); + }; + socket.onclose = onSocketClose; + socket.onmessage = handleSocketData; + connect.innerHTML = 'disconnect'; + connect.disabled = true; + }; + + if (socket && ports) { + updatePortList(ports); + emit(); + } + + // start sender + sender(); + } + + function sendOpenSequence() { + var seq = []; + switch (selectedMode) { + case 'grbl': seq = init_grbl; break; + case 'tinyg': seq = init_tinyg; break; + } + seq.forEach(function(line) { + sendNow(line); + }); + } + + function sender() { + // prevent multiple senders + if (senderTimeout) return; + drainQueue(); + // setup next call to sender() + senderTimeout = setTimeout(function() { + senderTimeout = null; + sender(); + }, 100); + } + + function o2js(o) { + return JSON.stringify(o); + } + + function js2o(s) { + try { + return JSON.parse(s); + } catch (e) { + console.log(e); + console.log(s); + } + } + + function log(msg) { + console.log("serial | "+msg); + } + + function sendGcode() { + if (gcode) { + // timeout and loop at waiting threshold + if (waitingForAck.length || localQueue.length > localQueueMax) { + setTimeout(sendGcode, 100); + return; + } + console.log({ + gcode: (gcode !== null ? gcode.length : 0), + idx: gcodeIndex, + waitack: waitingForAck.length, + abort: gcodeAbort + }); + // queue up 500 more + while (gcodeIndex < gcode.length && !gcodeAbort) { + sendToQueue(gcode[gcodeIndex++]); + // drain queue every 500 lines + if (gcodeIndex % 500 === 0) { + // wait 100ms before trying to send again + setTimeout(sendGcode, 100); + return; + } + } + drainQueue(); + } + } + + function setGCode(gc, runbox) { + try { + gcode = gc.split('\n'); + bounds = runbox; + $('sender-gc-lines').value = gcode ? gcode.length : ''; + } catch (e) { + console.log(e); + } + } + + function closePort() { + if (!selectedPort) return; + emit("** closing port: "+selectedPort.Name); + socket.send('close '+selectedPort.Name); + senderSelectPort.selectedIndex = 0; + senderSelectMode.selectedIndex = 0; + senderPortClose.disabled = true; + selectedPort = null; + selectedMode = null; + } + + function updatePortSettings() { + // console.log({ups:1, selectedPort:selectedPort, selectedMode:selectedMode}); + if (selectedPort) { + if (selectedPort.IsOpen) { + if (selectedMode) { + senderPortClose.disabled = false; + if (selectedMode != selectedPort.BufferAlgorithm) { + alert("to change a port's mode, first close it"); + selectPort(); + } else { + setTimeout(sendOpenSequence, 500); + } + } else { + closePort(); + } + } else { + if (selectedMode) { + emit("** opening port: "+selectedPort.Name); + socket.send('open '+selectedPort.Name+' 115200 '+selectedMode); + senderPortClose.disabled = false; + setTimeout(sendOpenSequence, 500); + } + } + } + } + + // UI select trigger + function selectPort() { + var value = senderSelectPort[senderSelectPort.selectedIndex].value, + newport = ports[value], + algo; + if (value >= 0) { + selectedPort = newport; + // console.log([selectedPort,value]); + switch (newport.BufferAlgorithm) { + case 'grbl': senderSelectMode.selectedIndex = 1; break; + case 'tinyg': senderSelectMode.selectedIndex = 2; break; + default: senderSelectMode.selectedIndex = 0; break; + } + senderPortClose.disabled = !newport.IsOpen; + if (newport.BufferAlgorithm) selectMode(); + } + } + + // UI select trigger + function selectMode() { + if (senderSelectMode.selectedIndex) { + selectedMode = senderSelectMode[senderSelectMode.selectedIndex].value; + } else { + selectedMode = ''; + } + // console.log([senderSelectMode,selectedMode]); + updatePortSettings(); + } + + function updatePortList(portList) { + emit("** received port list"); + + var portsHTML = '', + portModes = []; + + ports = portList; + ports.forEach(function(port, index) { + portsHTML += ''; + }); + + connect.disabled = false; + senderSelectMode.selecteIndex = 0; + senderSelectPort.selecteIndex = 0; + senderSelectMode.disabled = false; + senderSelectPort.disabled = false; + senderSelectPort.innerHTML = portsHTML; + } + + // send immediately bypassing queue + function sendNow(cmd) { + if (!(socket && selectedPort)) return; + socket.send('send '+selectedPort.Name+' '+cmd); + } + + // buffer to mem queue which drains to spjs + function sendToQueue(cmd) { + localQueue.push(cmd); + } + + // spjs ack (wrote) queue entry + function queueAck(data) { + data.forEach(function(el) { + waitingForAck.remove(el.Id); + gcLinesSent.value = gcodeIndex - waitingForAck.length; + }); + } + + // drain from mem queue to spjs + function drainQueue() { + // return if no live socket + if (!(socket && selectedPort)) return; + + // return if paused or empty outbound buffer + if (gcPaused || localQueue.length === 0) return; + + // return if waiting or queued to queue too large + if (waitingForAck.length > 0 || spjsQueueCount > spjsQueueCountMax) { + return; + } + + // blast out next batch of queued to queue + var toolChange = false, + count = 0, + data = [], + line, + next; + + while (count++ < sendBatchMax && localQueue.length > 0) { + next = (nextID++).toString(); + waitingForAck.push(next); + line = localQueue.shift(); + if (line.indexOf("M6") === 0) { + toolChange = true; + // grbl doesn't support M6 so + // just stop sending until unpaused + if (selectedMode === 'grbl') break; + } + data.push({D:line, Id:next}); + // for tinyg pause after M6 sent + if (toolChange) break; + } + + socket.send('sendjson '+o2js({ + P: selectedPort.Name, + Data: data + })); + + if (toolChange) { + emit("** tool change"); + setPause(true); + var alertToolChange = SDB['sender-alert-toolchange']; + if (!alertToolChange && !confirm("tool change. unpause to continue.\nshow this dialog in the future?")) { + SDB['sender-alert-toolchange'] = 'no'; + } + // alert("tool change. click unpause to continue"); + // setPause(false); + } + } + + function setPause(paused) { + if (paused === gcPaused) return; + gcPaused = paused; + senderGcPause.innerHTML = paused ? 'unpause' : 'pause'; + senderGcPause.style.color = paused ? '#800' : '#000'; + if (paused) emit("** program paused. unpause to continue"); + } + + function emit(msg) { + if (!logDiv) return console.log({unable_to_emit: msg}); + if (msg) { + msg = msg.replace('<','<').replace('>','>'); + logBuffer.push(msg); + if (logBuffer.length > 100) logBuffer = logBuffer.slice(1); + } + logDiv.innerHTML = logBuffer.join('
'); + logDiv.scrollTop = logDiv.scrollHeight; + } + + function jog(axis, delta) { + delta = (delta || 1) * (parseFloat(jogInput.value) || 1); + sendNow("G91 G0 " + axis + delta.toString()); + sendNow("G90"); + } + + function softReset() { + sendNow("\x18"); + // for tinyg, we need to flush the buffer and resume + // so that the commands will be processed + if (selectedMode === 'tinyg') sendNow('%~'); + // for grbl, also reset an alarms + if (selectedMode === 'grbl') sendNow('$X'); + } + + function feedHold() { + sendNow("!"); + } + + function feedResume() { + sendNow("~"); + // send unlock if alarm is set + if (selectedMode === 'grbl' && status === 'Alarm') sendNow('$X'); + } + + function runbox() { + if (!bounds) return; + sendNow(["G0X",bounds.min.x,"Y",bounds.min.y].join('')); + sendNow(["G0X",bounds.max.x,"Y",bounds.min.y].join('')); + sendNow(["G0X",bounds.max.x,"Y",bounds.max.y].join('')); + sendNow(["G0X",bounds.min.x,"Y",bounds.max.y].join('')); + sendNow(["G0X0Y0"]); + } + + function hide() { + senderDialog.style.display = 'none'; + } + + function show() { + senderDialog.style.display = 'block'; + senderDialog.style.right = (kiri.api.ui.ctrlRight.offsetWidth + 5) + 'px'; + } + + function toggle() { + if (senderDialog.style.display === 'block') hide(); else show(); + } + + function programStart() { + gcodeIndex = 0; + gcodeAbort = false; + gcLinesSent.value = 0; + setPause(false); + feedResume(); + sendGcode(); + } + + function programPause() { + setPause(!gcPaused); + gcodeAbort = false; + } + + function programAbort() { + gcodeIndex = 0; + gcodeAbort = true; + gcLinesSent.value = 0; + localQueue = []; + setPause(false); + feedHold(); + } +})(); diff --git a/js/kiri-slice.js b/js/kiri-slice.js new file mode 100644 index 00000000..b8f9be17 --- /dev/null +++ b/js/kiri-slice.js @@ -0,0 +1,1202 @@ +/** Copyright 2014-2017 Stewart Allen -- All Rights Reserved */ + +"use strict"; + +var gs_kiri_slice = exports; + +(function() { + + if (!self.kiri) self.kiri = {}; + if (self.kiri.Slice) return; + + var KIRI = self.kiri, + BASE = self.base, + UTIL = BASE.util, + DBUG = BASE.debug, + POLY = BASE.polygons, + SiP = Slice.prototype, + fillArea = POLY.fillArea, + newPoint = BASE.newPoint, + ROUND = UTIL.round, + MIN = Math.min, + MAX = Math.max, + DEG2RAD = Math.PI / 180, + NOKEY = BASE.key.NONE, + outline_colors = [0xffff00, 0x00ffff, 0xff00ff, 0xff0000, 0x00ff00, 0x0000ff, 0xffffff, 0x000000], + trace_color = 0x000000, + sparse_fill_color = 0x333366, + fill_offset_color = 0xeeeeee, + fill_color = 0x333333, + flat_color = 0xff00aa, + bridge_color = 0x00aaff, + solid_outline_color = 0x00dd00; + + KIRI.Top = Top; + KIRI.newTop = newTop; + KIRI.Slice = Slice; + KIRI.newSlice = newSlice; + + /** + * Object encapsulates a z-slice from an object. This code is shared by the + * client and the worker thread. As such, the view layers are ignored in the + * worker code paths. + * + * @param {number} z offset from ground + * @param {THREE.Group} [view] optional view parent object for layers + * @constructor + */ + function Slice(z, view) { + this.z = z; // z-index + this.index = 0; // slice index + this.lines = null; // slice raw (for rendermode/debug only) + this.groups = null; // grouped lines (for rendermode/debug only) + this.up = null; // slice above (linked list) + this.down = null; // slice below (linked list) + this.tops = []; // array of Top objects + this.view = view; // for rendering this slice + // bridge area polygons + this.bridges = null; + // flat area polygons + this.flats = null; + // areas requiring solid fill (from bridge/flats) + this.solids = { + poly: null, + trimmed: null + }; + this.offsets = null; // support clipping offsets + this.supports = null; // external support areas + this.isSolidFill = false; + this.isSparseFill = false; + this.camMode = null; // CAM mode + this.layers = null; + + if (view) this.addLayers(view); + } + + /** + * Represents a top-level (outer) polygon in a slice. Slices may contain + * multiple tops each with nested structures. Top objects contain cached + * and computed objects for quick access for rendering and dependent computations. + * + * @param {Polygon} polygon + * @constructor + */ + function Top(polygon) { + this.poly = polygon; // outline poly + this.traces = null; // array of offset/inset trace polygons + this.inner = null; // array of inner fillable polygon areas (inside last trace) + this.outer = null; // array of outer trace polygons + this.fill_lines = null; // array of solid fill lines + this.fill_sparse = null; // array of sparse fill polygons + this.solids = null; // solid fill regions in otherwise sparse fill (from solids.trimmed) + } + + /** + * return innermost traces under a given top. for FDM, this represents + * the outline shell that the fill touches. + */ + Top.prototype.innerTraces = function() { + var traces = this.traces, + array = []; + if (traces) traces.forEach(function(p) { + if (p.inner) array.appendAll(p.inner); + }); + return array; + }; + + Top.prototype.clone = function(deep) { + var top = new Top(this.poly.clone(deep)); + return top; + }; + + /** ****************************************************************** + * Slice Prototype Functions + ******************************************************************* */ + + /** + * returns a cloned slice the option of a deep clone on the top polys + */ + SiP.clone = function(deep) { + var from = this, + slice = newSlice(from.z, from.view); + from.tops.forEach(function(top) { + slice.addTop(top.poly.clone(deep)); + }); + return slice; + }; + + /** + * create layer objects for client-side rendering + * + * @param {THREE.Group} view + */ + SiP.addLayers = function(view) { + if (this.layers) return; + + // create views client side only + function nl() { return KIRI.newLayer(view) } + + // if to support creation in worker space + this.layers = { + outline: nl(), + trace: nl(), // also cam roughing + bridge: nl(), // also cam finishx + flat: nl(), // also cam finishy + solid: nl(), // also cam finish + fill: nl(), // also cam facing + sparse: nl(), + support: nl() + }; + }; + + /** + * Add a polygon to a slice creating a new top when necessary. + * + * @param {Polygon} poly to merge into a top + */ + SiP.mergeTop = function(poly) { + var scope = this, + tops = scope.tops, + union, i; + for (i=0; i 0) { + last.forEach(function(inner) { + POLY.trace2count(inner, top.inner, fillOffset, 1, 0); + }); + } + }); + }; + + /** + * Runs in client. Generate shell lines in the correct view layer. + * + * @param {number} renderMode + */ + SiP.renderShells = function(renderMode) { + var scope = this, + layers = scope.layers, + layer = layers.trace, + process = KIRI.driver.CAM.process; + + layer.clear(); + if (scope.camMode) { + layers.solid.clear(); // finish + layers.bridge.clear(); // finish x + layers.flat.clear(); // finish y + } + + scope.tops.forEach(function(top) { + switch (scope.camMode) { + case process.FINISH: + layer = layers.solid; + break; + case process.FINISH_X: + layer = layers.bridge; + break; + case process.FINISH_Y: + layer = layers.flat; + break; + default: + layer = layers.trace; + break; + } + if (top.traces) layer.poly(top.traces, trace_color, true, null); + }); + + layer.render(); + if (scope.camMode) { + layers.solid.render(); + layers.bridge.render(); + layers.flat.render(); + } + }; + + /** + * Clear fill cache in preparation for a slice or re-slice of a widget + */ + SiP.invalidateFill = function() { + this.tops.forEach(function(top) { + top.fill_lines = null; + top.fill_sparse = null; + }); + }; + + /** + * Calculate thin-wall sections and fill appropriately + * + * @param {number} minDist + */ + SiP.doThinWallDetection = function(mindist) { + this.tops.forEach(function(top) { + if (top.inner && top.inner.length > 0) { + // using next line2line algo from print lib + } + }); + }; + + /** + * Create an entirely solid layer by filling all top polygons + * with an alternating pattern. + * + * @param {number} linewidth + * @param {number} angle + * @param {number} density + */ + SiP.doSolidLayerFill = function(linewidth, angle, density) { + this.isSolidFill = false; + + if (this.tops.length === 0) return; + if (typeof(angle) != 'number') return; + + var debug = (this.index === DBUG.get('z-index')) && DBUG.get('layer-solid'); + + // effective nozzle_width: adjusted for desired density + var spacing = linewidth * (1 / density); + + this.tops.forEach(function(top) { + if (top.inner && top.inner.length > 0) { + top.fill_lines = fillArea(top.inner, angle, spacing); + } else { + top.fill_lines = null; + } + }); + + this.isSolidFill = true; + }; + + /** + * Runs in client. Generate solid lines in the correct view layer. + */ + SiP.renderSolidFill = function() { + var layer = this.layers.fill, + render; + + layer.clear(); + + this.tops.forEach(function(top) { + if (top.fill_lines) layer.lines(top.fill_lines, fill_color); + }); + + layer.render(); + }; + + /** + * Take output from pluggable sparse infill algorithm and clip to + * the bounds of the top polygons and their inner solid areas. + * + * @param {number} linewidth (nozzle width) + * @param {number} density spacing overlap (0.0 - 1.0) + * @param {number} percent infill 0.0 - 1.0 + * @param {Object} bounds -- TODO calc w/out mesh so it can run in a worker + */ + SiP.doSparseLayerFill = function(linewidth, density, percent, bounds) { + this.isSparseFill = false; + if (this.tops.length === 0 || percent === 0.0 || this.isSolidFill) return; + + var scope = this, + tops = scope.tops, + spacing = linewidth * (1 / density), + clib = self.ClipperLib, + ctyp = clib.ClipType, + ptyp = clib.PolyType, + cfil = clib.PolyFillType, + clip = new clib.Clipper(), + ctre = new clib.PolyTree(), + poly, + polys = [], + lines = [], + line = [], + // callback passed to pluggable infill algorithm + target = { + bounds: function() { return bounds }, + zIndex: function() { return scope.index }, + zValue: function() { return scope.z }, + density: function() { return percent }, + offset: function() { return spacing }, + emit: function(x,y) { line.push(newPoint(x,y,scope.z)) }, + newline: function() { + if (line.length > 0) { + lines.push(line); + line = []; + } + } + }; + + scope.isSparseFill = true; + // this is pluggable + sparseFillHex(target); + // force emit of last line + target.newline(); + + tops.forEach(function(top) { + top.fill_sparse = []; + polys.appendAll(top.inner); + polys.appendAll(top.solids); + }); + + clip.AddPaths(lines, ptyp.ptSubject, false); + clip.AddPaths(POLY.toClipper(polys), ptyp.ptClip, true); + + if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftNonZero, cfil.pftEvenOdd)) { + ctre.m_AllPolys.forEach(function(node) { + poly = POLY.fromClipperNode(node, scope.z); + tops.forEach(function(top) { + // filter out polygons under min length (0.5mm) + if (poly.isInside(top.poly) && poly.perimeter() > 0.5) { + top.fill_sparse.push(poly); + } + }); + }); + } + + }; + + /** + * Runs in client. Generate sparse lines in the correct view layer. + */ + SiP.renderSparseFill = function() { + var layer = this.layers.sparse; + + layer.clear(); + + this.tops.forEach(function(top) { + if (top.fill_sparse) { + top.fill_sparse.forEach(function(poly) { + // todo cull polys with single point before this + if (poly.length > 1) poly.render(layer, sparse_fill_color, false, true); + }); + } + }); + + layer.render(); + }; + + /** + * Find difference between fill inset poly on two adjacent layers. + * Used to calculate bridges, flats and then solid projections. + */ + SiP.doDiff = function(minArea) { + if (!this.down) return; + + var debug = (this.index === DBUG.get('z-index')) && DBUG.get('diff'), + top = this, + bottom = this.down; + + top.bridges = null; + bottom.flats = null; + + var topInner = top.gatherInner([]), + bottomInner = bottom.gatherInner([]), + bridges = [], + flats = []; + + if (debug) DBUG.log(['diff.b4', topInner, bottomInner, DBUG.view]); + POLY.subtract(topInner, bottomInner, bridges, flats, this.z, minArea); + if (debug) DBUG.log(['diff.af', topInner, bottomInner, bridges, flats, DBUG.view]); + + top.bridges = bridges; + bottom.flats = flats; + }; + + /** + * Runs in client. Generate polygon lines in the correct view layer. + */ + SiP.renderDiff = function() { + var scope = this, + layers = scope.layers, + bridgeLayer = layers.bridge, + flatLayer = layers.flat, + bridges = scope.bridges, + flats = scope.flats; + + bridgeLayer.clear(); + flatLayer.clear(); + + if (bridges) bridges.forEach(function (t) { + t.setZ(scope.z); + t.render(bridgeLayer, bridge_color, true); + }); + + if (flats) flats.forEach(function (t) { + t.setZ(scope.z); + t.render(flatLayer, flat_color, true); + }); + + bridgeLayer.render(); + flatLayer.render(); + }; + + /** + * + * + * @param {Polygon[]} polys + */ + SiP.addSolidFills = function(polys) { + this.solids.poly.appendAll(polys); + }; + + /** + * project bottom flats down + */ + SiP.projectFlats = function(count) { + if (this.isSolidFill || !this.down || !this.flats) return; + projectSolid(this, this.flats, count, false, true); + }; + + /** + * project top bridges up + */ + SiP.projectBridges = function(count) { + if (this.isSolidFill || !this.up || !this.bridges) return; + projectSolid(this, this.bridges, count, true, true); + }; + + /** + * fill projected areas and store line data + * @return {boolean} true if filled, false if not + */ + SiP.doSolidsFill = function(linewidth, angle, density, minArea) { + + var minarea = minArea || 1, + scope = this, + tops = scope.tops, + solids = scope.solids, + unioned = POLY.union(solids.poly), + debug = (this.index === DBUG.get('z-index') && DBUG.get('solid-fills')); + + if (solids.length === 0) return false; + if (unioned.length === 0) return false; + + var masks, + trims = [], + inner = scope.gatherInner([]); + + // trim each solid to the inner bounds + unioned.forEach(function(p) { + p.setZ(scope.z); + inner.forEach(function(i) { + if (p.del) return; + masks = p.mask(i); + if (masks && masks.length > 0) { + p.del = true; + trims.appendAll(masks); + } + }); + }); + + // then merge the resulting solids + solids.unioned = unioned; + solids.trimmed = trims; + + if (debug) DBUG.log(['project', this.index, polys.length, unioned.length, inner.length]); + + // effective nozzle_width: adjusted for desired density + var spacing = linewidth * (1 / density); + + // clear old solids and make array for new + tops.forEach(function(top) { top.solids = [] }); + + // parent each solid polygon inside the smallest bounding top + trims.forEach(function(solid) { + tops.forEach(function(top) { + if (top.poly.overlaps(solid)) { + if (!solid.parent || solid.parent.area() > top.poly.area()) { + if (solid.areaDeep() < minarea) { + // console.log({cull_solid:solid,area:solid.areaDeep()}); + return; + } + solid.parent = top.poly; + top.solids.push(solid); + } + } + }); + }); + + // create empty filled line array for each top + tops.forEach(function(top) { + top.fill_lines = []; + var tofill = [], + angfill = []; + trims.forEach(function(solid) { + if (solid.parent === top.poly) { + if (solid.fillang) { + angfill.push(solid); + } else { + tofill.push(solid); + } + } + }); + if (tofill.length > 0) { + fillArea(tofill, angle, spacing, top.fill_lines); + top.fill_lines_norm = {angle:angle,spacing:spacing}; + } + if (angfill.length > 0) { + top.fill_lines_ang = {spacing:spacing,list:[],poly:[]}; + angfill.forEach(function(af) { + fillArea([af], af.fillang.angle+90, spacing, top.fill_lines); + top.fill_lines_ang.list.push(af.fillang.angle+90); + top.fill_lines_ang.poly.push(af.clone()); + }); + } + }); + + return true; + }; + + SiP.renderSolidOutlines = function() { + var layer = this.layers.solid, + trimmed = this.solids.trimmed; + + layer.clear(); + + if (trimmed) trimmed.forEach(function(poly) { + poly.render(layer, solid_outline_color, true); + }); + + layer.render(); + }; + + /** + * calculate external overhangs requiring support + * this is done bottom-up + * + * @param {number} minOffset trigger for unsupported distance + * @param {number} maxBridge max length before mid supports added + * @param {number} expand outer support clip + * @param {number} offset inner support clip + */ + SiP.doSupport = function(minOffset, maxBridge, expand, minArea, pillarSize, offset) { + var min = minArea || 0.1, + size = (pillarSize || 2), + mergeDist = size * 3, // pillar merge dist + top = this, + tops = top.gatherTopPolys([]), + trimTo = tops; + + // creates outer clip offset from tops + if (expand) POLY.expand(tops, expand, top.z, trimTo = []); + + // create inner clip offset from tops + POLY.expand(tops, offset, top.z, top.offsets = []); + + // skip support detection for bottom layer + if (!top.down) return; + + var traces = POLY.flatten(top.gatherTraces([])), + fill = top.gatherFillLines([]), + points = [], + down = top.down, + down_tops = down.gatherTopPolys([]), + down_traces = POLY.flatten(down.gatherTraces([])); + + // check if point is supported by layer below + function checkSupported(point) { + // skip points close to other support points + for (var i=0; i= maxBridge) { + var slope = p1.slopeTo(p2).factor(1/dist), + segs = Math.floor(dist / maxBridge) + 1, + seglen = dist / segs; + while (i < segs) { + checkSupported(p1.projectOnSlope(slope, i++ * seglen)); + } + } + if (poly) checkSupported(p2); + } + + // check trace line support needs + traces.forEach(function(trace) { + trace.forEachSegment(function(p1, p2) { checkLine(p1, p2, true) }); + }); + + var supports = []; + + // add offset solids to supports (or fill depending) + fill.forEachPair(function(p1,p2) { checkLine(p1, p2, false) }); + // if (top.bridges) POLY.expand(top.bridges, -maxBridge/2, top.z, supports, 1); + + // skip the rest if no points or supports + if (!(points.length || supports.length)) return; + + var pillars = []; + + // TODO project points down instead of unioned pillars + // TODO merge point/rect into hull of next nearest (up to maxBridge/2 away) + // TODO eliminate unions in favor of progress hulling (using previous w/nearness) + // TODO align pillar diamond along line (when doing line checks) + + // for each point, create a bounding rectangle + points.forEach(function(point) { + pillars.push(BASE.newPolygon().centerRectangle(point, size/2, size/2)); + }); + + // merge pillars and replace with convex hull of outer points (aka smoothing) + pillars = POLY.union(pillars).forEach(function(pillar) { + supports.push(BASE.newPolygon().createConvexHull(pillar.points)); + }); + + // return top.supports = supports; + // then union supports + supports = POLY.union(supports); + + // constrain support poly to top polys + supports = POLY.trimTo(supports, trimTo); + + var depth = 0; + while (down && supports.length > 0) { + down.supports = down.supports || []; + + var trimmed = [], + culled = []; + + // clip supports to shell offsets + POLY.subtract(supports, down.gatherTopPolys([]), trimmed, null, top.z, min); + + // set depth hint on support polys for infill density + trimmed.forEach(function(trim) { + if (trim.area() < 0.1) 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 > 0) down.supports.appendAll(culled); + + supports = culled; + down = down.down; + depth++; + } + + }; + + /** + * @param {number} linewidth + * @param {number} angle + * @param {number} density + * @param {number} offset + */ + SiP.doSupportFill = function(linewidth, density, minArea) { + // return; + var slice = this, + supports = slice.supports, + nsB = [], + nsC = [], + min = minArea || 0.1; + + // create support clip offset + // POLY.expand(slice.gatherTopPolys([]), offset, slice.z, slice.offsets = []); + + if (!supports) return; + + // union supports + supports = POLY.union(supports); + + // trim to clip offsets + POLY.subtract(supports, slice.offsets, nsB, null, slice.z, min); + supports = nsB; + + // also trim to lower offsets, if they exist + if (slice.down) { + POLY.subtract(nsB, slice.down.offsets, nsC, null, slice.z, min); + supports = nsC; + } + + if (supports) supports.forEach(function (poly) { + // angle based on width/height ratio + var angle = (poly.bounds.width() / poly.bounds.height() > 1) ? 90 : 0, + // calculate fill density + spacing = linewidth * (1 / density), + offsets = []; + // offset support poly for fill lines + POLY.trace2count(poly, offsets, linewidth/4, 1, 0); + // do the fill + if (offsets.length > 0) fillArea(offsets, angle, spacing, poly.fills = []); + return true; + }); + + // re-assign new supports back to slice + slice.supports = supports; + }; + + /** + * + */ + SiP.renderSupport = function() { + var slice = this, + layer = slice.layers.support, + supports = slice.supports; + + layer.clear(); + + if (supports) supports.forEach(function(poly) { + layer.poly(poly, 0xff0000, true); + layer.lines(poly.fills, 0xff0000); + }); + + layer.render(); + }; + + /** + * for printing output optimization + * calls down to the outermost shell in this slice + * + * @param {Point} target + * @return {Object} + */ + SiP.findClosestPointTo = function(target) { + var min, find; + + this.tops.forEach(function(top) { + find = top.poly.findClosestPointTo(target); + if (!min || find.distance < min.distance) { + min = find; + } + }); + + return min; + }; + + /** ****************************************************************** + * Connect to kiri and Helpers + ******************************************************************* */ + + /** + * + * @param {Point} p1 + * @param {Point} p2 + * @param {Polygon | Polygon[]} poly + * @param {number} [minDist2] square of min distance + * @returns {?Point} + */ + function lineCrossesPoly(p1, p2, poly, minDist2) { + var ip; + if (Array.isArray(poly)) { + for (var i=0; i 0) { + if (up) projectSolid(slice.up, polys, count-1, true, false); + else projectSolid(slice.down, polys, count-1, false, false); + } + } + + /** + * + * @param layer + * @param poly + * @param colors + * @param idx + * @param {boolean} [recurse] + * @param {boolean} [open] + * @returns {THREE.Line} + */ + function renderPolygon(layer, poly, colors, idx, recurse, open) { + layer.poly(poly, colors[idx % colors.length], recurse, open); + if (recurse && poly.inner) { + poly.inner.forEach(function(inner) { + renderPolygon(layer, inner, colors, idx + 1, false, open); + }); + } + } + + /** + * emitter creates a hex infill pattern and sends to target + * + * @param {Object} target + */ + function sparseFillHex(target) { + // compute segment lengths (vert/horiz and 45) + var spacing = target.offset(), + vhlen = (1 - target.density()) * 4 + spacing, + anxlen = ROUND(Math.cos(30 * DEG2RAD) * vhlen, 7), + anylen = ROUND(Math.sin(30 * DEG2RAD) * vhlen, 7), + bounds = target.bounds(), + even = true, + evenZ = target.zIndex() % 2 === 0, + x, y; + + if (evenZ) { + x = bounds.min.x; + for (;;) { + if (even && x > bounds.max.x) break; + if (!even && x > bounds.max.x + anxlen + spacing) break; + y = bounds.min.y; + target.newline(); + while (y < bounds.max.y) { + target.emit(x,y); + y += vhlen; + target.emit(x,y); + if (even) x += anxlen; else x -= anxlen; + y += anylen; + target.emit(x,y); + y += vhlen; + target.emit(x,y); + if (even) x -= anxlen; else x += anxlen; + y += anylen; + } + x += spacing; + if (even) x += (anxlen * 2); + even = !even; + target.newline(); + } + } else { + y = bounds.min.y + vhlen; + for (;;) { + if (even && y > bounds.max.y) break; + if (!even && y > bounds.max.y + anylen) break; + x = bounds.min.x; + target.newline(); + while (x < bounds.max.x) { + target.emit(x,y); + if (even) y += anylen; else y -= anylen; + x += anxlen; + target.emit(x,y); + x += spacing; + target.emit(x,y); + if (even) y -= anylen; else y += anylen; + x += anxlen; + target.emit(x,y); + x += spacing; + } + y += vhlen; + if (even) y += (anylen * 2); + even = !even; + target.newline(); + } + } + } + + function newTop(poly) { + return new Top(poly); + } + + /** + * @param {number} z + * @param {THREE.Group} view + * @returns {Slice} + */ + function newSlice(z, view) { + return new Slice(z, view); + } + +})(); diff --git a/js/kiri-slicer.js b/js/kiri-slicer.js new file mode 100644 index 00000000..7dab0e86 --- /dev/null +++ b/js/kiri-slicer.js @@ -0,0 +1,781 @@ +/** Copyright 2014-2017 Stewart Allen -- All Rights Reserved */ + +"use strict"; + +var gs_kiri_slicer = exports; + +(function() { + + if (!self.kiri) self.kiri = {}; + if (self.kiri.slicer) return; + + var slicer = self.kiri.slicer = { + slice: slice, + sliceWidget: sliceWidget + }; + + var KIRI = self.kiri, + BASE = self.base, + CONF = BASE.config, + UTIL = BASE.util, + POLY = BASE.polygons, + time = UTIL.time, + newSlice = KIRI.newSlice, + newOrderedLine = BASE.newOrderedLine; + + /** + * Convenience method. Gets a Widget's points and calls slice() + * + * @param {Widget} widget + * @param {Object} options + * @param {Function} ondone callback when slicing complete + * @param {Function} onupdate callback on incremental updates + */ + function sliceWidget(widget, options, ondone, onupdate) { + slice(widget.getPoints(), widget.getBoundingBox(), options, ondone, onupdate); + } + + /** + * Given an array of points as triples, a bounding box and a set of + * slicing controls, emit an array of Slice objects to the ondone() + * function. onupdate() will be called with two parameters (% completion + * and an optional message) so that the UI can report progress to the user. + * + * @param {Array} points vertex array + * @param {Bounds} bounds bounding box for points + * @param {Object} options slicing parameters + * @param {Function} ondone callback when slicing done + * @param {Function} onupdate callback to report slicing progress + */ + function slice(points, bounds, options, ondone, onupdate) { + var topoMode = options.topo, + ox = 0, + oy = 0; + + // handle rotating meshes for CAM finishing. + // slicer expects things just so, so we alter + // geometry to satisfy + if (options.swapX || options.swapY) { + points = points.slice(); + + var btmp = new THREE.Box3(), + pref = {}, + cached; + + btmp.setFromPoints(points); + if (options.swapX) ox = -btmp.max.x; + if (options.swapY) oy = -btmp.max.y; + + // array re-uses points so we need + // to be careful not to alter a point + // more than once + for (var p, index=0; index bounds.min.z) { + var zkey = p1.z < bounds.max.z ? p1.z + 0.001 : p1.z, + area = Math.abs(UTIL.area2(p1,p2,p3))/2; + if (!zFlat[zkey]) { + zFlat[zkey] = area; + } else { + zFlat[zkey] += area; + } + } + } + } + + /** bucket polygons into z-bounded groups */ + var bucketCount = Math.max(1, Math.ceil(zMax / (zSum / points.length)) - 1); + + zScale = 1 / (zMax / bucketCount); + + if (bucketCount > 1) { + // create empty buckets + for (i = 0; i < bucketCount + 1; i++) buckets.push([]); + + // copy triples into all matching z-buckets + for (i = 0; i < points.length;) { + p1 = points[i++]; + p2 = points[i++]; + p3 = points[i++]; + var zm = Math.min(p1.z, p2.z, p3.z), + zM = Math.max(p1.z, p2.z, p3.z), + bm = Math.floor(zm * zScale), + bM = Math.ceil(zM * zScale); + for (j = bm; j < bM; j++) { + buckets[j].push(p1); + buckets[j].push(p2); + buckets[j].push(p3); + } + } + } + + // create zIndexes array (0 = auto-detect) + if (zInc === 0) { + // find unique z-index offsets for slicing + var key, zl = []; + for (key in zList) { + if (!zList.hasOwnProperty(key)) continue; + zl.push(parseFloat(key)); + } + zl.sort(function(a,b) { return a - b}); + for (i = 0; i < zl.length-1; i++) { + zIndexes.push((zl[i] + zl[i+1]) / 2); + } + zIndexes.sort(function(a,b) { return a - b}); + } else if (options.cam) { + // re-divide slice height so that top and + // bottom slices fall exactly on those faces + zInc = (zMax - zMin) / (Math.floor(zMax / zInc) + 1); + for (i = zMin; i < zMax; i += zInc) { + zIndexes.push(i); + } + for (key in zFlat) { + // todo make threshold for flat detection configurable + if (!zFlat.hasOwnProperty(key) || zFlat[key] < 100) continue; + key = parseFloat(key); + if (!zIndexes.contains(key) && key >= zMin) zIndexes.push(key); + } + // sort top down + zIndexes.sort(function(a,b) { + return b-a; + }); + } else { + // FDM is the simplest case. Just offset and slice at + // predicable offsets. First offset is half a slice height. + // this was once configurable, but that turned out not to be + // terribly useful. In future, first layer height may be + // adjusted separately, but that would also require other + // settings changes (flow rate, etc). + // ... only this one special case for first layer :) + if (options.firstHeight) { + zIndexes.push(options.firstHeight / 2); + zMin = options.firstHeight; + } + for (i = zMin + zOff; i < zMax; i += zInc) { + zIndexes.push(i); + } + } + + // create a Slice for each z offset in the zIndexes array + for (var i = 0; i < zIndexes.length; i++) { + zPos = zIndexes[i]; + sliceZ(zPos); + // kill slicing if onupdate() returns 42 + if (onupdate(i / zIndexes.length) === 42) { + return ondone(null); + } + } + + // for cam, mark top and bottom as mandatory (hasFlats) + if (options.cam && slices.length > 0) { + slices[0].hasFlats = true; + slices[slices.length-1].hasFlats = true; + } + + // connect slices into linked list for island/bridge projections + for (i=1; i 0; + + // for topo slices, we just need the raw lines + if (!topoMode) { + slice.groups = connectLines(slice.lines, slices.length); + POLY.nest(slice.groups).forEach(function(top) { slice.addTop(top) }); + } + + // fixup un-rotates polygons for CAM + if (options.swapX || options.swapY) { + var move = {x:ox, y:oy, z:0}; + slice.camMode = options.swapX ? CPRO.FINISH_X : CPRO.FINISH_Y; + if (topoMode) { + var lines = slice.lines, llen = lines.length, idx, line; + // shared points causing problems + for (idx=0; idx 10000) { + DBUG.log("excessive path options @ "+paths.length+" #"+input.length); + return; + } + for (;;) { + stack.push(point); + + var last = point, + links = point.group; + + path.push(point); + // use del to mark traversed path + point.del = true; + // set so point isn't used in another polygon search + point.pos = search++; + // seed path with two points to prevent redundant opposing seeks + if (path.length === 1) { + from = point; + point = links[0]; + continue; + } + + if (links.length > 2) { + // TODO optimize when > 2 and limit to left-most and right-most branches + // for now, pursue all possible branches + links.forEach(function(nextp) { + // do not backtrack + if (nextp === from) { + return; + } + if (nextp.del) { + paths.push(sliceAtTerm(path,nextp)); + } else { + findPathsMinRecurse(nextp, path.slice(), paths, point); + } + }); + break; + } else { + point = links[0] === from ? links[1] : links[0]; + from = last; + // hit an open end + if (!point) { + path.open = true; + paths.push(path); + break; + } + // hit a point previously in the path (or start) + if (point.del) { + paths.push(sliceAtTerm(path,point)); + break; + } + } + } + + for (var i=0; i 2) output.push(poly.clean()); + // if (poly.length > 2) output.push(poly); + } + + // given an array of paths, emit longest to shortest + // eliminating points from the paths as they are emitted + // shorter paths any point eliminated are eliminated as candidates. + function emitLongestAsPolygon(paths) { + var longest = null, + emitted = 0, + closed = 0, + open = 0; + + paths.forEach(function(path, index) { + // use longest perimeter vs longest path? + if (!longest || path.length > longest.length) longest = path; + if (!path.open) closed++; else open++; + }); + + if (debug) DBUG.log({closed:closed, open:open}); + // it gets more complicated with multiple possible output paths + if (closed > 1 && open === 0) { + // add polygon to path (for area sorting) + paths.forEach(function(path) { path.poly = BASE.newPolygon().addPoints(path) }); + + // sort descending by area VS (length below -- better in most cases) + // paths.sort(function(a,b) { return b.poly.area() - a.poly.area() }); + + // sort descending by length + paths.sort(function(a,b) { return b.poly.length - a.poly.length }); + + if (debug) DBUG.log({paths:paths}); + + // emit polygons largest to smallest + // omit polygon if it intersects previously emitted (has del points) + paths.forEach(function(path) { + if (path.length < 3) return; + var len = path.length, i; + for (i = 0; i < len; i++) if (path[i].del) return; + for (i = 0; i < len; i++) path[i].del = true; + emit(path.poly); + emitted++; + }); + + if (debug) DBUG.log({longest:longest.length, paths:paths.length, closed:closed, emitted:emitted}); + } else { + if (longest.open) { + connect.push(longest); + } else { + emit(BASE.newPolygon().addPoints(longest)); + } + } + } + + // create point map, unique point list and point group arrays + input.forEach(function(line) { + p1 = cachedPoint(line.p1.round(7)); + p2 = cachedPoint(line.p2.round(7)); + addConnected(p1,p2); + addConnected(p2,p1); + }); + + // first trace paths starting at dangling endpoinds (bad polygon soup) + points.forEach(function(point) { + // must not have been used and be a dangling end + if (point.pos === 0 && point.group.length === 1) { + var path = [], + paths = []; + findPathsMinRecurse(point, path, paths); + if (paths.length > 0) emitLongestAsPolygon(paths); + } + }); + + // for each point, find longest path back to self + points.forEach(function(point) { + // must not have been used or be at a split + if (point.pos === 0 && point.group.length === 2) { + var path = [], + paths = []; + findPathsMinRecurse(point, path, paths); + if (paths.length > 0) emitLongestAsPolygon(paths); + } + }); + + // return true if points are deemed "close enough" close a polygon + function close(p1,p2) { + return p1.distToSq2D(p2) <= 0.01; + } + + // reconnect dangling/open polygons to closest endpoint + for (var i=0; i -- all rights reserved" +}; + +(function() { + + if (!self.kiri) self.kiri = {}; + if (self.kiri.Widget) return; + + var KIRI = self.kiri, + DRIVERS = KIRI.driver, + CAM = DRIVERS.CAM, + FDM = DRIVERS.FDM, + LASER = DRIVERS.LASER, + CPRO = CAM.process, + BASE = self.base, + CONF = BASE.config, + DBUG = BASE.debug, + UTIL = BASE.util, + POLY = BASE.polygons, + MATH = Math, + ABS = MATH.abs, + MIN = MATH.min, + MAX = MATH.max, + SQRT = MATH.sqrt, + CEIL = MATH.ceil, + FLOOR = MATH.floor, + ROUND = MATH.round, + SLICER = KIRI.slicer, + newLine = BASE.newLine, + newPoint = BASE.newPoint, + newSlice = KIRI.newSlice, + newPolygon = BASE.newPolygon, + newOrderedLine = BASE.newOrderedLine, + time = UTIL.time, + WP = Widget.prototype, + solid_opacity = 1.0, + nextId = 0; + + KIRI.Widget = Widget; + KIRI.newWidget = newWidget; + + function newWidget(id) { return new Widget(id) } + + /** ****************************************************************** + * Constructor + ******************************************************************* */ + + /** + * @params {String} [id] + * @constructor + */ + function Widget(id) { + this.id = id || new Date().getTime().toString(36)+(nextId++); + this.mesh = null; + this.points = null; + // todo resolve use of this vs. mesh.bounds + this.bounds = null; + this.wire = null; + this.topo = null; + this.slices = null; + this.settings = null; + this.modified = true; + this.orient = { + scale: { + x: 1.0, + y: 1.0, + z: 1.0 + }, + rot: { + x: 0, + y: 0, + z: 0 + }, + pos: { + x: 0, + y: 0, + z: 0 + }, + mirror: false + }, + this.stats = { + slice_time: 0, + load_time: 0, + progress: 0 + }; + this.saved = false; + // cancel slice/print ops + this.cancel = false; + } + + /** ****************************************************************** + * Widget Class Functions + ******************************************************************* */ + + Widget.loadFromCatalog = function(filename, ondone) { + KIRI.catalog.getFile(filename, function(data) { + ondone(newWidget().loadVertices(data)); + }); + }; + + Widget.loadFromState = function(id, ondone, move) { + var widget = newWidget(); + widget.id = id; + widget.saved = time(); + KIRI.odb.get('ws-save-'+id, function(data) { + if (data) { + var vertices = data.geo || data, + orient = data.orient || null; + ondone(widget.loadVertices(vertices)); + // restore widget position if specified + if (move && orient && orient.pos) { + widget.orient = orient; + widget.move(orient.pos.x, orient.pos.y, orient.pos.z, true); + } + } else { + ondone(null); + } + }); + }; + + Widget.deleteFromState = function(id,ondone) { + KIRI.odb.remove('ws-save-'+id, ondone); + }; + + /** + * converts a geometry point array into a kiri point array + * with auto-decimation + * + * @param {Float32Array} array + * @param {boolean} [decimate] + * @returns {Array} + */ + Widget.verticesToPoints = function(array,decimate) { + var parr = new Array(array.length / 3), + i = 0, + j = 0, + t = time(), + hash = {}, + unique = 0, + passes = 0, + points, + oldpoints = parr.length, + newpoints; + // replace point objects with their equivalents + while (i < array.length) { + var p = newPoint(array[i++], array[i++], array[i++]), + k = p.key, + m = hash[k]; + if (!m) { + m = p; + hash[k] = p; + unique++; + } + parr[j++] = m; + } + // decimate until all point spacing > precision_decimate + while (parr.length > BASE.config.decimate_threshold && decimate && BASE.config.precision_decimate > 0.0) { + var lines = [], line, dec = 0; + for (i=0; i= BASE.config.precision_decimate) break; + if (line.p1.op || line.p2.op) continue; + // todo skip dropping lines where either point is a "sharp" on 3 vectors + line.p1.op = line.p2.op = line.p1.midPointTo3D(line.p2); + dec++; + } + // exit if nothing to decimate + if (dec === 0) break; + passes++; + // create new facets + points = new Array(oldpoints); + newpoints = 0; + for (i=0; i 0 || host === "localhost" ? + proto + "//" + host + ":" + port : + "", + time = function() { return new Date().getTime() }, + KIRI = self.kiri, + BASE = self.base, + seqid = 1, + running = {}, + worker = new Worker(pre + "/code/worker.js/" + exports.VERSION); + + // new moto.Ajax(function(body) { + // console.log({body:body}); + // var blob = new Blob([body], {type : 'application/json'}); + // worker = new Worker(URL.createObjectURL(blob)); + // }).request(pre + "/code/worker.js/"); + + worker.onmessage = function(e) { + var now = time(), + reply = e.data, + record = running[reply.seq], + onreply = record.fn; + + if (reply.done) delete running[reply.seq]; + + // calculate and replace recv time + reply.time_recv = now - reply.time_recv; + + onreply(reply.data, reply); + }; + + function send(fn, data, onreply, async, zerocopy) { + var seq = seqid++; + + running[seq] = {fn:onreply, async:async||false}; + + worker.postMessage({ + seq: seq, + task: fn, + time: time(), + data: data + }, zerocopy); + } + + KIRI.work = { + decimate : function(vertices, callback) { + var vertices = vertices.buffer.slice(0); + send("decimate", vertices, function(output) { + callback(output); + }); + }, + + clear : function(widget) { + send("clear", widget ? {id:widget.id} : {}, function(reply) { + // console.log({clear:reply}); + }); + }, + + cancel : function(widget) { + send("cancel", widget ? {id:widget.id} : {}); + }, + + slice : function(settings, widget, callback) { + var vertices = widget.getGeoVertices().buffer.slice(0); + send("slice", { + id: widget.id, + settings: settings, + vertices: vertices, + position: widget.mesh.position + }, function(reply) { + callback(reply); + }, null, [vertices]); + }, + + printSetup : function(settings, callback) { + send("printSetup", {settings:settings}, function(reply) { + callback(reply); + }); + }, + + printGCode : function(callback) { + var gcode = [], + start = BASE.util.time(); + send("printGCode", {}, function(reply) { + if (reply.line) { + gcode.push(reply.line); + } else { + if (!reply.gcode) reply.gcode = gcode.join("\n"); + // console.log({printGCode:(BASE.util.time() - start)}); + callback(reply); + } + }); + }, + + sliceToGCode : function(settings, vertices, callback) { + var wid = new Date().toString(36); + var vertices = widget.getGeoVertices().buffer.slice(0); + send("slice", {settings:settings, id:wiwd, vertices:vertices, position:{x:0,y:0,z:0}}, function(reply) { + send("printSetup", {settings:settings}, function(reply) { + send("printGCode", {}, function(reply) { + callback(reply); + }); + }, null, [vertices]); + callback(reply); + }); + } + }; + +} else { + module = { exports: {} }; + + var loc = self.location, + host = loc.hostname, + ver = exports.VERSION, + time = function() { return new Date().getTime() }; + + console.log("kiri | init work | " + ver); + + // when running the server in 'web' mode, the obfuscated code is served as a + // unified ball via /code/work.js -- otherwise, map "localhost" to "debug" + // to debug unobfuscated code. if not, /js/ paths will 404. + if (host !== 'grid.space' && host !== 'debug') { + [ + "license","ext-n3d","ext-clip","add-array", + "add-three","geo","geo-point","geo-debug","geo-bounds", + "geo-line","geo-slope","geo-polygon","geo-polygons", + "kiri-slice","kiri-slicer","kiri-driver-fdm","kiri-driver-cam", + "kiri-driver-laser","kiri-widget","kiri-pack","kiri-print","kiri-codec" + ].forEach(function(scr) { + importScripts(["/js/",scr,".js","/v"+ver].join('')); + }) + } else { + importScripts("/code/work.js/"+ver); + base.debug.disable(); + } + + var base = self.base, + moto = self.moto, + dbug = base.debug, + util = base.util, + kiri = self.kiri, + Widget = kiri.Widget, + currentPrint, + cache = {}; + + var dispatch = { + decimate: function(vertices, send) { + vertices = new Float32Array(vertices), + vertices = Widget.pointsToVertices(Widget.verticesToPoints(vertices, true)); + send.done(vertices); + }, + + cancel: function(data) { + if (data.id) { + var widget = cache[data.id]; + if (widget) widget.cancel = true; + } else { + for (var id in cache) { + if (cache.hasOwnProperty(id)) cache[id].cancel = true; + } + } + }, + + slice: function(data, send) { + var settings = data.settings, + vertices = new Float32Array(data.vertices), + position = data.position, + points = Widget.verticesToPoints(vertices); + + send.data({update:0.05, updateStatus:"slicing"}); + + var widget = kiri.newWidget(data.id).setPoints(points), + last = util.time(), + now; + + // clear previous widget data before reslice + delete cache[data.id]; + + // fake mesh object to satisfy printing + widget.mesh = { + widget: widget, + position: position + }; + + widget.slice(settings, function() { + send.data({send_start:time()}); + send.data({ + topo: settings.synth.sendTopo ? widget.topo : null, + stats: widget.stats, + slices: widget.slices.length + }); + widget.slices.forEach(function(slice,index) { + send.data({index:index, slice:slice.encode()}); + }) + send.data({send_end:time()}); + send.done({done:true}); + // cache results for future printing + cache[data.id] = widget; + }, function(update, msg) { + now = util.time(); + if (now - last < 10 && update < 0.99) return; + // on update + send.data({update:(0.05 + update * 0.95), updateStatus:msg}); + last = now; + }); + }, + + printSetup: function(data, send) { + var widgets = [], key; + for (key in cache) { + if (cache.hasOwnProperty(key)) widgets.push(cache[key]); + } + + send.data({update:0.05, updateStatus:"preview"}); + + currentPrint = kiri.newPrint(data.settings, widgets, data.id); + currentPrint.setup(false, function(update, msg) { + send.data({ + update: update, + updateStatus: msg + }); + }, function() { + send.done({ + done: true, + output: currentPrint.encodeOutput() + }); + }); + }, + + printGCode: function(data, send) { + currentPrint.exportGCode(false, function(gcode) { + send.done({ + gcode: gcode, + lines: currentPrint.lines, + bytes: currentPrint.bytes, + bounds: currentPrint.bounds, + distance: currentPrint.distance, + time: currentPrint.time + }); + }, function(line) { + send.data({line:line}); + }); + }, + + clear: function(data, send) { + if (!data.id) { + cache = {}; + currentPrint = null; + send.done({ clear: true }); + return; + } + var had = cache[data.id] !== undefined; + delete cache[data.id]; + send.done({ + id: data.id, + had: had, + has: cache[data.id] !== undefined + }); + }, + }; + + self.onmessage = function(e) { + var time_recv = util.time(), + msg = e.data, + run = dispatch[msg.task], + send = { + data : function(data,direct) { + self.postMessage({ + seq: msg.seq, + task: msg.task, + done: false, + data: data + },direct); + }, + done : function(data,direct) { + self.postMessage({ + seq: msg.seq, + task: msg.task, + done: true, + data: data + },direct); + } + }; + + if (run) { + var time_xfer = (time_recv - msg.time), + output = run(msg.data, send), + time_send = util.time(), + time_proc = time_send - time_recv; + + if (output) self.postMessage({ + seq: msg.seq, + task: msg.task, + time_send: time_xfer, + time_proc: time_proc, + // replaced on reply side + time_recv: util.time(), + data: output + }); + } else { + console.log({kiri_msg:e}); + } + }; + + // catch clipper alerts and convert to console messages + self.alert = function(o) { + console.log(o); + }; + +} diff --git a/js/kiri.js b/js/kiri.js new file mode 100644 index 00000000..7148df5f --- /dev/null +++ b/js/kiri.js @@ -0,0 +1,3291 @@ +/** + * Copyright 2014-2017 Stewart Allen -- All Rights Reserved + */ +"use strict"; + +self.kiri = (self.kiri || {}); +self.kiri.version = exports.VERSION; +self.kiri.copyright = exports.COPYRIGHT; +self.kiri.license = exports.LICENSE; + +(function () { + if (kiri.init) return; + + var iOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent), + // --------------- + MODES = { + FDM: 1, // fused deposition modeling (also FFF) + LASER: 2, // laser cutting + CAM: 3 // 3 axis milling/machining + }, + VIEWS = { + ARRANGE: 1, + SLICE: 2, + PREVIEW: 3 + }, + // --------------- settings filters + sf = { + fdm:{ + d:{ + bedWidth: 1, + bedDepth: 1, + bedHeight: 1, + maxHeight: 1, + filamentSize: 1, + nozzleSize: 1, + gcodePre: 1, + gcodePost: 1, + gcodeFan: 1, + gcodeTrack: 1 + }, + p:{ + processName: 1, + sliceHeight: 1, + sliceShells: 1, + sliceShellSpacing: 1, + sliceFillAngle: 1, + sliceFillOverlap: 1, + sliceFillDensity: 1, + sliceFillSparse: 1, + sliceSupportEnable: 1, + sliceSupportDensity: 1, + sliceSupportOffset: 1, + sliceSupportSize: 1, + sliceSupportArea: 1, + sliceSupportExtra: 1, + sliceSupportSpan: 1, + sliceSolidMinArea: 1, + sliceSolidLayers: 1, + sliceBottomLayers: 1, + sliceTopLayers: 1, + sliceVase: 1, + firstLayerHeight: 1, + firstLayerSpeed: 1, + outputTemp: 1, + outputFanMax: 1, + outputBedTemp: 1, + outputFeedrate: 1, + outputSeekrate: 1, + outputShellMult: 1, + // outputShellSpeed: 1, + outputFillMult: 1, + outputRetractOver: 1, + outputRetractDist: 1, + outputRetractSpeed: 1, + outputBrimCount: 1, + outputBrimOffset: 1, + outputWipeDistance: 1, + outputShortDistance: 1, + outputShortFactor: 1, + outputClockwise: 1, + outputOriginCenter: 1, + outputInvertX: 1, + outputInvertY: 1 + } + }, + cam:{ + d:{ + bedWidth: 1, + bedDepth: 1, + bedHeight: 1, + spindleMax: 1, + gcodePre: 1, + gcodePost: 1, + gcodeDwell: 1, + gcodeChange: 1, + gcodeFExt: 1, + gcodeSpace: 1, + gcodeStrip: 1 + }, + p:{ + processName: 1, + roughingTool: 1, + roughingSpindle: 1, + roughingDown: 1, + roughingOver: 1, + roughingSpeed: 1, + roughingPlunge: 1, + roughingStock: 1, + roughingOn: 1, + finishingTool: 1, + finishingSpindle: 1, + finishingDown: 1, + finishingOver: 1, + finishingAngle: 1, + finishingSpeed: 1, + finishingPlunge: 1, + finishingOn: 1, + finishingXOn: 1, + finishingYOn: 1, + finishCurvesOnly: 1, + drillTool: 1, + drillSpindle: 1, + drillDownSpeed: 1, + drillDown: 1, + drillDwell: 1, + drillLift: 1, + drillingOn: 1, + camTabsWidth: 1, + camTabsHeight: 1, + camTabsOn: 1, + camPocketOnly: 1, + camDepthFirst: 0, + camEaseDown: 1, + camOriginTop: 1, + camTolerance: 1, + camZTopOffset: 1, + camZBottom: 1, + camZClearance: 1, + camStockX: 1, + camStockY: 1, + camStockZ: 1, + outputClockwise: 1, + outputOriginCenter: 1, + outputInvertX: 1, + outputInvertY: 1 + } + }, + laser: { + d:{ + bedWidth: 1, + bedDepth: 1, + }, + p:{ + processName: 1, + laserOffset: 1, + laserSliceHeight: 1, + outputTileSpacing: 1, + outputTileScaling: 1, + outputLaserPower: 1, + outputLaserSpeed: 1, + outputOriginCenter: 1, + outputInvertX: 1, + outputInvertY: 1 + } + } + }, + // --------------- + settings = { + tools:[ + { + id: 1000, + number: 1, + type: "endmill", + name: "end 1/4", + metric: false, + flute_diam: 0.25, + flute_len: 0, + shaft_diam: 0.25, + shaft_len: 0 + }, + { + id: 1001, + number: 2, + type: "endmill", + name: "end 1/8", + metric: false, + flute_diam: 0.125, + flute_len: 0, + shaft_diam: 0.125, + shaft_len: 0 + }, + { + id: 1002, + number: 3, + type: "endmill", + name: "end 1/16", + metric: false, + flute_diam: 0.0625, + flute_len: 0, + shaft_diam: 0.0625, + shaft_len: 0 + } + ], + device:{ + bedWidth: 300, + bedDepth: 175, + bedHeight: 2.5, + maxHeight: 150, // FDM + filamentSize: 1.75, // FDM + nozzleSize: 0.4, // FDM + spindleMax: 0, // CAM + gcodePre: [], // header script + gcodePost: [], // footer script + gcodeFan: "", // FDM fan command + gcodeTrack: "", // FDM progress command + gcodeDwell: [], // CAM dwell script + gcodeChange: [], // CAM tool change script + gcodeFExt: "", // CAM file extension + gcodeSpace: "", // CAM token spacing + gcodeStrip: true // CAM strip comments + }, + process:{ + processName: "default", + + sliceHeight: 0.25, + sliceShells: 2, + sliceShellSpacing: 0.9, + sliceFillAngle: 45, + sliceFillOverlap: 0.5, + sliceFillDensity: 1.0, + sliceFillSparse: 0.5, + + sliceSupportEnable: false, + sliceSupportDensity: 0.25, + sliceSupportOffset: 1.0, + sliceSupportSize: 4, + sliceSupportArea: 1, + sliceSupportExtra: 1, + sliceSupportSpan: 6, + + sliceSolidMinArea: 0.1, + sliceSolidLayers: 2, + sliceBottomLayers: 2, + sliceTopLayers: 2, + sliceVase: false, + + firstLayerHeight: 1.0, // FDM + firstLayerSpeed: 0.5, // FDM + + laserOffset: 0.25, + laserSliceHeight: 1, + + roughingTool: 1000, + roughingSpindle: 1000, + roughingDown: 2, + roughingOver: 0.5, + roughingSpeed: 1000, + roughingPlunge: 250, + roughingStock: 0, + roughingOn: true, + + finishingTool: 1000, + finishingSpindle: 1000, + finishingDown: 3, + finishingOver: 0.5, + finishingAngle: 85, + finishingSpeed: 800, + finishingPlunge: 250, + finishingOn: true, + finishingXOn: true, + finishingYOn: true, + finishCurvesOnly: false, + + drillTool: 1000, + drillSpindle: 1000, + drillDownSpeed: 250, + drillDown: 5, + drillDwell: 250, + drillLift: 2, + drillingOn: false, + + camTabsWidth: 5, + camTabsHeight: 5, + camTabsOn: false, + + camPocketOnly: false, + camDepthFirst: false, + camEaseDown: false, + camOriginTop: true, + camTolerance: 0.15, + camZTopOffset: 0, + camZBottom: 0, + camZClearance: 1, + + camStockX: 0, + camStockY: 0, + camStockZ: 0, + + outputTileSpacing: 1, // LASER + outputTileScaling: 1, // LASER + outputLaserPower: 100, // LASER + outputLaserSpeed: 1000, // LASER + + outputTemp: 230, + outputFanMax: 255, + outputBedTemp: 0, + outputFeedrate: 100, + outputSeekrate: 150, + outputShellMult: 1.0, + // outputShellSpeed: 1.0, + outputFillMult: 1.25, + outputRetractOver: 5.0, + outputRetractDist: 0.5, + outputRetractSpeed: 20, + outputBrimCount: 2, + outputBrimOffset: 3, + outputWipeDistance: 1, + outputShortDistance: 5.0, + outputShortFactor: 0.2, + + outputClockwise: true, + + outputOriginCenter: true, + outputInvertX: false, + outputInvertY: false + }, + // saved processes by name + sproc:{ + FDM: {}, + CAM: {}, + LASER: {} + }, + // current process name + cproc:{ + FDM: null, + CAM: null, + LASER: null + }, + // cached device settings by mode + cdev: { + FDM: null, + CAM: null + }, + // now they're called devices instead of gcode filters + filter:{ + FDM: "Any.Generic.Marlin", + CAM: "Any.Generic.Grbl" + }, + // custom devices by name + devices:{ + }, + layers:{ + layerOutline: true, + layerTrace: true, + layerFacing: true, + layerRough: true, + layerFinish: true, + layerFinishX: true, + layerFinishY: true, + layerDelta: false, + layerSolid: false, + layerSparse: true, + layerFill: true, + layerSupport: true, + layerPrint: false, + layerDebug: false + }, + synth: { + // set in updateSettingsFromFields() + fillOffsetMult: 0, + diffOffsetMult: 0 + }, + controller:{ + view: null, + zoomSpeed: 1.0, + reverseZoom: false + }, + mode: 'FDM' + }, + settingsDefault = settings, + autoDecimate = true, + // --------------- + SELF = self, + MOTO = moto, + KIRI = SELF.kiri, + BASE = SELF.base, + UTIL = BASE.util, + DBUG = BASE.debug, + // --------------- + WIN = SELF.window, + DOC = SELF.document, + LOC = SELF.location, + SETUP = parseOpt(LOC.search.substring(1)), + LOCAL = LOC.host.indexOf('localhost') === 0, + SECURE = isSecure(LOC.protocol), + // --------------- + SDB = MOTO.KV, + ODB = KIRI.odb = new MOTO.Storage(SETUP.d ? SETUP.d[0] : 'kiri'), + SPACE = KIRI.space = MOTO.Space, + WIDGETS = KIRI.widgets = [], + CATALOG = KIRI.catalog = KIRI.openCatalog(ODB,autoDecimate), + STATS = new Stats(SDB), + // ACTIVE = 'kiri-active', + // IDLE = 'kiri-idle', + SEED = 'kiri-seed', + // --------------- + Widget = kiri.Widget, + newWidget = kiri.newWidget, + // --------------- + UI = {}, + UC = MOTO.ui.prefix('kiri').inputAction(updateSettings).hideAction(onControlResize), + DEFMODE = SETUP.dm && SETUP.dm.length === 1 ? SETUP.dm[0] : 'FDM', + STARTMODE = SETUP.sm && SETUP.sm.length === 1 ? SETUP.sm[0] : null, + MODE = MODES.FDM, + onEvent = {}, + currentPrint = null, + selectedMeshes = [], + localFilterKey ='kiri-gcode-filters', + localFilters = js2o(SDB.getItem(localFilterKey)) || [], + OCTOPRINT = null, + // --------------- + wireframe_color = 0x444444, + wireframe_model_opacity = 0.25, + widget_selected_color = 0xbbff00, + widget_deselected_color = 0xffff00, + widget_slicing_color = 0xffaaaa, + widget_cam_preview_color = 0x0055bb, + preview_opacity_cam = 1, + preview_opacity = 0.0, + model_opacity = 1.0, + slicing_opacity = 0.5, + sliced_opacity = 0.0, + sliced_opacity_cam = 0.25, + // --------------- + catalogSize = 0, + showLayerRange = 0, + showLayerValue = 0, + showLayerMax = 0, + renderMode = 4, + viewMode = VIEWS.ARRANGE, + layoutOnAdd = true, + local = SETUP.local, + sliceRemote = !local, + printRemote = !local, + mouseMoved = false, + camStock = null, + camTopZ = 0; + + DBUG.enable(); + DBUG.set('print-move'); + + if (SETUP.dc) DBUG.set('connect'); + if (SETUP.zi) DBUG.set('z-index',parseInt(SETUP.zi[0])); + if (SETUP.rm) renderMode = parseInt(SETUP.rm[0]); + + KIRI.api = { + ui : UI, + on : addOnEvent, + sdb : SDB, + o2js : o2js, + js2o : js2o, + ajax : ajax, + help : showHelp, + load : loadWidget, + focus : takeFocus, + stats : STATS, + import : loadFile, + catalog : CATALOG, + getMode : getMode, + setMode : setMode, + showModal : showModal, + hideModal : hideModal, + showDialog : showDialog, + hideDialog : hideDialog, + showCatalog : showCatalog, + addWidget : platformAdd, + selectAll : platformSelectAll, + selectNone : widgetDeselect, + showProgress : setProgress, + clearWorker : KIRI.work.clear, + getSettings : getSettings, + putSettings : putSettings, + hideImport : function() { UI.import.style.display = 'none' }, + mouse : { + moved : function() { return mouseMoved }, + movedSet : function(b) { mouseMoved = b } + } + }; + + /** ****************************************************************** + * Stats accumulator + ******************************************************************* */ + + function sendOnEvent(name, data) { + if (name && onEvent[name]) onEvent[name](data); + } + + function addOnEvent(name, handler) { + if (name && typeof(name) === 'string' && typeof(handler) === 'function') { + onEvent[name] = handler; + } + } + + function triggerSettingsEvent() { + sendOnEvent('settings', settings); + } + + function oremap(o,k,p) { + try { + var ov = o[k]; + delete o[k]; + o[p+(k.split('-')[1].toLowerCase())] = ov; + } catch (e) { + console.log(e) + } + } + + function Stats(db) { + this.db = db; + this.obj = js2o(db['stats'] || '{}'); + var o = this.obj, k; + for (k in o) { + if (!o.hasOwnProperty(k)) continue; + if (k.indexOf('slice-') === 0) oremap(o,k,'s-'); + if (k.indexOf('prep-') === 0) oremap(o,k,'p-'); + if (k.indexOf('export-') === 0) oremap(o,k,'e-'); + if (k.indexOf('show-') === 0) oremap(o,k,'d-'); + } + } + Stats.prototype.save = function() { + this.db['stats'] = o2js(this.obj); + return this; + }; + Stats.prototype.get = function(k) { + return this.obj[k]; + } + Stats.prototype.set = function(k,v) { + this.obj[k] = v; + this.save(); + return this; + }; + Stats.prototype.add = function(k,v) { + this.obj[k] = (this.obj[k] || 0) + (v || 1); + this.save(); + return this; + }; + Stats.prototype.del = function(k) { + delete this.obj[k]; + this.save(); + return this; + } + + STATS.set('upgrade', kiri.version !== STATS.get('kiri') && STATS.get('init') > 0); + STATS.set('kiri', kiri.version); + + /** ****************************************************************** + * Utility Functions + ******************************************************************* */ + + function isSecure(proto) { + return proto.toLowerCase().indexOf("https") === 0; + } + + function parseOpt(ov) { + var opt = {}, kv, kva; + ov.split(',').forEach(function(el) { + kv = el.split(':'); + if (kv.length === 2) { + kva = opt[kv[0]] = opt[kv[0]] || []; + kva.push(decodeURIComponent(kv[1])); + } + }); + return opt; + } + + function ajax(url, fn, rt, po, hd) { + return new MOTO.Ajax(fn, rt).request(url, po, hd); + } + + function o2js(o,def) { + return o ? JSON.stringify(o) : def || null; + } + + function js2o(s,def) { + try { + return s ? JSON.parse(s) : def || null; + } catch (e) { + console.log({malformed_json:s}); + return def || null; + } + } + + function ls2o(key,def) { + return js2o(SDB.getItem(key),def); + } + + function set(to, from) { + for (var k in from) { + if (from.hasOwnProperty(k)) to[k] = from[k]; + } + } + + function cull(o, f) { + for (var k in o) { + if (!o.hasOwnProperty(k)) continue; + if (!f.hasOwnProperty(k)) delete o[k]; + } + } + + function setProgress(value, msg) { + if (value) { + value = UTIL.round(value*100,4); + UI.loading.display = 'block'; + UI.progress.width = value+'%'; + if (msg) UI.prostatus.innerHTML = msg; + } else { + UI.loading.display = 'none'; + } + } + + function bound(v,min,max) { + return Math.max(min,Math.min(max,v)); + } + + function setVisibleLayer(v) { + showSlices(showLayerValue = bound(v, 0, showLayerMax)); + } + + function meshArray() { + var out = []; + forWidgets(function(widget) { + out.push(widget.mesh); + }); + return out; + } + + function forWidgets(f) { + WIDGETS.slice().forEach(function(widget) { + f(widget); + }); + } + + function forSelectedWidgets(f) { + var m = selectedMeshes; + if (m.length === 0 && WIDGETS.length === 1) m = [ WIDGETS[0].mesh ]; + m.slice().forEach(function (mesh) { f(mesh.widget) }); + } + + function forSelectedMeshes(f) { + selectedMeshes.slice().forEach(function (mesh) { f(mesh) }); + } + + function toggleWireframe(color, opacity) { + forWidgets(function(w) { w.toggleWireframe(color, opacity) }); + SPACE.update(); + } + + function updateSliderMax(set) { + var max = 0; + if (viewMode === VIEWS.PREVIEW && currentPrint) { + max = currentPrint.getLayerCount(); + } else { + forWidgets(function(widget) { + if (!widget.slices) return; + max = Math.max(max, widget.slices.length - 1); + }); + } + showLayerMax = max; + if (UI.layerID.convert() > max || showLayerValue > max) { + showLayerValue = max; + UI.layerID.value = max; + UI.layerSlider.value = showLayerMax; + } + UI.layerSlider.max = max; + if (set) { + showLayerValue = showLayerMax; + UI.layerSlider.value = showLayerMax; + } + } + + function hideSlices() { + var showing = false; + setOpacity(model_opacity); + forWidgets(function(widget) { + widget.setWireframe(false); + showing = widget.hideSlices() || showing; + }); + clearPrint(); + return showing; + } + + function showSlice(index, range, layer) { + return range ? + (index <= layer && index > layer-range) : (index <= layer); + MODE === MODES.CAM ? (index >= layer) : (index <= layer); + } + + /** + * hide or show slice-layers and their sub-elements + * + * @param {number} [layer] + */ + function showSlices(layer) { + if (typeof(layer) === 'string' || typeof(layer) === 'number') { + layer = parseInt(layer); + } else { + layer = showLayerValue; + } + + layer = bound(layer, 0, showLayerMax); + + UI.layerID.value = layer; + UI.layerSlider.value = layer; + + var j, + slice, + slices, + layers, + range = UI.layerRange.checked ? UI.layerSpan.convert() || 1 : 0, + print = UI.layerPrint.checked; + + if (MODE === MODES.CAM && showLayerRange !== range && range && layer === showLayerMax) { + layer = 0; + } + + showLayerRange = range; + showLayerValue = layer; + + forWidgets(function(widget) { + if (print) return widget.hideSlices(); + + slices = widget.slices; + if (!slices) return; + + for (j = 0; j < slices.length; j++) { + slice = slices[j]; + slice.view.visible = showSlice(j, range, layer); + layers = slice.layers; + layers.outline.setVisible( + MODE === MODES.CAM ? + UI.layerOutline.checked && LOCAL : + UI.layerOutline.checked + ); + layers.trace.setVisible( + MODE === MODES.CAM ? + UI.layerRough.checked : + UI.layerTrace.checked + ); + layers.bridge.setVisible( + MODE === MODES.CAM ? + UI.layerFinishX.checked : + UI.layerDelta.checked + ); + layers.flat.setVisible( + MODE === MODES.CAM ? + UI.layerFinishY.checked : + UI.layerDelta.checked + ); + layers.solid.setVisible( + MODE === MODES.CAM ? + UI.layerFinish.checked : + UI.layerSolid.checked + ); + layers.fill.setVisible( + MODE === MODES.CAM ? + UI.layerFacing.checked : + UI.layerFill.checked + ); + layers.sparse.setVisible(UI.layerSparse.checked); + layers.support.setVisible(UI.layerSupport.checked); + } + }); + + if (currentPrint) { + for (j = 0; j < currentPrint.layerView.length; j++) { + currentPrint.showLayer(j, print && showSlice(j, range, layer)); + } + } + + SPACE.update(); + } + + function newButton(name,action) { + var b = DOC.createElement("button"); + b.appendChild(DOC.createTextNode(name)); + b.onclick = action; + return b; + } + + function loadGCode(gcode) { + setViewMode(VIEWS.PREVIEW); + clearPrint(); + setOpacity(0); + currentPrint = kiri.newPrint(settings, []); + currentPrint.parseGCode(gcode, settings.process.outputOriginCenter ? null : { + x: -settings.device.bedWidth / 2, + y: -settings.device.bedDepth / 2, + }); + currentPrint.render(); + SPACE.platform.add(currentPrint.group); + SPACE.update(); + UI.layerPrint.checked = true; + updateSliderMax(true); + showSlices(); + exportGCode(gcode); + } + + function preparePrint(callback) { + setViewMode(VIEWS.PREVIEW); + + // kick off slicing it hasn't been done already + for (var i=0; i < WIDGETS.length; i++) { + if (!WIDGETS[i].slices || WIDGETS[i].isModified()) { + prepareSlices(function() { preparePrint(callback) }); + return; + } + } + + clearPrint(); + saveSettings(); + + if (MODE === MODES.CAM) { + setOpacity(preview_opacity_cam); + forWidgets(function(widget) { + widget.setColor(widget_cam_preview_color); + }); + } else { + setOpacity(preview_opacity); + } + + currentPrint = kiri.newPrint(settings, WIDGETS); + currentPrint.setup(printRemote, function(update, status) { + // on update + setProgress(update, status); + }, function() { + setProgress(0); + currentPrint.render(); + + // on done + STATS.add('p-'+getMode().toLowerCase()); + SPACE.platform.add(currentPrint.group); + SPACE.update(); + + UI.layerPrint.checked = true; + updateSliderMax(true); + showSlices(); + + if (typeof(callback) === 'function') callback(); + }) + } + + function exportPrint() { + if (!currentPrint) { + preparePrint(exportPrint); + return; + } + STATS.add('e-'+getMode().toLowerCase()); + switch (settings.mode) { + case 'LASER': return exportPrintLaser(); + case 'FDM': return exportPrintGCODE(); + case 'CAM': return exportPrintGCODE(); + } + } + + function exportPrintGCODE() { + if (!currentPrint) { + preparePrint(exportPrint); + return; + } + currentPrint.exportGCode(printRemote, function(gcode) { + exportGCode(gcode); + }); + } + + function exportPrintLaser() { + if (!currentPrint) { + preparePrint(exportPrintLaser); + return; + } + + var filename = "laser-"+(new Date().getTime().toString(36)); + + function download_svg() { + saveAs(new Blob( + [currentPrint.exportSVG()], + {type:"application/octet-stream"}), + $('print-filename').value + ".svg"); + } + + function download_dxf() { + saveAs(new Blob( + [currentPrint.exportDXF()], + {type:"application/octet-stream"}), + $('print-filename').value + ".dxf"); + } + + function download_gcode() { + saveAs(new Blob( + [currentPrint.exportLaserGCode()], + {type:"application/octet-stream"}), + $('print-filename').value + ".gcode"); + } + + ajax("/kiri/output-laser.html", function(html) { + UI.print.innerHTML = html; + $('print-filename').value = filename; + $('print-lines').value = currentPrint.lines; + $('print-close').onclick = hideModal; + $('print-svg').onclick = download_svg; + $('print-dxf').onclick = download_dxf; + $('print-lg').onclick = download_gcode; + showModal('print'); + }); + } + + function exportGCode(gcode) { + if (MODE === MODES.CAM) { + KIRI.serial.setGCode(gcode, currentPrint.bounds); + } + + var pre = MODE === MODES.CAM ? "cnc-" : "print-", + filename = pre+(new Date().getTime().toString(36)), + fileext = settings.device.gcodeFExt || "gcode", + octo_host, + octo_apik, + grid_host, + grid_apik, + grid_target; + + function getBlob() { + return new Blob( + [gcode], + {type:"application/octet-stream"}); + } + + function octoprint() { + if (!(octo_host && octo_apik)) return; + + var form = new FormData(), + ajax = new XMLHttpRequest(), + host = octo_host.value.toLowerCase(), + apik = octo_apik.value; + + if (host.indexOf("http") !== 0) { + alert("host missing protocol (http:// or https://)"); + return; + } + if (SECURE && !isSecure(host)) { + alert("host must begin with 'https' on a secure site"); + return; + } + + SDB['octo-host'] = host.trim(); + SDB['octo-apik'] = apik.trim(); + + filename = $('print-filename').value; + form.append("file", getBlob(), filename+"."+fileext); + ajax.onreadystatechange = function() { + if (ajax.readyState === 4) { + var status = ajax.status; + if (status >= 200 && status < 300) { + hideModal(); + } else { + alert("octoprint error\nstatus: "+status+"\nmessage: "+ajax.responseText); + } + } + }; + ajax.upload.addEventListener('progress', function(evt) { + setProgress(Math.ceil(evt.loaded/evt.total), "sending"); + }); + ajax.open("POST", host+"/api/files/local"); + ajax.setRequestHeader("X-Api-Key", apik); + ajax.send(form); + } + + function gridprint_tracker(host,key) { + ajax(host+"/api/check?key="+key, function(data) { + data = js2o(data); + DBUG.log(data); + if (!data.done) setTimeout(function() { gridprint_tracker(host,key) }, 1000); + }); + } + + function gridprint() { + if (!(grid_host && grid_apik)) return; + + var xhtr = new XMLHttpRequest(), + host = grid_host.value, + apik = grid_apik.value, + target = grid_target.value; + + if (host.indexOf("http") !== 0) { + alert("host missing protocol (http:// or https://)"); + return; + } + if (host.indexOf("://") < 0) { + alert("host:port malformed"); + return; + } + if (SECURE && !isSecure(host)) { + alert("host must begin with 'https' on a secure site"); + return; + } + + SDB['grid-host'] = host.trim(); + SDB['grid-apik'] = apik.trim(); + SDB['grid-target'] = target.trim(); + + xhtr.onreadystatechange = function() { + if (xhtr.readyState === 4) { + var status = xhtr.status; + if (status >= 200 && status < 300) { + var json = js2o(xhtr.responseText); + gridprint_tracker(host,json.key); + ajax(host+"/api/wait?key="+json.key, function(data) { + data = js2o(data); + DBUG.log(data); + alert("print to "+target+" @ "+host+" : "+data.status); + }); + } else { + alert("grid:print error\nstatus: "+status+"\nmessage: "+xhtr.responseText); + } + setProgress(0); + } + }; + xhtr.upload.addEventListener('progress', function(evt) { + setProgress(Math.ceil(evt.loaded/evt.total), "sending"); + }); + filename = $('print-filename').value; + xhtr.open("POST", host+"/api/print?filename="+filename+"&target="+target+"&key="+apik); + xhtr.setRequestHeader("Content-Type", "text/plain"); + xhtr.send(gcode); + hideModal(); + } + + function download() { + filename = $('print-filename').value; + saveAs(getBlob(), filename+"."+fileext); + } + + function pad(v) { + v = v.toString(); + return v.length < 2 ? '0' + v : v; + } + + function calcWeight() { + $('print-weight').value = ( + UTIL.round( + (Math.PI * UTIL.sqr(currentPrint.settings.device.filamentSize/2)) * currentPrint.distance * parseFloat($('print-weight').value || 1.25) / 1000, 2) + ); + } + + function calcTime() { + var floor = Math.floor, + time = floor(currentPrint.time), + hours = floor(time / 3600), + newtime = time - hours * 3600, + mins = floor(newtime / 60), + secs = newtime - mins * 60; + + $('mill-time').value = $('print-time').value = [pad(hours),pad(mins),pad(secs)].join(':'); + } + + ajax("/kiri/output-gcode.html", function(html) { + UI.print.innerHTML = html; + $('print-close').onclick = hideModal; + $('print-download').onclick = download; + $('print-octoprint').onclick = octoprint; + $('print-gridprint').onclick = gridprint; + $('print-serial').onclick = showSerial; + $('print-serial').style.display = MODE === MODES.CAM ? '' : 'none'; + $('print-filament-row').style.display = MODE === MODES.FDM ? '' : 'none'; + $('mill-info').style.display = MODE === MODES.CAM ? '' : 'none'; + $('print-filename').value = filename; + $('print-filesize').value = currentPrint.bytes; + $('print-filament').value = Math.round(currentPrint.distance); + calcTime(); + calcWeight(); + octo_host = $('octo-host'); + octo_apik = $('octo-apik'); + if (OCTOPRINT) { + $('ophost').style.display = 'none'; + $('opapik').style.display = 'none'; + $('ophint').style.display = 'none'; + $('send-to-gridprint').style.display = 'none'; + } + octo_host.value = SDB['octo-host'] || ''; + octo_apik.value = SDB['octo-apik'] || ''; + grid_host = $('grid-host'); + grid_apik = $('grid-apik'); + grid_target = $('grid-target'); + grid_host.value = SDB['grid-host'] || ''; + grid_apik.value = SDB['grid-apik'] || ''; + grid_target.value = SDB['grid-target'] || ''; + showModal('print'); + }); + } + + function clearWidgetCache() { + hideSlices(); + clearSlices(); + clearPrint(); + } + + function clearPrint() { + if (currentPrint) { + SPACE.platform.remove(currentPrint.group); + currentPrint = null; + } + UI.layerPrint.checked = false; + } + + function clearSlices() { + forWidgets(function(widget) { + widget.slices = null; + }); + } + + /** + * incrementally slice all meshes then incrementally update them + * + * @param {Function} callback + */ + function prepareSlices(callback) { + setViewMode(VIEWS.SLICE); + + var selectSave = selectedMeshes.slice(); + + clearPrint(); + saveSettings(); + widgetDeselect(); + + var firstMesh = true, + countdown = WIDGETS.length, + preserveMax = showLayerMax, + preserveLayer = showLayerValue, + totalProgress, + track = {}; + + // require topo be sent back from worker for local printing + settings.synth.sendTopo = !printRemote; + + setOpacity(slicing_opacity); + + // for each widget, slice + forWidgets(function(widget) { + var segtimes = {}, + segNumber = 0, + startTime, + lastMsg; + + // skip non-selected widgets in CAM mode when any widget is selected + if (MODE === MODES.CAM && selectSave.length > 0 && selectSave.indexOf(widget.mesh) < 0) return --countdown; + + widget.stats.progress = 0; + widget.setColor(widget_slicing_color); + widget.slice(settings, function(sliced) { + var mark = UTIL.time(); + // on done + widget.render(renderMode, MODE === MODES.CAM); + // clear wireframe + widget.setWireframe(false, wireframe_color, wireframe_model_opacity); + widget.setOpacity(settings.mode === 'CAM' ? sliced_opacity_cam : sliced_opacity); + widget.setColor(widget_deselected_color); + // update UI info + if (sliced) { + // update segment time + if (lastMsg) segtimes[segNumber+"_"+lastMsg] = mark - startTime; + DBUG.log(segtimes); + STATS.add('s-'+getMode().toLowerCase()); + updateSliderMax(true); + if (preserveMax != showLayerMax) { + preserveLayer = showLayerMax; + } + firstMesh = false; + } + // on the last exit, update ui and call the callback + if (--countdown === 0) { + setProgress(0); + showSlices(preserveLayer); + if (callback && typeof callback === 'function') callback(); + setOpacity(settings.mode === 'CAM' ? sliced_opacity_cam : sliced_opacity); + } + // update slider window + onControlResize(); + }, function(update, msg) { + if (msg !== lastMsg) { + var mark = UTIL.time(); + if (lastMsg) segtimes[segNumber+"_"+lastMsg] = mark - startTime; + lastMsg = msg; + startTime = mark; + segNumber++; + } + // on update + track[widget.id] = update; + totalProgress = 0; + forWidgets(function(w) { + totalProgress += (track[w.id] || 0); + }); + setProgress(totalProgress / WIDGETS.length, msg); + }, sliceRemote); + }); + } + + function fillOffsetMult() { + return 1.0-bound(settings.process.sliceFillOverlap, 0, 0.8); + } + + function diffOffset() { + return (settings.device.nozzleSize / 2) * fillOffsetMult(); + } + + function meshUpdateInfo(mesh) { + UI.selection.style.display = selectedWidgetCount() ? 'block' : 'none'; + if (!mesh) { + if (selectedMeshes.length === 0) { + UI.selWidth.innerHTML = '0'; + UI.selDepth.innerHTML = '0'; + UI.selHeight.innerHTML = '0'; + UI.scaleX.value = ''; + UI.scaleY.value = ''; + UI.scaleZ.value = ''; + } + return + } + UI.selWidth.innerHTML = UTIL.round(mesh.w,2); + UI.selDepth.innerHTML = UTIL.round(mesh.h,2); + UI.selHeight.innerHTML = UTIL.round(mesh.d,2); + UI.scaleX.value = 1; + UI.scaleY.value = 1; + UI.scaleZ.value = 1; + } + + function setOpacity(value) { + forWidgets(function (w) { w.setOpacity(value) }); + UI.viewModelOpacity.value = value * 100; + SPACE.update(); + } + + function moveSelection(x, y, z, abs) { + forSelectedWidgets(function (w) { w.move(x, y, z, abs) }); + SPACE.update(); + } + + function scaleSelection(ev) { + var dv = parseFloat(ev.target.value || 1); + if (UI.scaleUniform.checked) { + UI.scaleX.value = dv; + UI.scaleY.value = dv; + UI.scaleZ.value = dv; + } + var x = parseFloat(UI.scaleX.value || dv), + y = parseFloat(UI.scaleY.value || dv), + z = parseFloat(UI.scaleZ.value || dv); + forSelectedWidgets(function (w) { + w.scale(x,y,z); + meshUpdateInfo(w.mesh); + }); + UI.scaleX.value = 1; + UI.scaleY.value = 1; + UI.scaleZ.value = 1; + platformComputeMaxZ(); + SPACE.update(); + } + + function rotateSelection(x, y, z) { + forSelectedWidgets(function (w) { w.rotate(x, y, z) }); + platformComputeMaxZ(); + SPACE.update(); + } + + function boundsSelection() { + var bounds = new THREE.Box3(); + forSelectedWidgets(function(widget) { + bounds.union(widget.mesh.getBoundingBox()); + }); + return bounds; + } + + function platformComputeMaxZ() { + var maxZ = 0; + forWidgets(function(widget) { + maxZ = Math.max(maxZ, widget.mesh.getBoundingBox().max.z); + }); + SPACE.platform.setMaxZ(maxZ); + } + + function platformAdd(widget, shift, nolayout) { + WIDGETS.push(widget); + SPACE.platform.add(widget.mesh); + widgetSelect(widget, shift); + platformComputeMaxZ(); + if (nolayout) return; + if (layoutOnAdd) layoutPlatform(0); + } + + function platformDelete(widget) { + if (Array.isArray(widget)) { + var mc = widget.slice(), i; + for (i=0; i= 0); + if (sel) { + if (shift) { + widgetDeselect(widget) + } else if (selectedMeshes.length > 1) { + widgetDeselect(); + widgetSelect(widget, false); + } + } else { + // prevent selection in slice view + if (!mesh.material.visible) return; + if (!shift) widgetDeselect(); + selectedMeshes.push(mesh); + widget.setColor(widget_selected_color); + meshUpdateInfo(mesh); + } + SPACE.update(); + } + + function selectedWidgetCount() { + return viewMode === VIEWS.ARRANGE ? selectedMeshes.length : 0; + } + + function widgetDeselect(widget) { + if (viewMode !== VIEWS.ARRANGE) return; + if (!widget) { + forWidgets(function(widget) { + widgetDeselect(widget); + }); + return; + } + var mesh = widget.mesh, + si = selectedMeshes.indexOf(mesh), + sel = (si >= 0); + if (sel) selectedMeshes.splice(si,1); + widget.setColor(widget_deselected_color); + SPACE.update(); + meshUpdateInfo(); + } + + function layoutPlatform(event, space) { + var layout = (viewMode === VIEWS.ARRANGE), + modified = false, + gap = space || 1, + topZ = MODE === MODES.CAM ? camTopZ : 0; + + setViewMode(VIEWS.ARRANGE); + hideSlices(); + + // do not layout when switching back from slice view + if (!space && !layout) return SPACE.update(); + + forWidgets(function(w) { + modified |= w.isModified(); + }); + + // in CNC mode with >1 widget, force layout with spacing @ 1.5x largest tool diameter + if (MODE === MODES.CAM && WIDGETS.length > 1) { + var spacing = space || 1, proc = settings.process, CAM = KIRI.driver.CAM; + if (proc.roughingOn) spacing = Math.max(spacing, CAM.getToolDiameter(settings, proc.roughingTool)); + if (proc.finishingOn || proc.finishingXOn || proc.finishingYOn) spacing = Math.max(spacing, CAM.getToolDiameter(settings, proc.finishingTool)); + gap = spacing * 1.5; + } + + var i, m, sz = SPACE.platform.size(), + mp = [sz.x, sz.y], + ms = [mp[0] / 2, mp[1] / 2], + mi = mp[0] > mp[1] ? [(mp[0] / mp[1]) * 10, 10] : [10, (mp[1] / mp[1]) * 10], + c = meshArray().sort(function (a, b) { return (b.w * b.h) - (a.w * a.h) }), + p = new MOTO.Pack(ms[0], ms[1], gap).fit(c); + while (!p.packed) { + ms[0] += mi[0]; + ms[1] += mi[1]; + p = new MOTO.Pack(ms[0], ms[1], gap).fit(c); + } + 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.widget.move(p.max.w / 2 - m.fit.x, p.max.h / 2 - m.fit.y, 0, true); + m.widget.setTopZ(topZ); + m.material.visible = true; + } + + SPACE.update(); + } + + function loadWidget(url, onload) { + if (url.toLowerCase().indexOf(".stl") > 0) { + loadSTL(url, onload); + } else { + ajax(url, function(vertices) { + vertices = js2o(vertices).toFloat32(); + platformAdd(newWidget().loadVertices(vertices)); + if (onload) onload(vertices); + }); + } + } + + function loadSTL(url, onload) { + new MOTO.STL().load(url, function(vertices) { + platformAdd(newWidget().loadVertices(vertices)); + if (onload) onload(vertices); + }) + } + + function resetSettings(force) { + if (force || confirm('reset all values to system defaults?')) { + settings = settingsDefault; + updateFields(); + }; + } + + /** + * fill in missing settings from default template to pick up new fields + * that may have been recently added and expected in the code + * + * @param {Object} osrc + * @param {Object} odst + */ + function fillMissingSettings(osrc, odst) { + var key, val; + for (key in osrc) { + if (!osrc.hasOwnProperty(key)) continue; + val = odst[key]; + if (typeof val === 'undefined' || val === null || val === '') { + odst[key] = osrc[key]; + } else if (typeof osrc[key] === 'object') { + fillMissingSettings(osrc[key], odst[key]); + } + } + } + + /** + * @returns {Object} + */ + function updateFieldsFromSettings(scope) { + if (!scope) return console.trace("missing scope"); + + var key, val; + + fillMissingSettings(settingsDefault, settings); + + for (key in scope) { + if (!scope.hasOwnProperty(key)) continue; + val = scope[key]; + if (UI.hasOwnProperty(key)) { + var uie = UI[key], + typ = uie ? uie.type : null; + if (typ === 'text') { + uie.value = val; + } else if (typ === 'checkbox') { + uie.checked = val; + } else if (typ === 'select-one') { + uie.innerHTML = ''; + var chosen = null; + settings.tools.forEach(function(tool,index) { + if (val === tool.id) chosen = index+1; + var opt = DOC.createElement('option'); + opt.appendChild(DOC.createTextNode(tool.name)); + opt.setAttribute('value', tool.id); + uie.appendChild(opt); + }); + if (chosen) uie.selectedIndex = chosen; + } + } + } + + return settings; + } + + /** + * @returns {Object} + */ + function updateSettingsFromFields(scope) { + if (!scope) return console.trace("missing scope"); + + var key, + changed = false; + + // for each key in scope object + for (key in scope) { + if (!scope.hasOwnProperty(key)) continue; + if (UI.hasOwnProperty(key)) { + var nval = null, + uie = UI[key]; + // skip empty UI values + if (!uie || uie === '') continue; + if (uie.type === 'text') { + nval = UI[key].convert(); + } else if (uie.type === 'checkbox') { + nval = UI[key].checked; + } else if (uie.type === 'select-one') { + if (uie.selectedIndex > 0) { + nval = parseInt(uie.options[uie.selectedIndex].value); + } + } + if (scope[key] != nval) { + scope[key] = nval; + } + } + } + + settings.synth.fillOffsetMult = fillOffsetMult(); + settings.synth.diffOffsetMult = diffOffset(); + + return settings; + } + + function camUpdateWidgetZ() { + var ztop = MODE === MODES.CAM ? camTopZ : 0; + forWidgets(function(widget) { + widget.setTopZ(ztop); + }); + } + + function updateCamStock() { + var sd = settings.process; + // create/inject cam stock if stock size other than default + if (sd.camStockX && sd.camStockY && sd.camStockZ) { + if (!camStock) { + var geo = new THREE.BoxGeometry(1, 1, 1); + var mat = new THREE.MeshBasicMaterial({ color: 0x777777, opacity: 0.2, transparent: true, side:THREE.DoubleSide }); + var cube = new THREE.Mesh(geo, mat); + SPACE.platform.add(cube); + camStock = cube; + } + camStock.scale.x = sd.camStockX; + camStock.scale.y = sd.camStockY; + camStock.scale.z = sd.camStockZ; + camStock.position.z = sd.camStockZ / 2; + camStock.material.visible = settings.mode === 'CAM'; + camTopZ = sd.camStockZ - sd.camZTopOffset; + camUpdateWidgetZ(); + SPACE.update(); + } else if (camStock) { + SPACE.platform.remove(camStock); + SPACE.update(); + camStock = null; + camTopZ = 0; + camUpdateWidgetZ(); + } + } + + function updateFields() { + updateFieldsFromSettings(settings.device); + updateFieldsFromSettings(settings.process); + updateFieldsFromSettings(settings.layers); + } + + function updateSettings() { + updateSettingsFromFields(settings.device); + updateSettingsFromFields(settings.process); + updateSettingsFromFields(settings.layers, true); + saveSettings(); + updateCamStock(); + } + + function saveSettings(nocam) { + // remove settings invalid for a given mode (cleanup) + cull(settings, settingsDefault); + switch (settings.mode) { + case 'FDM': + cull(settings.device, sf.fdm.d); + cull(settings.process, sf.fdm.p); + break; + case 'CAM': + cull(settings.device, sf.cam.d); + cull(settings.process, sf.cam.p); + break; + case 'LASER': + cull(settings.device, sf.laser.d); + cull(settings.process, sf.laser.p); + settings.cdev.LASER = settings.device; + break; + } + cull(settings.cdev.FDM, sf.fdm.d); + cull(settings.cdev.CAM, sf.cam.d); + + // store camera view + var view = SPACE.view.save(); + if (view.left || view.up) settings.controller.view = view; + + SDB.setItem('ws-settings', JSON.stringify(settings)); + } + + function saveWorkspace() { + saveSettings(); + var newWidgets = [], + oldWidgets = js2o(SDB.getItem('ws-widgets'), []); + forWidgets(function(widget) { + newWidgets.push(widget.id); + oldWidgets.remove(widget.id); + widget.saveState(); + }); + SDB.setItem('ws-widgets', o2js(newWidgets)); + oldWidgets.forEach(function(wid) { + Widget.deleteFromState(wid); + }); + } + + function loadFiles(files) { + for (var i=0; i 0, + isgcode = lower.indexOf(".gcode") > 0 || lower.indexOf(".nc") > 0; + reader.file = files[i]; + reader.onloadend = function (e) { + if (isstl) + platformAdd( + newWidget() + .loadVertices(new MOTO.STL().parse(e.target.result)) + .saveToCatalog(e.target.file.name) + ); + if (isgcode) loadGCode(e.target.result); + }; + reader.readAsBinaryString(reader.file); + } + } + + function loadFile() { + $('load-file').onchange = function(event) { + DBUG.log(event); + loadFiles(event.target.files); + }; + $('load-file').click(); + // alert("drag/drop STL files onto platform to import\nreload page to return to last saved state"); + } + + // kiri api + function getSettings() { + return settings; + } + + // kiri api + function putSettings(newset) { + settings = newset; + saveSettings() + restoreWorkspace(null, true); + } + + function updatePlatformSize() { + var dev = settings.device, + width, depth, + height = Math.round(Math.max(dev.bedHeight, dev.bedWidth/100, dev.bedDepth/100)); + SPACE.platform.setGZOff(height/2 - 0.1); + SPACE.platform.setSize( + width = parseInt(dev.bedWidth), + depth = parseInt(dev.bedDepth), + height + ); + SPACE.platform.setHidden(width > 500 || depth > 500); + } + + function restoreWorkspace(ondone, skipwidgets) { + var loaded = 0, + toload = ls2o('ws-widgets',[]), + newset = ls2o('ws-settings'), + camera = ls2o('ws-camera'), + isFDM = (newset || settings).mode === 'FDM'; + + if (newset) { + fillMissingSettings(settingsDefault, newset); + settings = newset; + // override camera from settings + if (settings.controller.view) { + camera = settings.controller.view; + SDB.removeItem('ws-camera'); + UI.reverseZoom.checked = settings.controller.reverseZoom; + } + // update/integrate old settings + ["FDM","CAM","LASER"].forEach(function(mode) { + var key = 'ws-settings-'+mode, + oset = ls2o(key, settings); + SDB.removeItem(key); + SDB.removeItem('ws-camera-'+mode); + // create default settings if missing + if (!settings.cproc[mode]) { + // merge bed into device + set(oset.device, oset.bed); + // merge output into process + set(oset.process, oset.output); + settings.cproc[mode] = "default"; + settings.sproc[mode].default = oset.process; + } + // copy device into cdev cache if missing + if (!settings.cdev[mode]) settings.cdev[mode] = oset.device; + // prefer tools object from CAM settings + if (mode === 'CAM' && oset.tools) { + settings.tools = oset.tools; + } + // restore per-mode filter + settings.filter[mode] = oset.filter[mode]; + }); + // merge custom filters from localstorage into settings + localFilters.forEach(function(fname) { + var fkey = "gcode-filter-"+fname, ov = ls2o(fkey); + if (ov) settings.devices[fname] = ov; + SDB.removeItem(fkey) + }); + SDB.removeItem(localFilterKey); + // save updated settings + saveSettings(); + } + + updateFields(); + updatePlatformSize(); + updateCamStock(); + + SPACE.view.reset(); + + if (camera) SPACE.view.load(camera); + else setTimeout(SPACE.view.home, 100); + + if (skipwidgets) return; + + forWidgets(function(widget) { + platformDelete(widget); + }); + + toload.forEach(function(widgetid) { + Widget.loadFromState(widgetid, function(widget) { + if (widget) platformAdd(widget, 0, isFDM); + if (++loaded === toload.length) { + widgetDeselect(); + if (ondone) ondone(); + } + }, isFDM); + }); + + return toload.length > 0; + } + + function clearWorkspace() { + // free up worker cache/mem + KIRI.work.clear(); + platformSelectAll(); + platformDelete(selectedMeshes); + } + + function modalShowing() { + var showing = false; + ["modal","catalog","devices","tools"].forEach(function(dialog) { + showing = showing || UI[dialog].style.display === 'block'; + }); + return showing; + } + + function showModal(which) { + UI.modal.style.display = 'block'; + ["print","help"].forEach(function(modal) { + UI[modal].style.display = (modal === which ? 'block' : 'none'); + }); + } + + function hideDialog() { + showDialog(null); + } + + function showDialog(which, force) { + ["catalog","devices","tools","settings"].forEach(function(dialog) { + var style = UI[dialog].style; + style.display = (dialog === which && (force || style.display !== 'block') ? 'block' : 'none'); + }); + } + + function showCatalog() { + showDialog("catalog"); + } + + function loadNamedSetting(e) { + var name = e ? e.target.getAttribute("load") : settings.cproc[getMode()], + load = settings.sproc[getMode()][name]; + if (!load) return; + for (var k in load) { + if (!load.hasOwnProperty(k)) continue; + settings.process[k] = load[k]; + } + settings.process.processName = name; + settings.cproc[getMode()] = name; + updateFields(); + hideDialog(); + updateSettings(); + if (e) triggerSettingsEvent(); + } + + function deleteNamedSetting(e) { + var name = e.target.getAttribute("del"); + delete settings.sproc[getMode()][name]; + updateSettingsList(); + saveSettings(); + triggerSettingsEvent(); + } + + function updateSettingsList() { + var list = [], s = settings, sp = s.sproc[getMode()] || {}, table = UI.settingsList; + table.innerHTML = ''; + for (var k in sp) { + if (sp.hasOwnProperty(k)) list.push(k); + } + list.sort().forEach(function(sk) { + var row = DOC.createElement('div'), + load = DOC.createElement('button'), + del = DOC.createElement('button'), + name = sk; + + load.setAttribute('load', sk); + load.onclick = loadNamedSetting; + load.appendChild(DOC.createTextNode(sk)); + + del.setAttribute('del', sk); + del.setAttribute('title', "remove '"+sk+"'"); + del.onclick = deleteNamedSetting; + del.appendChild(DOC.createTextNode('x')); + + row.setAttribute("class", "flow-row"); + row.appendChild(load); + row.appendChild(del); + table.appendChild(row); + }); + onControlResize(); + } + + function showSettings() { + updateSettingsList(); + showDialog("settings"); + } + + function onWindowResize() { + onControlResize(); + } + + function onControlResize() { + var left = UI.ctrlLeft.getBoundingClientRect(), + right = UI.ctrlRight.getBoundingClientRect(); + UI.catalog.style.left = (left.width + 5) + 'px'; + UI.devices.style.left = (left.width + 5) + 'px'; + UI.tools.style.left = (left.width + 5) + 'px'; + UI.settings.style.right = (right.width + 5) + 'px'; + } + + function hideModal() { + UI.modal.style.display = 'none'; + } + + function showSerial(toggle) { + if (MODE !== MODES.CAM) return; + hideModal(); + if (toggle === true) { + KIRI.serial.toggle(); + } else { + KIRI.serial.show(); + } + STATS.add('d-sender'); + } + + function showHelpLocal() { + showHelp("/kiri/help-kiri.html"); + } + + function showHelp(local) { + hideDialog(); + if (!local) { + WIN.open("//wiki.grid.space/wiki/Kiri:Moto", "_help"); + STATS.add('d-help'); + return; + } + ajax(local, function(html) { + UI.help.innerHTML = html; + $('help-close').onclick = hideModal; + $('kiri-version').innerHTML = 'version '+kiri.version+""; + showModal('help'); + STATS.add('d-help'); + }); + } + + function takeFocus(el) { + DOC.activeElement.blur(); + el = [ el || DOC.body, UI.ctrlLeft, UI.container, UI.assets, UI.control, UI.modeFDM, UI.reverseZoom, UI.viewModelOpacity, DOC.body ]; + for (var es, i=0; i 0) showLayerValue--; + else if (delta < 0) showLayerValue++; + showSlices(); + }); + SPACE.platform.onMove(saveSettings); + + set(UI, { + container: container, + ctrlLeft: $('control-left'), + ctrlRight: $('control-right'), + layerView: $('layer-view'), + layerSlider: $('layer-slider'), + + assets: assets, + control: control, + modal: $('modal'), + print: $('print'), + help: $('help'), + + devices: $('devices'), + deviceAdd: $('device-add'), + deviceDelete: $('device-del'), + deviceSave: $('device-save'), + deviceClose: $('device-close'), + deviceSelect: $('device-select'), + + device: UC.newGroup("device", $('device')), + deviceName: UC.newInput("name", {size:20}), + setDeviceFilament: UC.newInput("filament", {title:"diameter in millimeters", convert:UC.toFloat, modes:FDM}), + setDeviceNozzle: UC.newInput("nozzle", {title:"diameter in millimeters", convert:UC.toFloat, modes:FDM}), + setDeviceWidth: UC.newInput("bed width", {title:"millimeters", convert:UC.toInt}), + setDeviceDepth: UC.newInput("bed depth", {title:"millimeters", convert:UC.toInt}), + setDeviceHeight: UC.newInput("max height", {title:"max build height\nin millimeters", convert:UC.toInt, modes:FDM}), + setDeviceMaxSpindle: UC.newInput("max spindle rpm", {title:"max spindle speed\n0 to disable", convert:UC.toInt, modes:CAM}), + setDeviceOrigin: UC.newBoolean("origin center", onBooleanClick, {title:"bed origin center"}), + setDeviceOriginTop: UC.newBoolean("origin top", onBooleanClick, {title:"part z origin top", modes:CAM}), + + setDevice: UC.newGroup("gcode", $('device')), + setDeviceFan: UC.newInput("fan power", {title:"set cooling fan power", modes:FDM, size:15}), + setDeviceTrack: UC.newInput("progress", {title:"set progress meter (makerbot)", modes:FDM, size:15}), + setDeviceToken: UC.newBoolean("token spacing", null, {title:"gcode token spacing", modes:CAM}), + setDeviceStrip: UC.newBoolean("strip comments", null, {title:"strip gcode comments", modes:CAM}), + setDeviceFExt: UC.newInput("file ext", {title:"file name exension", modes:CAM, size:5}), + setDeviceDwell: UC.newInput("dwell", {title:"gcode dwell script", modes:CAM, size:14, height:2}), + setDeviceChange: UC.newInput("tool change", {title:"tool change script", modes:CAM, size:14, height:2}), + setDevicePre: UC.newInput("header", {title:"gcode header script", modes:FDM_CAM, size:14, height:3}), + setDevicePost: UC.newInput("footer", {title:"gcode footer script", modes:FDM_CAM, size:14, height:3}), + + tools: $('tools'), + toolsSave: $('tools-save'), + toolsClose: $('tools-close'), + toolSelect: $('tool-select'), + toolAdd: $('tool-add'), + toolDelete: $('tool-del'), + toolType: $('tool-type'), + toolName: $('tool-name'), + toolNum: $('tool-num'), + toolFluteDiam: $('tool-fdiam'), + toolFluteLen: $('tool-flen'), + toolShaftDiam: $('tool-sdiam'), + toolShaftLen: $('tool-slen'), + toolMetric: $('tool-metric'), + + catalog: $('catalog'), + catalogBody: $('catalogBody'), + catalogList: $('catalogList'), + + settings: $('settings'), + settingsBody: $('settingsBody'), + settingsList: $('settingsList'), + + layerID: $('layer-id'), + layerSpan: $('layer-span'), + layerRange: $('layer-range'), + + loading: $('loading').style, + progress: $('progress').style, + prostatus: $('prostatus'), + + selection: $('selection'), + selWidth: $('sel_width'), + selHeight: $('sel_height'), + selDepth: $('sel_depth'), + scaleX: $('scale_x'), + scaleY: $('scale_y'), + scaleZ: $('scale_z'), + scaleUniform: $('scale_uni'), + + mode: UC.newGroup('mode', assets), + modeTable: UC.newTableRow([ + [ + UI.modeFDM = + UC.newButton("FDM Printing", function() { setMode('FDM',null,updatePlatformSize) }), + ],[ + UI.modeLASER = + UC.newButton("Laser Cutting", function() { setMode('LASER',null,updatePlatformSize) }), + ],[ + UI.modeCAM = + UC.newButton("CNC Milling", function() { setMode('CAM',null,updatePlatformSize) }, {id:"modeCAM"}), + ] + ]), + system: UC.newGroup('setup'), + sysTable: UC.newTableRow([ + [ + UI.setupDevices = + UC.newButton("Devices", showDevices, {modes:FDM_CAM}) + ],[ + UI.setupTools = + UC.newButton('Tools', showTools, {modes:CAM}), + ],[ + UI.helpButton = + UC.newButton("Help", showHelp) + ] + ]), + wsFunc: UC.newGroup('function'), + wsFuncTable: UC.newTableRow([ + [ + UC.newButton("Import", function() { KIRI.api.import() }), + UI.import = + UC.newButton("+") + ],[ + UI.modeArrange = + UC.newButton("Arrange", layoutPlatform), + ],[ + UI.modeSlice = + UC.newButton("Slice", prepareSlices) + ],[ + UI.modePreview = + UC.newButton("Preview", preparePrint), + ],[ + UI.modeExport = + UC.newButton("Export", exportPrint) + ] + ]), + workspace: UC.newGroup('platform'), + wsTable: UC.newTableRow([ + [ + UI.saveButton = + UC.newButton("Save", saveWorkspace), + ],[ + UC.newButton("Clear", clearWorkspace) + ] + ]), + camera: UC.newGroup('view'), + camTable: UC.newTableRow([ + [ + UC.newButton("home", SPACE.view.home), + UC.newButton("reset", SPACE.view.reset) + ],[ + UC.newButton("top", SPACE.view.top), + UC.newButton("front", SPACE.view.front), + ],[ + UC.newButton("left", SPACE.view.left), + UC.newButton("right", SPACE.view.right) + ] + ]), + viewOpt: UC.newTableRow([[ + UI.reverseZoom = UC.newBoolean(null, invertMouse, {title:"invert mouse\nscroll zoom"}), + UI.viewModelOpacity = UC.newRange(null, {title:"change model opacity"}) + ]]), + + layers: UC.setGroup($("layers")), + layerOutline: UC.newBoolean("outline", onBooleanClick, {modes:LOCAL ? ALL : FDM_LASER}), + layerTrace: UC.newBoolean("trace", onBooleanClick, {modes:FDM_LASER}), + layerFacing: UC.newBoolean("facing", onBooleanClick, {modes:CAM}), + layerRough: UC.newBoolean("roughing", onBooleanClick, {modes:CAM}), + layerFinish: UC.newBoolean("finishing", onBooleanClick, {modes:CAM}), + layerFinishX: UC.newBoolean("finish x", onBooleanClick, {modes:CAM}), + layerFinishY: UC.newBoolean("finish y", onBooleanClick, {modes:CAM}), + layerDelta: UC.newBoolean("delta", onBooleanClick, {modes:FDM}), + layerSolid: UC.newBoolean("solids", onBooleanClick, {modes:FDM}), + layerFill: UC.newBoolean("solid fill", onBooleanClick, {modes:FDM}), + layerSparse: UC.newBoolean("sparse fill", onBooleanClick, {modes:FDM}), + layerSupport: UC.newBoolean("support", onBooleanClick, {modes:FDM}), + layerPrint: UC.newBoolean("print", onBooleanClick), + layerDebug: UC.newBoolean("debug", onBooleanClick, {hide:!LOCAL}), + + settingsGroup: UC.newGroup("settings", control), + settingsTable: UC.newTableRow([ + [ + UI.settingsLoad = + UC.newButton("load", settingsLoad), + UI.settingsSave = + UC.newButton("save", settingsSave), + ] + ]), + + platform: UC.newGroup("build area", control, {modes:LASER}), + bedWidth: UC.newInput("width", {title:"millimeters", convert:UC.toInt, modes:LASER}), + bedDepth: UC.newInput("depth", {title:"millimeters", convert:UC.toInt, modes:LASER}), + + process: UC.newGroup("process", control, {modes:FDM_LASER}), + processName: UC.newInput("name", {modes:[]}), + + // 3d print + sliceHeight: UC.newInput("slice height", {title:"millimeters", convert:UC.toFloat, modes:FDM}), + sliceShells: UC.newInput("shell count", {convert:UC.toInt, modes:FDM}), + sliceShellSpacing: UC.newInput("shell spacing", {title:"as a percentage of nozzle width\n< 1.0 causes shell overlap\nrecommended 0.85 - 1.0", convert:UC.toFloat, bound:UC.bound(0.5,1.0), modes:FDM}), + sliceFillOverlap: UC.newInput("fill overlap", {title:"overlap with shell\nas % of nozzle width\nhigher bonds better\n0.0 - 1.0", convert:UC.toFloat, bound:UC.bound(0.0,2.0), modes:FDM}), + sliceFillDensity: UC.newInput("fill density", {title:"fill line density\n0.0 - 2.0", convert:UC.toFloat, bound:UC.bound(0.0,2.0), modes:FDM}), + sliceFillAngle: UC.newInput("fill angle", {title:"base angle in degrees", convert:UC.toFloat, modes:FDM}), + sliceFillSparse: UC.newInput("infill %", {title:"for solid layers\n0.0 - 1.0", convert:UC.toFloat, bound:UC.bound(0.0,1.0), modes:FDM}), + sliceSolidMinArea: UC.newInput("solid area", {title:"minimum area (cm^2)\nrequired to keep solid\nmust be > 0.1", convert:UC.toFloat, modes:FDM}), + sliceSolidLayers: UC.newInput("solid layers", {title:"solid fill projections\nbased on infill deltas", convert:UC.toInt, modes:FDM}), + sliceBottomLayers: UC.newInput("base layers", {title:"base solid layers", convert:UC.toInt, modes:FDM}), + sliceTopLayers: UC.newInput("top layers", {title:"top solid layers", convert:UC.toInt, modes:FDM}), + sliceVase: UC.newBoolean("vase mode", onBooleanClick, {modes:FDM}), + + // laser + laserOffset: UC.newInput("cut offset", {title:"millimeters\nadjust for beam width", convert:UC.toFloat, modes:LASER}), + laserSliceHeight: UC.newInput("layer height", {title:"millimeters\n0 = auto/detect", convert:UC.toFloat, modes:LASER}), + + // cam + roughing: UC.newGroup("roughing", null, {modes:CAM}), + roughingTool: UC.newSelectField("tool", {modes:CAM}), + roughingSpindle: UC.newInput("spindle rpm", {title:"spindle speed rpm", convert:UC.toInt, modes:CAM}), + roughingOver: UC.newInput("step over", {title:"0.1 - 1.0\npercentage of\ntool diameter", convert:UC.toFloat, bound:UC.bound(0.1,1.0), modes:CAM}), + roughingDown: UC.newInput("step down", {title:"step down depth for\neach roughing pass\nin millimeters\n0 to disable", convert:UC.toFloat, modes:CAM}), + roughingSpeed: UC.newInput("feed rate", {title:"max speed while cutting\nmillimeters / minute", convert:UC.toInt, modes:CAM}), + roughingPlunge: UC.newInput("plunge rate", {title:"max speed on z axis\nmillimeters / minute", convert:UC.toInt, modes:CAM}), + roughingStock: UC.newInput("leave stock", {title:"horizontal offset from vertical faces\nstock to leave for finishing pass\nin millimeters", convert:UC.toFloat, modes:CAM}), + roughingOn: UC.newBoolean("enable", onBooleanClick, {modes:CAM}), + + // cam + finishing: UC.newGroup("finishing", null, {modes:CAM}), + finishingTool: UC.newSelectField("tool", {modes:CAM}), + finishingSpindle: UC.newInput("spindle rpm", {title:"spindle speed rpm", convert:UC.toInt, modes:CAM}), + finishingOver: UC.newInput("step over", {title:"0.05 - 1.0\npercentage of\ntool diameter", convert:UC.toFloat, bound:UC.bound(0.05,1.0), modes:CAM}), + finishingDown: UC.newInput("step down", {title:"step down depth for\neach roughing pass\nin millimeters\n0 to disable", convert:UC.toFloat, modes:CAM}), + finishingAngle: UC.newInput("max angle", {title:"angles greater than this\nare considered vertical", convert:UC.toFloat, bound:UC.bound(45,90), modes:CAM}), + finishingSpeed: UC.newInput("feed rate", {title:"max speed while cutting\nmillimeters / minute", convert:UC.toInt, modes:CAM}), + finishingPlunge: UC.newInput("plunge rate", {title:"max speed on z axis\nmillimeters / minute", convert:UC.toInt, modes:CAM}), + finishingOn: UC.newBoolean("waterline", onBooleanClick, {title:"contour finishing\ndisabled when pocketing", modes:CAM}), + finishingXOn: UC.newBoolean("linear x", onBooleanClick, {title:"linear x-axis finishing", modes:CAM}), + finishingYOn: UC.newBoolean("linear y", onBooleanClick, {title:"linear y-axis finishing", modes:CAM}), + finishCurvesOnly: UC.newBoolean("curves only", onBooleanClick, {title:"limit linear cleanup\nto curved surfaces\nto reduce time", modes:CAM}), + + // cam + drilling: UC.newGroup("drilling", null, {modes:CAM}), + drillTool: UC.newSelectField("tool", {modes:CAM}), + drillSpindle: UC.newInput("spindle rpm", {title:"spindle speed rpm", convert:UC.toInt, modes:CAM}), + drillDown: UC.newInput("plunge per", {title:"max plunge between\ndwell periods\nin millimeters\n0 to disable", convert:UC.toFloat, modes:CAM}), + drillDownSpeed: UC.newInput("plunge rate", {title:"plunge rate\nin millimeters / minute\n0 to disable", convert:UC.toFloat, modes:CAM}), + drillDwell: UC.newInput("dwell time", {title:"dwell time\nbetween plunges in\nin milliseconds", convert:UC.toFloat, modes:CAM}), + drillLift: UC.newInput("drill lift", {title:"lift between plunges\nafter dwell period\nin millimeters\n0 to disable", convert:UC.toFloat, modes:CAM}), + drillingOn: UC.newBoolean("enable", onBooleanClick, {modes:CAM}), + + // cam tabs + camTabs: UC.newGroup("cutout tabs", null, {modes:CAM}), + camTabsWidth: UC.newInput("width", {title:"width in millimeters\nperpendicular to part", convert:UC.toFloat, bound:UC.bound(1,100), modes:CAM}), + camTabsHeight: UC.newInput("height", {title:"height in millimeters\nfrom part bottom", convert:UC.toFloat, bound:UC.bound(1,100), modes:CAM}), + camTabsOn: UC.newBoolean("enable", onBooleanClick, {title:"enable or disable tabs\ntab generation skipped when\npocket only mode enabled", modes:CAM}), + + output: UC.newGroup("output"), + outputTileSpacing: UC.newInput("spacing", {title:"millimeters\ndistance between layer output", convert:UC.toInt, modes:LASER}), + outputTileScaling: UC.newInput("scaling", {title:"multiplier (0.1 to 100)", convert:UC.toInt, bound:UC.bound(0.1,100), modes:LASER}), + outputLaserPower: UC.newInput("power", {title:"0 - 100 %", convert:UC.toInt, bound:UC.bound(1,100), modes:LASER}), + outputLaserSpeed: UC.newInput("speed", {title:"millimeters / minute", convert:UC.toInt, modes:LASER}), + + outputBedTemp: UC.newInput("bed temp", {title:"degrees celsius", convert:UC.toInt, modes:FDM}), + outputTemp: UC.newInput("nozzle temp", {title:"degrees celsius", convert:UC.toInt, modes:FDM}), + outputFeedrate: UC.newInput("print speed", {title:"print move max speed\nmillimeters / minute", convert:UC.toInt, modes:FDM}), + outputSeekrate: UC.newInput("move speed", {title:"non-print move max speed\nmillimeters / minute", convert:UC.toInt, modes:FDM}), + // outputShellSpeed: UC.newInput("shell speed", {title:"speed multiplier\n0.1 - 2.0", convert:UC.toFloat, bound:UC.bound(0.1,2.0), modes:FDM}), + outputShellMult: UC.newInput("shell factor", {title:"extrusion multiplier\n0.0 - 2.0", convert:UC.toFloat, bound:UC.bound(0.0,2.0), modes:FDM}), + outputFillMult: UC.newInput("fill factor", {title:"extrusion multiplier\n0.0 - 2.0", convert:UC.toFloat, bound:UC.bound(0.0,2.0), modes:FDM}), + + // cam + camTolerance: UC.newInput("tolerance", {title:"surface precision\nin millimeters", convert:UC.toFloat, bound:UC.bound(0.05,1.0), modes:CAM}), + camZTopOffset: UC.newInput("z top offset", {title:"offset from stock surface\nto top face of part\nin millimeters", convert:UC.toFloat, modes:CAM}), + camZBottom: UC.newInput("z bottom", {title:"offset from part bottom\nto limit cutting depth\nin millimeters", convert:UC.toFloat, modes:CAM}), + camZClearance: UC.newInput("z clearance", {title:"travel offset from z top\nin millimeters", convert:UC.toFloat, bound:UC.bound(1,100), modes:CAM}), + camPocketOnly: UC.newBoolean("pocket only", onBooleanClick, {title:"constrain to\npart boundaries", modes:CAM}), + // camEaseDown: UC.newBoolean("ease down", onBooleanClick, {title:"rough plunge cuts\nwill spiral down", modes:CAM}), + camDepthFirst: UC.newBoolean("depth first", onBooleanClick, {title:"optimize pocket cuts\nwith depth priority", modes:CAM}), + outputClockwise: UC.newBoolean("clockwise", onBooleanClick, {title:"waterline milling direction", modes:CAM}), + + // cam stock + camStock: UC.newGroup("stock", null, {modes:CAM}), + camStockX: UC.newInput("width", {title:"width in millimeters\n0 defaults to part size", convert:UC.toFloat, bound:UC.bound(0,9999), modes:CAM}), + camStockY: UC.newInput("depth", {title:"depth in millimeters\n0 defaults to part size", convert:UC.toFloat, bound:UC.bound(0,9999), modes:CAM}), + camStockZ: UC.newInput("height", {title:"depth in millimeters\n0 defaults to part size", convert:UC.toFloat, bound:UC.bound(0,9999), modes:CAM}), + outputOriginCenter: UC.newBoolean("origin center", onBooleanClick, {modes:CAM_LASER}), + camOriginTop: UC.newBoolean("origin top", onBooleanClick, {modes:CAM}), + + support: UC.newGroup("supports", null, {modes:FDM}), + sliceSupportDensity: UC.newInput("density", {title:"0.0 - 1.0\nrecommended 0.15\n0 to disable", convert:UC.toFloat, bound:UC.bound(0.05,1.0), modes:FDM}), + sliceSupportSize: UC.newInput("pillar size", {title:"width in millimeters", bound:UC.bound(1.0,200.0), convert:UC.toFloat, modes:FDM}), + sliceSupportOffset: UC.newInput("part offset", {title:"millimeters\noffset from part", bound:UC.bound(0.0,200.0), convert:UC.toFloat, modes:FDM}), + sliceSupportSpan: UC.newInput("max bridge", {title:"span length that\ntriggers support\nin millimeters", bound:UC.bound(0.0,200.0), convert:UC.toFloat, modes:FDM}), + sliceSupportArea: UC.newInput("min area", {title:"min area for a\nsupport column\nin millimeters", bound:UC.bound(0.1,200.0), convert:UC.toFloat, modes:FDM}), + sliceSupportExtra: UC.newInput("expand", {title:"expand support area\nbeyond part boundary\nin millimeters", bound:UC.bound(0.0,200.0), convert:UC.toFloat, modes:FDM}), + sliceSupportEnable: UC.newBoolean("enable", onBooleanClick, {modes:FDM}), + + firstLayer: UC.newGroup("first layer", null, {modes:FDM}), + firstLayerHeight: UC.newInput("height factor", {title:"0.5 - 2.5\nmultiplier on slice height\nshould be no more\nthan 80% nozzle diam", convert:UC.toFloat, modes:FDM}), + firstLayerSpeed: UC.newInput("speed factor", {title:"0.1 - 1.0\nmultiplier on print speed", convert:UC.toFloat, modes:FDM}), + outputBrimCount: UC.newInput("skirt count", {title:"number of skirts", convert:UC.toInt, modes:FDM}), + outputBrimOffset: UC.newInput("skirt offset", {title:"millimeters", convert:UC.toFloat, modes:FDM}), + + advanced: UC.newGroup("advanced", null, {modes:FDM}), + outputRetractOver: UC.newInput("retract over", {title:"retract filament for\nnon-print moves\nover this distance\n0 = disable", convert:UC.toFloat, modes:FDM}), + outputRetractDist: UC.newInput("retract dist", {title:"amount to retract filament", convert:UC.toFloat, modes:FDM}), + outputRetractSpeed: UC.newInput("retract rate", {title:"speed of filament\nretraction in mm/s", convert:UC.toInt, modes:FDM}), + outputWipeDistance: UC.newInput("wipe distance", {title:"wipe at end of loop\nin millimeters", convert:UC.toFloat, modes:FDM}), + outputShortDistance: UC.newInput("short distance", {title:"segment length cutoff\nfor short segments\nin millimeters", bound:UC.bound(0,200), convert:UC.toFloat, modes:FDM}), + outputShortFactor: UC.newInput("short factor", {title:"max speed reduction factor\nfor short segments\nas % of print speed", bound:UC.bound(0.05,1), convert:UC.toFloat, modes:FDM}), + }); + + function toolUpdate(a,b,c) { + DBUG.log(['toolUpdate',a,b,c]) + } + + function invertMouse() { + settings.controller.reverseZoom = UI.reverseZoom.checked; + SPACE.view.setZoom(settings.controller.reverseZoom, settings.controller.zoomSpeed); + saveSettings(); + } + + function onBooleanClick() { + updateSettings(); + showSlices(); + } + + function clearSelectedText() { + var sel = WIN.getSelection(); + if (sel && sel.rangeCount) sel.collapseToStart(); + } + + function keyUpHandler(evt) { + switch (evt.keyCode) { + // escape + case 27: + // clear text selection + clearSelectedText(); + // dismiss modals + hideModal(); + // deselect widgets + widgetDeselect(); + // hide all dialogs + hideDialog(); + // cancel slicing + KIRI.work.cancel(); + break; + } + return false; + } + + function inputHasFocus() { + var active = DOC.activeElement; + return active && (active.nodeName === "INPUT" || active.nodeName === "TEXTAREA"); + } + + function textAreaHasFocus() { + var active = DOC.activeElement; + return active && active.nodeName === "TEXTAREA"; + } + + function inputSize() { + return parseInt(DOC.activeElement.size); + } + + function keyDownHandler(evt) { + if (modalShowing()) return false; + var move = evt.altKey ? 5 : 0, + deg = move ? 0 : -Math.PI / (evt.shiftKey ? 36 : 2); + switch (evt.keyCode) { + case 8: // delete + if (inputHasFocus()) return false; + if (selectedMeshes.length > 0) { + platformDelete(selectedMeshes); + } + evt.preventDefault(); + break; + case 37: // left arrow + if (inputHasFocus()) return false; + // if (selectedMeshes.length === 0) return; + if (deg) rotateSelection(0, 0, -deg); + if (move > 0) moveSelection(-move, 0, 0); + evt.preventDefault(); + break; + case 39: // right arrow + if (inputHasFocus()) return false; + // if (selectedMeshes.length === 0) return; + if (deg) rotateSelection(0, 0, deg); + if (move > 0) moveSelection(move, 0, 0); + evt.preventDefault(); + break; + case 38: // up arrow + if (inputHasFocus()) return false; + if (evt.metaKey) return setVisibleLayer(showLayerValue+1); + // if (selectedMeshes.length === 0) return; + if (deg) rotateSelection(deg, 0, 0); + if (move > 0) moveSelection(0, move, 0); + evt.preventDefault(); + break; + case 40: // down arrow + if (inputHasFocus()) return false; + if (evt.metaKey) return setVisibleLayer(showLayerValue-1); + // if (selectedMeshes.length === 0) return; + if (deg) rotateSelection(-deg, 0, 0); + if (move > 0) moveSelection(0, -move, 0); + evt.preventDefault(); + break; + case 65: // 'a' for select all + if (evt.metaKey) { + if (inputHasFocus()) return false; + evt.preventDefault(); + widgetDeselect(); + platformSelectAll(); + clearSelectedText(); + } + break; + case 83: // 's' for save workspace + if (evt.ctrlKey) { + evt.preventDefault(); + saveSettings(); + log("settings saved"); + } else + if (evt.metaKey) { + evt.preventDefault(); + saveWorkspace(); + } + break; + case 76: // 'l' for restore workspace + if (evt.metaKey) { + evt.preventDefault(); + restoreWorkspace(); + } + break; + } + } + + function cca(c) { + return c.charCodeAt(0); + } + + function keyHandler(evt) { + var handled = true, + style, sel, i, m, bb, + ncc = evt.charCode - 48; + if (modalShowing()) return false; + if (inputHasFocus() && (inputSize() > 10 || (ncc >= 0 && ncc <= 9))) { + return false; + } + switch (evt.charCode) { + case cca('`'): showSlices(0); break; + case cca('0'): showSlices(showLayerMax); break; + case cca('1'): // toggle control left + if (evt.ctrlKey) { + style = UI.ctrlLeft.style; + style.display = style.display === 'none' ? 'block' : 'none'; + } else { + showSlices(showLayerMax/10); + } + break; + case cca('2'): // toggle control right + if (evt.ctrlKey) { + style = UI.ctrlRight.style; + style.display = style.display === 'none' ? 'block' : 'none'; + } else { + showSlices(showLayerMax*2/10); + } + break; + case cca('3'): + if (evt.ctrlKey) { + style = !SPACE.platform.isHidden(); + SPACE.platform.setHidden(style); + SPACE.platform.showGrid(!style); + SPACE.update(); + } else { + showSlices(showLayerMax*3/10); + } + break; + case cca('4'): showSlices(showLayerMax*4/10); break; + case cca('5'): showSlices(showLayerMax*5/10); break; + case cca('6'): showSlices(showLayerMax*6/10); break; + case cca('7'): showSlices(showLayerMax*7/10); break; + case cca('8'): showSlices(showLayerMax*8/10); break; + case cca('9'): showSlices(showLayerMax*9/10); break; + case cca('?'): + showHelpLocal(); + break; + case cca('Z'): // reset stored state + if (confirm('clear all settings?')) { + SDB.clear(); + } + break; + case cca('C'): // refresh catalog + CATALOG.refresh(); + break; + case cca('c'): // open control window + showSerial(true); + break; + case cca('s'): // complete slice + prepareSlices(); + break; + case cca('p'): // prepare print + preparePrint(); + break; + case cca('P'): // position widget + positionSelection(); + break; + case cca('x'): // export print + exportPrint(); + break; + case cca('e'): // devices + showDevices(); + break; + case cca('o'): // tools + showTools(); + break; + case cca('v'): // toggle single slice view mode + UI.layerRange.checked = !UI.layerRange.checked; + showSlices(); + break; + case cca('d'): // duplicate object + sel = selectedMeshes.slice(); + widgetDeselect(); + for (i=0; i= 0) { + UI.deviceSelect.selectedIndex = selectedIndex; + selectDevice(selected); + } else { + UI.deviceSelect.selectedIndex = 0; + selectDevice(devices[0]); + } + + showDialog('devices', true); + onWindowResize(); + + UI.deviceSelect.focus(); + } + + function renderTools() { + UI.toolSelect.innerHTML = ''; + editTools.forEach(function(tool, index) { + tool.order = index; + var opt = DOC.createElement('option'); + opt.appendChild(DOC.createTextNode(tool.name)); + opt.onclick = function() { selectTool(tool) }; + UI.toolSelect.appendChild(opt); + }); + } + + function selectTool(tool) { + selectedTool = tool; + UI.toolName.value = tool.name; + UI.toolNum.value = tool.number; + UI.toolFluteDiam.value = tool.flute_diam; + UI.toolFluteLen.value = tool.flute_len; + UI.toolShaftDiam.value = tool.shaft_diam; + UI.toolShaftLen.value = tool.shaft_len; + UI.toolMetric.checked = tool.metric; + UI.toolType.selectedIndex = ( + tool.type === 'endmill' ? 0 : + tool.type === 'ballmill' ? 1 : + -1 + ); + } + + function updateTool() { + selectedTool.name = UI.toolName.value; + selectedTool.number = parseInt(UI.toolNum.value); + selectedTool.flute_diam = parseFloat(UI.toolFluteDiam.value); + selectedTool.flute_len = parseFloat(UI.toolFluteLen.value); + selectedTool.shaft_diam = parseFloat(UI.toolShaftDiam.value); + selectedTool.shaft_len = parseFloat(UI.toolShaftLen.value); + selectedTool.metric = UI.toolMetric.checked; + selectedTool.type = ['endmill','ballmill'][UI.toolType.selectedIndex]; + renderTools(); + UI.toolSelect.selectedIndex = selectedTool.order; + editTools.changed = true; + } + + function showTools() { + if (MODE !== MODES.CAM) return; + + var selectedIndex = null; + + editTools = settings.tools.slice(); + + UI.toolsClose.onclick = function() { + if (editTools.changed && !confirm("abandon changes?")) return; + hideDialog(); + }; + UI.toolAdd.onclick = function() { + editTools.push({ + id: UTIL.time(), + number: editTools.length, + name: "new", + type: "endmill", + flute_diam: 0.25, + flute_len: 1, + shaft_diam: 0.25, + shaft_len: 3, + metric: false + }); + editTools.changed = true; + renderTools(); + UI.toolSelect.selectedIndex = editTools.length-1; + selectTool(editTools[editTools.length-1]); + }; + UI.toolDelete.onclick = function() { + editTools.remove(selectedTool); + editTools.changed = true; + renderTools(); + }; + UI.toolsSave.onclick = function() { + editTools.changed = false; + if (selectedTool) updateTool(); + settings.tools = editTools; + saveSettings(); + updateFields(); + hideDialog(); + triggerSettingsEvent(); + }; + + renderTools(); + if (editTools.length > 0) { + selectTool(editTools[0]); + UI.toolSelect.selectedIndex = 0; + } else { + UI.toolAdd.onclick(); + } + + showDialog('tools'); + UI.toolSelect.focus(); + } + + function showDevices() { + if (MODE === MODES.LASER) return; + + setViewMode(VIEWS.ARRANGE); + + ajax("/api/filters-"+getMode().toLowerCase(), function(flvalue) { + if (!flvalue) return; + renderDevices(js2o(flvalue)); + STATS.add('d-gcode'); + }); + } + + function dragOverHandler(evt) { + evt.stopPropagation(); + evt.preventDefault(); + evt.dataTransfer.dropEffect = 'copy'; + SPACE.platform.setColor(0x00ff00); + } + + function dragLeave() { + SPACE.platform.setColor(0x555555); + } + + function dropHandler(evt) { + evt.stopPropagation(); + evt.preventDefault(); + + SPACE.platform.setColor(0x555555); + + var files = evt.dataTransfer.files, + plate = files.length < 5 || confirm('add objects to table?'); + + if (plate) loadFiles(files); + } + + function loadCatalogFile(e) { + Widget.loadFromCatalog(e.target.getAttribute('load'), function(widget) { + platformAdd(widget); + hideDialog(); + }); + } + + function deleteCatalogFile(e) { + CATALOG.deleteFile(e.target.getAttribute('del')); + } + + function updateCatalog(files) { + var table = UI.catalogList, + list = []; + table.innerHTML = ''; + for (var name in files) { + list.push({n:name, ln:name.toLowerCase(), v:files[name].vertices, t:files[name].updated}); + } + list.sort(function(a,b) { + return a.ln < b.ln ? -1 : 1; + }); + for (var i=0; i 0; + } + }); + + function checkSeed(ondone) { + // skip sample object load in onshape (or any script postload) + if (!SDB[SEED] && !SETUP.s) { + loadSTL("/obj/cube.stl", function(vert) { + CATALOG.putFile("sample cube.stl", vert); + SDB[SEED] = new Date().getTime(); + platformComputeMaxZ(); + SPACE.view.home(); + setTimeout(saveWorkspace,500); + ondone(); + showHelpLocal(); + }); + return true; + } + return false; + } + + function ondone() { + widgetDeselect(); + CATALOG.addFileListener(updateCatalog); + SPACE.view.setZoom(settings.controller.reverseZoom, settings.controller.zoomSpeed); + SPACE.platform.setZOff(0.2); + + if (SETUP.s) SETUP.s.forEach(function(lib) { + var scr = DOC.createElement('script'); + scr.setAttribute('async',true); + scr.setAttribute('src','/code/'+lib+'.js'); + DOC.body.appendChild(scr); + STATS.add('l-'+lib); + }); + + if (SETUP.ss) SETUP.ss.forEach(function(style) { + var ss = DOC.createElement('link'); + ss.setAttribute("type", "text/css"); + ss.setAttribute("rel", "stylesheet"); + ss.setAttribute("href", "/kiri/style-"+style+".css"); + DOC.body.appendChild(ss); + }); + + if (SETUP.v) SETUP.v.forEach(function(kv) { + kv = kv.split('='); + SDB.setItem(kv[0],kv[1]); + }); + + // import octoprint settings from url + if (SETUP.ophost) { + OCTOPRINT = { + host: SETUP.ophost[0], + apik: SETUP.opkey ? SETUP.opkey[0] : '' + }; + SDB['octo-host'] = OCTOPRINT.host; + SDB['octo-apik'] = OCTOPRINT.apik; + console.log({octoprint:OCTOPRINT}); + } + + // mode passed in on url + var SETMODE = SETUP.mode ? SETUP.mode[0] : null; + + setMode(SETMODE || STARTMODE || settings.mode, SETMODE); + setControlsVisible(true); + updatePlatformSize(); + takeFocus(); + + if (STATS.get('upgrade')) DBUG.log("kiri | version upgrade"); + STATS.del('upgrade'); + + if (!SETUP.s) console.log("kiri | init main | "+kiri.version); + STATS.set('seed', SDB[SEED]); + STATS.add('init'); + // updateActive(true); + + var zdebug = DBUG.get('z-index'); + if (zdebug) { + prepareSlices(function() { + UI.layerRange.checked = true; + showSlices(parseInt(zdebug)); + }); + } + + // init gcode sender + KIRI.serial.init(); + + // place version number a couple of places to help users + UI.helpButton.title = "version " + KIRI.version; + + // setup tab visibility watcher + // DOC.addEventListener('visibilitychange', function() { document.title = document.hidden }); + + // ensure settings has gcode + selectDevice(currentDeviceName()); + + // ensure field data propagation + updateSettings(); + + // set initial layer slider size + onControlResize(); + + // send init-done event + sendOnEvent('init-done', STATS); + } + + restoreWorkspace(ondone) || checkSeed(ondone) || ondone(); + + } // end init() + + SPACE.addEventListener(DOC, 'DOMContentLoaded', function () { init() }, false); + SPACE.addEventListener(WIN, 'mousemove', function() { mouseMoved = true }); + + // prevent safari from exiting full screen mode + DOC.onkeydown = function (evt) { + if (evt.keyCode == 27) evt.preventDefault(); + } + + // run optional module functions + if (Array.isArray(self.kirimod)) { + kirimod.forEach(function(mod) { mod(kiri.api) }); + } +})(); diff --git a/js/license.js b/js/license.js new file mode 100644 index 00000000..d5c5d593 --- /dev/null +++ b/js/license.js @@ -0,0 +1,7 @@ +var exports = { + COPYRIGHT:"Copyright (C) 2014-2017 Stewart Allen - All Rights Reserved", + LICENSE:"Unauthorized copying, modification and re-distribution are strictly prohibited without prior written consent.", + VERSION:"1.0.59" +}; +if (!module) var module = {}; +module.exports = exports; diff --git a/js/meta.js b/js/meta.js new file mode 100644 index 00000000..fdca2d17 --- /dev/null +++ b/js/meta.js @@ -0,0 +1,2136 @@ +/** + * Copyright 2014-2017 Stewart Allen -- All Rights Reserved + * + * TODO add history segment on right for quick rollback + * TODO save-as for harder fork of model + * TODO add support for hiding / showing selections (ghosting / unselectable) + * TODO select plane vs face to fix plane/face marking (peril w/ nonflat) + * TODO show/hide selection (to edit around, prevent mod, etc) + * TODO make synth more efficient by only rebuilding if a relevant node has changed + * TODO grouping and group naming (helps download / batch download)? + * TODO bit encoding + stl instead of xyz + * TODO undo/redo stack ... and revert to previous save hotkey + * TODO improve paint/preproc and preview modes + * TODO auto-save of workspace, selection and undo/redo stack (on idle) + * TODO mark "hard" saves and auto-cleanup "soft/auto" saves after some time + * TODO add selection rotation + * TODO add reserved spaces (links) for signups + * TODO shared live-edit workspaces? + * TODO add OBJ export (http://en.wikipedia.org/wiki/Wavefront_.obj_file) + * TODO add parametric relations (via marks?) w/ var table + * TODO l&f and import stl from http://openjscad.org/ + */ + +"use strict"; + +var gs_meta = { + copyright:"stewart allen -- all rights reserved", + sendTo:"http://grid.space" +}; + +THREE.Material.prototype.motoSetup = function() { + this.fog = false; + var hidden = this.clone(); + this.m_hide = hidden; + this.m_show = this; + hidden.m_hide = hidden; + hidden.m_show = this; + hidden.visible = false; + return this; +}; + +THREE.MeshFaceMaterial.prototype.mSet = function(idx, mat) { + this.materials[idx] = mat; +}; + +THREE.MeshFaceMaterial.prototype.mVisible = function(idx, show) { + this.materials[idx] = show ? this.materials[idx].m_show : this.materials[idx].m_hide; +}; + +THREE.Face3.prototype.mVisible = function(show) { + this.onface.cube.showHideFace(this.materialIndex, show); +}; + +(function() { + var $ = function(id) { return document.getElementById(id)}, + // --------------- + WIN = self, + DOC = WIN.document, + LOC = WIN.location, + ABS = Math.abs, + MAX = Math.max, + MIN = Math.min, + BOUND = function(v,min,max) { return MAX(min,MIN(max,v))}, + ROUND = Math.round, + PI = Math.PI, + PI2 = PI/2, + SCALE = 1, + HALF = SCALE / 2, + HAS = function(a,b) { return a.hasOwnProperty(b) }, + // --------------- + MOTO = self.moto, + SPACE = MOTO.Space, + SDB = MOTO.KV, + MDB = kiri.openCatalog(new MOTO.Storage('meta')), + KDB = kiri.openCatalog(new MOTO.Storage('kiri')), + // --------------- + CP = Cube.prototype, + BP = Bounds.prototype, + // --------------- + FACES = ['bt','tb','lr','rl','fb','bf'], + FACE = { + rl: { mi:0, x: HALF, y:0, z:0, rx:0, ry:PI2, rz:0, ox: 1, oy:0, oz:0, tx:PI2, ty:0, tz:0 }, + lr: { mi:1, x:-HALF, y:0, z:0, rx:0, ry:PI2, rz:0, ox:-1, oy:0, oz:0, tx:PI2, ty:0, tz:0 }, + bf: { mi:2, x:0, y:-HALF, z:0, rx:0, ry:0, rz:0, ox:0, oy: 1, oz:0, tx:PI2, ty:0, tz:0 }, + fb: { mi:3, x:0, y: HALF, z:0, rx:0, ry:0, rz:0, ox:0, oy:-1, oz:0, tx:PI2, ty:0, tz:0 }, + tb: { mi:4, x:0, y:0, z: HALF, rx:PI2, ry:0, rz:0, ox:0, oy:0, oz: 1, tx:0, ty:0, tz:0 }, + bt: { mi:5, x:0, y:0, z:-HALF, rx:PI2, ry:0, rz:0, ox:0, oy:0, oz:-1, tx:0, ty:0, tz:0 } + }, + FACEROT = { + '00':{x:0,y:1,z:2}, + '01':{x:0,y:1,z:3}, + '02':{x:0,y:1,z:0}, + '03':{x:0,y:1,z:1}, + '10':{x:3,y:0,z:0}, + '11':{x:3,y:0,z:1}, + '12':{x:3,y:0,z:2}, + '13':{x:3,y:0,z:3}, + '20':{x:0,y:0,z:1}, + '21':{x:0,y:0,z:2}, + '22':{x:0,y:0,z:3}, + '23':{x:0,y:0,z:0}, + '30':{x:0,y:3,z:0}, + '31':{x:0,y:3,z:3}, + '32':{x:0,y:3,z:2}, + '33':{x:0,y:3,z:1}, + '40':{x:1,y:0,z:0}, + '41':{x:1,y:0,z:3}, + '42':{x:1,y:0,z:2}, + '43':{x:1,y:0,z:1}, + '50':{x:2,y:0,z:1}, + '51':{x:2,y:0,z:2}, + '52':{x:2,y:0,z:3}, + '53':{x:2,y:0,z:0} + }, + // --------------- + EDIT = { + ADD: 1, + SELECT: 2, + DELETE: 3, + CLONE: 4, + MARK: 5 + }, + SELECT = { + PLANE: 1, + REGION: 2, + JOINED: 3, + DRILL: 4, + CUBE: 5 + }, + MARK = { + NONE: 1, + EMIT: 2, + SLIDE: 3, + CLEAR: 5 + }, + COLOR = { + ADD: 0x00ff00, + SELECT: 0xffff00, + DELETE: 0xff0000, + CLONE: 0xff00ff, + EMIT: 0x555555, + SLIDE: 0xcccccc, + CLEAR: 0xffffff, + DEFAULT: 0xdddddd + }, + // --------------- + boxMaterial = new THREE.MeshPhongMaterial({ + side:THREE.DoubleSide, + color: 0x00ff00, + specular: 0x111111, + transparent: false, + shininess: 100, + opacity: 0.8 + }).motoSetup(), + boxMaterialSelected = new THREE.MeshPhongMaterial({ + side:THREE.DoubleSide, + color: 0xffff00, + specular: 0x111111, + transparent: true, + shininess: 100, + opacity: 0.6 + }).motoSetup(), + boxMaterialSelectedSynthetic = new THREE.MeshPhongMaterial({ + side:THREE.DoubleSide, + color: 0xffff77, + specular: 0x111111, + transparent: true, + shininess: 100, + opacity: 0.6 + }).motoSetup(), + faceMaterialSelected = new THREE.MeshPhongMaterial({ + side:THREE.DoubleSide, + color: 0xffbb00, + specular: 0x111111, + transparent: true, + shininess: 100, + opacity: 0.8 + }).motoSetup(), + faceMaterialSlide = new THREE.MeshPhongMaterial({ + side:THREE.DoubleSide, + color: COLOR.SLIDE, + specular: 0x111111, + transparent: true, + shininess: 100, + opacity: 0.8 + }).motoSetup(), + faceMaterialEmit = new THREE.MeshPhongMaterial({ + side:THREE.DoubleSide, + color: COLOR.EMIT, + specular: 0x111111, + transparent: true, + shininess: 100, + opacity: 0.8 + }).motoSetup(), + faceMaterialInside = new THREE.MeshPhongMaterial({ + side:THREE.DoubleSide, + color: 0xffffff, + specular: 0x111111, + transparent: true, + shininess: 100, + opacity: 0.0 + }).motoSetup(), + faceMaterialSynthetic = new THREE.MeshPhongMaterial({ + side:THREE.DoubleSide, + color: 0x55ff55, + specular: 0x111111, + transparent: false, + shininess: 100, + opacity: 0.6 + }).motoSetup(), + hoverMaterial = new THREE.MeshBasicMaterial({ + color: 0x0, + opacity: 0.75, + transparent: true, + side: THREE.DoubleSide + }).motoSetup(), + // --------------- + UI = {}, + SPACES = [], + SPACENAMES = {}, + LIBCACHE = {}, + HASHMATCH = null, + DATA = "/data/", + // --------------- + hashWatcher = null, + selectMode = SELECT.REGION, + editMode = EDIT.SELECT, + markMode = MARK.EMIT, + updateFacesScheduled = false, + updateSynthetic = true, + enablePost = true, + dragCopy = false, + selectedFace = null, + hoverSpot = null, + // --------------- + editModeButtons = {}, + selectModeButtons = {}, + markTypeButtons = {}, + // --------------- + spaceUnits = null, + spaceName = 'untitled', + spaceID = null, + spaceVer = 0, + gridSize = 5, + matrix = {}, + selected = [], // cubes + selectable = [], // meshes + selectedBounds = new Bounds(); + + /** ****************************************************************** + * LETS_GET_THIS_PARTY_STARTED() + ******************************************************************* */ + + if (MOTO.KV.__mem__) alert( + "browser is blocking local storage or\n"+ + "3rd party cookies required to store\n" + + "application state."); + + SPACE.addEventListener(DOC, 'DOMContentLoaded', metaInit, false); + + function metaInit() { + SPACE.showSkyGrid(false); + SPACE.setSkyColor(0xf8f8f8); + + SPACE.init($('container')); + SPACE.selectRecurse(true); + SPACE.useDefaultKeys(false); + + SPACE.platform.setSize(300,300,0.5); + SPACE.platform.setHiding(true); + SPACE.platform.setHidden(true); + SPACE.platform.setZOff(0.55); + SPACE.platform.setGZOff(0.3); + SPACE.platform.opacity(0.2); + + hoverSpot = new THREE.Mesh(new THREE.PlaneBufferGeometry(0.8, 0.8, 1, 1), hoverMaterial); + SPACE.scene.add(hoverSpot); + + SPACE.platform.onHover(function(point) { + hoverSpot.visible = true; + hoverSpot.position.set(ROUND(point.x), -0.5, ROUND(point.z)); + hoverSpot.rotation.set(PI2, 0, 0); + SPACE.update(25); + }); + + SPACE.platform.onClick(function(point) { + if (editMode === EDIT.ADD) new Cube(ROUND(point.x), -ROUND(point.z), 0).add(); + }); + + SPACE.mouse.downSelect(function(selection, event) { + dragCopy = selection && event.metaKey; + return selectedFace ? [ selectedFace.cube.boxM ] : null; + }); + + SPACE.mouse.upSelect(function(selection, event) { + if (event) { + if (selection && selection.faceIndex) { + var face = selection.face.onface, + cube = face.cube; + switch (editMode) { + case EDIT.MARK: + if (!cube.isSynthetic()) markFace(face); + scheduleUpdateFaces(cube, face); + break; + case EDIT.SELECT: + var shift = event.shiftKey, + meta = event.metaKey, + isSelected = cube.isSelected(); + if (shift) { + selectRegion(face, true, meta ? SELECT.REGION : selectMode); + } else if (!isSelected) { + clearSelections(); + selectRegion(face, true, meta ? SELECT.REGION : selectMode); + } + if (cube.isSelected()) { + selectedFace = face; + scheduleUpdateFaces(cube, face); + } + var s = face.set, rot = { x: s.tx, y: s.ty, z: s.tz }; + SPACE.alignTracking(selection.point, rot, {x:-ABS(s.ox), y:-ABS(s.oz), z:-ABS(s.oy)}); + break; + case EDIT.ADD: + clearSelections(); + cube.newAdjacent(face).add(); + break; + case EDIT.CLONE: + clearSelections(); + cube.newAdjacent(face).add().cloneFrom(cube); + break; + case EDIT.DELETE: + if (cube.isSynthetic()) return; + clearSelections(); + removeCube(cube); + break; + } + } else { + clearSelections(); + } + } + return selectable; + }); + + SPACE.mouse.onDrag(function(delta, offset, end) { + if (offset) moveSelection(offset); + if (end) moveComplete(); + return selectedFace.cube.boxM; + }); + + SPACE.mouse.onHover(function(selection, event) { + if (event) { + if (selection && selection.faceIndex) { + var cf = selection.face.onface, + s = cf.set, + p = cf.cube.pos; + hoverSpot.rotation.set(s.rx, s.ry, s.rz); + hoverSpot.position.set(p.x + s.x * 1.05, p.z + s.z * 1.05, -p.y + s.y * 1.05); + SPACE.update(25); + } + } else { + return selectable; + } + }); + + function inputHasFocus() { + return DOC.activeElement && (DOC.activeElement != DOC.body); + } + + function setAnchorSelection(x,y,z) { + if (selected.length === 0) return; + for (var i=0; i 0) { + while (i < selected.length) { + bounds.update(selected[i++].pos); + } + } else { + while (i < selectable.length) { + bounds.update(selectable[i++].cube.pos); + } + } + SPACE.platform.setMaxZ(i > 0 ? bounds.z.max : 0); + } + + function keyPressHandler(evt) { + if (inputHasFocus()) return false; + var handled = true, style; + switch(evt.charCode) { + case cca('h'): + computeCenter(); + SPACE.view.home(); + break; + case cca('t'): + computeCenter(); + SPACE.view.top(); + break; + case cca('a'): + setEditMode(EDIT.ADD); + break; + case cca('s'): + setEditMode(EDIT.SELECT); + break; + case cca('d'): + setEditMode(EDIT.DELETE); + break; + case cca('e'): + setMarkMode(MARK.EMIT); + break; + case cca('w'): + setMarkMode(MARK.SLIDE); + break; + case cca('q'): + setMarkMode(MARK.CLEAR); + break; + case cca('c'): + if (selected.length === 0) { + setEditMode(EDIT.CLONE) + } else { + cloneSelection(false); + } + break; + case cca('m'): + mirrorSelection(); + break; + case cca('n'): + window.n = []; + for (var i=0; i 15 ? sname.substring(0,13)+"..." : sname; + html.push('
'); + i++; + }); + html.push(); + lib.innerHTML = html.join(''); + + i = 0; + filenames.forEach(function(name) { + button = $('slib_'+i); + button.onclick = function() { libraryToSelection(this.filename) }; + button.title = name+'\rvertices: '+files[name].vertices; + button.filename = name; + button = $('dlib_'+i); + button.onclick = function() { if (confirm('delete '+this.filename+'?')) MDB.deleteFile(this.filename) }; + button.filename = name; + button.title = 'delete'; + i++; + }); + } + + /** setup UI control elements */ + + var UC = moto.ui.prefix('meta'), + ctrlLeft = $('control-left'), + ctrlRight = $('control-right'), + assets = $('assets'), + control = $('control'), + selCube = UC.newButton('cube', function() { setSelectMode(SELECT.CUBE) }), + selRegion = UC.newButton('region', function() { setSelectMode(SELECT.REGION) }), + selJoined = UC.newButton('joined', function() { setSelectMode(SELECT.JOINED) }), + selPlane = UC.newButton('planar', function() { setSelectMode(SELECT.PLANE) }), + selDrill = UC.newButton('core', function() { setSelectMode(SELECT.DRILL) }), + editAdd = UC.newButton('add', function() { setEditMode(EDIT.ADD) }), + editDel = UC.newButton('delete', function() { setEditMode(EDIT.DELETE) }), + editSel = UC.newButton('select', function() { setEditMode(EDIT.SELECT) }), + editClo = UC.newButton('clone', function() { setEditMode(EDIT.CLONE) }), + editMark = UC.newButton('mark', function() { setEditMode(EDIT.MARK) }), + markEmit = UC.newButton('emit', function() { setMarkMode(MARK.EMIT) }), + mtypStop = UC.newButton('stop', function() { setMarkMode(MARK.SLIDE) }), + mtypClear = UC.newButton('clear', function() { setMarkMode(MARK.CLEAR) }), + verDown = UC.newButton('-', function() { restoreWorkspaceVersion(spaceVer-1) }), + verUp = UC.newButton('+', function() { restoreWorkspaceVersion(spaceVer+1) }); + + selectModeButtons[SELECT.CUBE] = selCube; + selectModeButtons[SELECT.REGION] = selRegion; + selectModeButtons[SELECT.JOINED] = selJoined; + selectModeButtons[SELECT.PLANE] = selPlane; + selectModeButtons[SELECT.DRILL] = selDrill; + + editModeButtons[EDIT.ADD] = editAdd; + editModeButtons[EDIT.DELETE] = editDel; + editModeButtons[EDIT.SELECT] = editSel; + editModeButtons[EDIT.CLONE] = editClo; + editModeButtons[EDIT.MARK] = editMark; + + markTypeButtons[MARK.EMIT] = markEmit; + markTypeButtons[MARK.SLIDE] = mtypStop; + markTypeButtons[MARK.CLEAR] = mtypClear; + + UC.newGroup('workspace', assets); + UC.newTableRow([[ + UC.newButton('new', newWorkspace), + UC.newButton('fork', forkWorkspace), + UC.newButton('save', saveWorkspace) + ]]); + UC.newGroup('mode').setAttribute("title","choose adding, deleting\ncloning or selecting cubes"); + UC.newTableRow([ + [ editAdd, editDel ], + [ editSel, editClo ], + [ editMark ] + ]); + UC.newGroup('action').setAttribute("title","action to perform on current selection"); + UC.newTableRow([ + [ + UC.newButton('clone', cloneSelection ), + UC.newButton('mirror', mirrorSelection ) + ],[ + UC.newButton('solidify', convertSelection ), + UC.newButton('clear', selectionClearMesh ) + ],[ + UC.newButton('to meshes', selectionToLibrary ) + ],[ + UC.newButton('to kiri:moto', selectionToKiri ) + ],[ + UC.newButton('download STL', downloadSelection ) + ] + ]); + UC.newGroup('select').setAttribute("title","criteria for choosing the selection"); + UC.newTableRow([ + [ selCube, selRegion ], + [ selJoined, selPlane ], + [ selDrill ] + ]); + UC.newGroup('mark').setAttribute("title","mark cube faces to control\nselection and region fills"); + UC.newTableRow([ + [ markEmit, mtypStop ], + [ mtypClear ] + ]); + UC.newGroup('mesh rotate'); + UC.newTableRow([ + [ + UC.newButton('x-', function() { rotateSelection(-1, 0, 0) }), + UC.newButton('x+', function() { rotateSelection( 1, 0, 0) }) + ],[ + UC.newButton('y-', function() { rotateSelection( 0,-1, 0) }), + UC.newButton('y+', function() { rotateSelection( 0, 1, 0) }) + ],[ + UC.newButton('z-', function() { rotateSelection( 0, 0,-1) }), + UC.newButton('z+', function() { rotateSelection( 0, 0, 1) }) + ] + ]); + UC.newGroup('mesh anchor'); + UC.newTableRow([ + [ + UC.newButton('x-', function() { updateAnchorSelection(-1, 0, 0) }), + UC.newButton('x+', function() { updateAnchorSelection( 1, 0, 0) }) + ],[ + UC.newButton('y-', function() { updateAnchorSelection( 0,-1, 0) }), + UC.newButton('y+', function() { updateAnchorSelection( 0, 1, 0) }) + ],[ + UC.newButton('z-', function() { updateAnchorSelection( 0, 0,-1) }), + UC.newButton('z+', function() { updateAnchorSelection( 0, 0, 1) }) + ],[ + UC.newButton('center', function() { setAnchorSelection(0,0,0) }) + ] + ]); + + UC.newGroup('space', control); + UI.name = UC.newInput('name', {size:10}); + UI.units = UC.newInput('units'); + UI.grid = UC.newInput('grid'); + UI.version = UC.newInput('version', {disabled:true}); + UC.newTableRow([[ verDown, verUp ]]); + + UC.newGroup('selection'); + UI.sel_x = UC.newInput('width', {disabled:true}); + UI.sel_y = UC.newInput('depth', {disabled:true}); + UI.sel_z = UC.newInput('height', {disabled:true}); + UI.sel_b = UC.newInput('blocks', {disabled:true}); + + UC.newGroup('spaces'); + UI.models = UC.newRow(); + + UC.newGroup('import mesh'); + UI.libdrop = UC.newButton('(drop mesh here)'); + UI.libdrop.id = "dropbutton"; + UC.newRow([UI.libdrop]); + + UC.newGroup('meshes'); + UI.library = UC.newRow(); + + /** attach our key board controls */ + + SPACE.addEventHandlers(window, [ + 'keyup', keyUpHandler, + 'keydown', keyDownHandler, + 'keypress', keyPressHandler + ]); + + SPACE.onEnterKey([ + UI.grid, function(ev) { setGridSize(parseInt(UI.grid.value)) }, + UI.name, function(ev) { updateSpaceName(UI.name.value) }, + UI.units, function(ev) { setSpaceUnits(UI.units.value) } + ]); + + /** prevent mouse hover, click in control panels from affecting space */ + + function killev(ev) { ev.stopImmediatePropagation(); return false } + + SPACE.addEventHandlers(ctrlLeft, [ + 'mousemove', killev, + 'mousedown', killev, + 'mouseup', killev + ]); + SPACE.addEventHandlers(ctrlRight, [ + 'mousemove', killev, + 'mousedown', killev, + 'mouseup', killev + ]); + + /** wire up drag and drop handlers and library listener */ + + SPACE.addEventHandlers(UI.libdrop, [ + 'dragover', dragOverHandler, + 'dragleave', dragLeave, + 'drop', dropHandler + ]); + + MDB.addFileListener(updateMeshList); + + /** and a few more settings before we're done */ + + setGridSize(gridSize); + setMarkMode(MARK.NONE); + setEditMode(EDIT.SELECT); + setSelectMode(SDB['meta-smode']); + setSpaceUnits('1 cm'); + + restoreWorkspace(); + seedCorners(); + + ctrlLeft.style.display = 'block'; + ctrlRight.style.display = 'block'; + } + + /** ****************************************************************** + * Object Definitions + ******************************************************************* */ + + function MinMax() { + this.min = Infinity; + this.max = -Infinity; + } + + MinMax.prototype.update = function(v) { + if (v < this.min) this.min = v; + if (v > this.max) this.max = v; + }; + + function Bounds() { + this.x = new MinMax(); + this.y = new MinMax(); + this.z = new MinMax(); + this.dx = 0; + this.dy = 0; + this.dz = 0; + this.count = 0; + } + + BP.update = function(pos) { + this.x.update(pos.x); + this.y.update(pos.y); + this.z.update(pos.z); + this.count++; + this.dx = this.x.max - this.x.min + 1; + this.dy = this.y.max - this.y.min + 1; + this.dz = this.z.max - this.z.min + 1; + }; + + BP.center = function() { + return { + x: this.x.min + (this.dx / 2), + y: this.y.min + (this.dy / 2), + z: this.z.min + (this.dz / 2) + }; + }; + + function Cube(x, y, z, synth) { + this.pos = {x:x, y:y, z:z}; + this.key = [x,y,z].join(','); + + this.synth = synth; + this.selected = false; + + this.materials = new THREE.MeshFaceMaterial([ + boxMaterial, boxMaterial, boxMaterial, boxMaterial, boxMaterial, boxMaterial + ]); + + this.box = new THREE.BoxGeometry(1,1,1,1,1,1); + this.boxM = new THREE.Mesh(this.box, this.materials); + this.boxM.position.set(x, y, z); + this.boxM.cube = this; + + this.mesh = null; + this.meshRotate = null; + + this.group = new THREE.Object3D(); + this.group.cube = this; + this.group.add(this.boxM); + + this.faces = { + rl: this.makeFace('rl'), + lr: this.makeFace('lr'), + bf: this.makeFace('bf'), + fb: this.makeFace('fb'), + tb: this.makeFace('tb'), + bt: this.makeFace('bt') + }; + + var b = this.box, f, key; + for (var i=0; i 0) { fdir = [0,2,4,3,5,1]; fro = [ 1,-1, 0, 1,-1, 0] } else + if (x < 0) { fdir = [0,5,1,3,2,4]; fro = [-1, 0, 1,-1, 0, 1] } else + if (y > 0) { fdir = [5,1,0,2,4,3]; fro = [ 0, 1,-1, 0, 1,-1] } else + if (y < 0) { fdir = [2,1,3,5,4,0]; fro = [ 1,-1, 0, 1,-1, 0] } else + if (z > 0) { fdir = [1,3,2,4,0,5]; fro = [ 1, 0, 1, 1, 0,-1] } else + if (z < 0) { fdir = [4,0,2,1,3,5]; fro = [ 0, 1,-1, 0, 1, 1] } + + //console.log(['was', mr.f, mr.r, 'fro', fro[mr.f]]); + + if (fro[mr.f] === 0) { + // invert rotation when changing face sign (i.e. +x to -y) + mr.r = 3 - mr.r; + } else { + mr.r = mr.r + fro[mr.f]; + // normalize in 0-3 range + if (mr.r <= 0) mr.r += 4; + if (mr.r >= 4) mr.r -= 4; + } + mr.f = fdir[mr.f]; + + //console.log(['now', mr.f, mr.r]); + + this.updateMeshAnchor(); + } + }; + + CP.setFaceMaterial = function(index, mat) { + this.materials.mSet(index, mat); + }; + + CP.showHideFace = function(index, show) { + this.materials.mVisible(index, show); + }; + + CP.canMoveTo = function(delta) { + if (!this.isSelected()) return true; + var adjacent = this.offsetCube(delta); + if (adjacent === this) return true; + if (this.pos.z + delta.z < 0) return false; + return (!adjacent || adjacent.isSelected() || adjacent.isSynthetic()); + }; + + CP.moveTo = function(delta) { + if (this.selected) { + this.group.position.set(delta.x, delta.y, delta.z); + } + }; + + CP.dropAndUpdate = function(clone) { + var pos = this.pos, + delta = this.group.position, + faces = this.faces, + npos = {x:pos.x + delta.x, y:pos.y + delta.y, z:pos.z + delta.z}, + i, k, face; + if (clone) { + this.group.position.set(0,0,0); + return this.cloneTo(npos.x, npos.y, npos.z); + } else { + this.boxM.position.add(delta); + if (this.mesh) this.mesh.position.add(delta); + this.group.position.set(0,0,0); + if (!delete matrix[this.key]) throw "missing cube @ "+this.key; + this.pos = npos; + this.key = [npos.x, npos.y, npos.z].join(','); + return this; + } + }; + + CP.add = function() { + return addCube(this); + }; + + CP.remove = function() { + removeCube(this); + }; + + CP.newAdjacent = function(face, mult, synth) { + var pos = this.pos, + off = FACE[face.key], + prod = mult || 1; + if (pos.z + off.oz * prod < 0) return null; + return new Cube(pos.x + off.ox * prod, pos.y + off.oy * prod, pos.z + off.oz * prod, synth); + }; + + CP.offsetPosition = function(off, mult) { + var pos = this.pos, + prod = mult || 1; + return { + x: pos.x + off.x * prod, + y: pos.y + off.y * prod, + z: pos.z + off.z * prod + }; + }; + + CP.offsetCube = function(off, mult) { + var pos = this.pos, + prod = mult || 1, + key = [pos.x + off.ox * prod, pos.y + off.oy * prod, pos.z + off.oz * prod].join(','); + return matrix[[pos.x + off.ox * prod, pos.y + off.oy * prod, pos.z + off.oz * prod].join(',')]; + }; + + CP.adjacentCube = function(face) { + return this.offsetCube(FACE[face.key]); + }; + + CP.adjacentCubeFace = function(face, key) { + var cube = this.adjacentCube(face); + if (cube) return cube.faces[key.reverse()]; + return null; + }; + + CP.makeFace = function(key) { + return { + mark: MARK.NONE, + cube: this, + set: FACE[key], + key: key + }; + }; + + CP.mirrorSwapFace = function(facekey) { + var f = this.faces, + rev = facekey.reverse(), + tmp = f[facekey].mark; + f[facekey].mark = f[rev].mark; + f[rev].mark = tmp; + if (this.mesh) { + var fd = 0, mr = this.meshRotate; + switch (facekey) { + case 'tb': + case 'bt': + this.mirrorMesh(0,0,1); + break; + case 'lr': + case 'rl': + this.mirrorMesh(1,0,0); + break; + case 'fb': + case 'bf': + this.mirrorMesh(0,1,0); + break; + } + } + }; + + // todo: make more efficient by marking cube changes and only updating + // todo: if this cube or one if it's neighbors has been changed + CP.updateFaces = function() { + if (this.mesh) { + this.mesh.material = this.selected ? boxMaterialSelected : boxMaterial; + } + for (var k in this.faces) { + if (!HAS(this.faces, k)) continue; + var select = this.selected, + face = this.faces[k], + cube = face.cube, + syn = this.isSynthetic(), + sel = select && face === selectedFace, + mat = select ? boxMaterialSelected : boxMaterial, + adj = cube.adjacentCube(face), + acf = cube.adjacentCubeFace(face, k), + adjm = adj && adj.mesh, + mark = face.mark != MARK.NONE; + if (syn) { + mat = select ? boxMaterialSelectedSynthetic : faceMaterialSynthetic; + } + // set adjacent face material + if (adj && !adjm) { + mat = faceMaterialInside; + } + if (mark) { + if (face.mark === MARK.EMIT) mat = faceMaterialEmit; + if (face.mark === MARK.SLIDE) mat = faceMaterialSlide; + } + // mark adjacent faces with this face's mark + //if (acf && !adj.isSynthetic() && face.mark != MARK.NONE) { + // acf.mark = face.mark; + //} + if (sel) { + mat = faceMaterialSelected; + } + face.inside = adj && !adjm; + cube.setFaceMaterial(face.set.mi, mat); + cube.showHideFace(face.set.mi, mark || sel || !(face.inside || this.mesh)); + } + SPACE.update(); + }; + + CP.clearMarks = function() { + for (var k in this.faces) { + if (!HAS(this.faces, k)) continue; + this.faces[k].mark = MARK.NONE; + } + this.updateFaces(); + }; + + CP.addSynthetic = function() { + for (var k in this.faces) { + if (!HAS(this.faces, k)) continue; + var face = this.faces[k], + krev = k.reverse(), + offset = FACE[face.key]; + if (face.mark === MARK.EMIT) { + var found = false, + build = [], + count = 1, + next; + while (true) { + if (count > 50) break; + next = this.offsetCube(offset,count++); + if (!next) { + build.push(count-1); + continue; + } + if (next.isSynthetic()) continue; + if (next.faces[krev].mark === MARK.EMIT) { + found = true; + break; + } else { + return; + } + } + if (found) { + for (var i=0; i 0) arr.reverse(); + } + if (off.oy) { + fix = pos.y + HALF * off.oy; + arr = [ + // triangle 1 + [pos.x - HALF, fix, pos.z - HALF], + [pos.x + HALF, fix, pos.z + HALF], + [pos.x + HALF, fix, pos.z - HALF], + // triangle 2 + [pos.x - HALF, fix, pos.z - HALF], + [pos.x - HALF, fix, pos.z + HALF], + [pos.x + HALF, fix, pos.z + HALF] + ]; + if (off.oy < 0) arr.reverse(); + } + if (off.oz) { + fix = pos.z + HALF * off.oz; + arr = [ + // triangle 1 + [pos.x - HALF, pos.y - HALF, fix], + [pos.x + HALF, pos.y + HALF, fix], + [pos.x + HALF, pos.y - HALF, fix], + // triangle 2 + [pos.x - HALF, pos.y - HALF, fix], + [pos.x - HALF, pos.y + HALF, fix], + [pos.x + HALF, pos.y + HALF, fix] + ]; + if (off.oz > 0) arr.reverse(); + } + for (k = 0; k < arr.length; k++) { + vertices.appendAll(arr[k]); + } + normals.appendAll([ + 0,0,0, + 0,0,0 + ]); + } + } + if (scale) { + for (i = 0; i < vertices.length; i++) { + vertices[i] *= scale; + } + } + return {vertices: vertices, normals: null}; + } + + function unitsToMM(units) { + var num = parseFloat(units), + scale = 10.0; + units = units.toString(); + if (units.indexOf('mm') > 0) scale = 1.0; + else if (units.indexOf('cm') > 0) scale = 10.0; + else if (units.indexOf('in') > 0) scale = 25.4; + else if (units.indexOf('ft') > 0) scale = 25.4 * 12; + else if (units.indexOf('feet') > 0) scale = 25.4 * 12; + else if (units.indexOf('yard') > 0) scale = 25.4 * 12 * 3; + else if (units.indexOf('meter') > 0) scale = 100.0; + else if (units.indexOf('m') > 0) scale = 10.0; + return num * scale; + } + + function geoToMesh(geo) { + var newgeo = geo.clone(), + mesh = new THREE.Mesh(newgeo, boxMaterial); + newgeo.filename = geo.filename; + mesh.filename = geo.filename; + return mesh; + } + + function getLibraryGeo(filename, callback) { + if (!filename) return; // todo this should not happen -- bad encoding + if (LIBCACHE[filename]) callback(LIBCACHE[filename]); + MDB.getFile(filename, function(vertices) { + if (!vertices) return callback(); + var geo = THREE.Geometry.fromVertices(vertices).unitScale().center().fixNormals(); + geo.filename = filename; + callback(LIBCACHE[filename] = geo); + }); + } + + function libraryToSelection(filename) { + if (selected.length === 0) return alert("no selection for mesh"); + getLibraryGeo(filename, function(geo) { + for (var i=0; i 0) KDB.putFile(name, geo.vertices.toFloat32(), function(done) { + if (done) alert("selection sent to kiri"); + }); + } + + function selectionToLibrary() { + if (selected.length === 0) return alert("make a selection to import"); + var scale = unitsToMM(spaceUnits), + geo = selectionToGeometry(scale), + name = prompt("Name of Selection", ""); + if (name && name.length > 0) MDB.putFile(name, geo.vertices.toFloat32()); + } + + function downloadSelection() { + if (selected.length === 0) return alert("make a selection to export"); + var name = prompt("Download Name", spaceName); + if (!name) return; + var scale = unitsToMM(spaceUnits), + geo = selectionToGeometry(scale), + stl = new moto.STL().encode(geo.vertices, geo.normals), + blob = new Blob([stl], {type: 'application/octet-binary'}), + save = saveAs(blob, name+".stl"); + } + + function sendWorkspace(to) { + var data = (to || gs_meta.sendTo)+"/data/"+spaceID, + space = (to || gs_meta.sendTo)+"/meta/#"+spaceID; + new moto.Ajax(function(res, ajax) { + if (res) WIN.location = space; + }).request(data, SDB['workspace']); + } + + function selectAll() { + selected = []; + for (var i = 0; i < selectable.length; i++) { + var cube = selectable[i].cube; + cube.setSelected(true); + selected.push(cube); + } + updateSelectionStats(); + } + + function setSelection(cubes) { + clearSelections(); + selected = cubes; + for (var i=0; i 0) { + cube = stack.pop(); + if (modify) cube.setSelected(mode); + cache[cube.key] = cube; + for (k in cube.faces) { + if (!HAS(cube.faces, k)) continue; + face = cube.faces[k]; + if (selType === SELECT.CUBE) break; + if (selType === SELECT.DRILL && k != revkey) continue; + if (selType === SELECT.PLANE && pkeys.contains(k)) continue; + if (selType === SELECT.REGION) { + if (face.mark != MARK.NONE) continue; + var adjface = cube.adjacentCubeFace(face,k); + if (adjface && adjface.mark !== MARK.NONE) continue; + } + next = cube.adjacentCube(face); + if (next && !cache[next.key]) { + if (next.isSynthetic() && !cube.isSynthetic()) { + if (selType === SELECT.REGION || !modify) continue; + } + stack.push(next); + } + } + } + if (modify) { + for (k in matrix) { + if (HAS(matrix,k)) { + if ((cube = matrix[k]).isSelected()) newSelected.push(cube) + } + } + } else { + for (k in cache) { + if (HAS(cache,k)) { + if (!(cube = cache[k]).isSynthetic()) { + newSelected.push(cube); + } + } + } + } + if (modify) { + selected = newSelected; + scheduleUpdateFaces(); + updateSelectionStats(); + } + return newSelected; + } + + function updateSelectionStats() { + var bounds = new Bounds(); + UI.sel_b.value = selected.length || ''; + if (selected.length === 0) { + UI.sel_x.value = ''; + UI.sel_y.value = ''; + UI.sel_z.value = ''; + } else { + var i = 0; + while (i < selected.length) { + bounds.update(selected[i++].pos); + } + UI.sel_x.value = bounds.dx; + UI.sel_y.value = bounds.dy; + UI.sel_z.value = bounds.dz; + } + selectedBounds = bounds; + } + + function moveSelection(delta) { + var k, rd = {x:ROUND(delta.x), y:-ROUND(delta.z), z:ROUND(delta.y)}; + // TODO ugly hack to workaround faces to box transition ... fix + rd.ox = rd.x; + rd.oy = rd.y; + rd.oz = rd.z; + for (k in selected) if (HAS(selected,k) && !selected[k].canMoveTo(rd)) return; + for (k in selected) if (HAS(selected,k)) selected[k].moveTo(rd); + } + + function moveComplete() { + removeSynthetic(); + var i, list = selected.slice(); + for (i=0; i 0) out.push(v); + } + return out; + } + + function setSpaceUnits(units) { + spaceUnits = units || '1 cm'; + UI.units.value = spaceUnits; + } + + function updateSpaceName(name) { + setSpaceName(name); + updateSpacesList(); + SDB['spaceNames'] = JSON.stringify(SPACENAMES); + } + + function setSpaceName(name) { + spaceName = name || 'untitled'; + SPACENAMES[spaceID] = spaceName; + UI.name.value = spaceName; + } + + function setSpaceVersion(ver) { + spaceVer = Math.max(ver || 1, 1); + UI.version.value = spaceVer; + } + + function setGridSize(size) { + gridSize = ROUND(size); + UI.grid.value = size; + SPACE.platform.setGrid(gridSize, 1); + SPACE.update(); + } + + function loadSTL(url, name) { + new moto.STL().load(url, function(vertices) { + if (vertices && vertices.length > 0) MDB.putFile(name, vertices); + }); + } + + function seedCorners() { + if (!SDB['meta-seed']) { + loadSTL("/obj/meta-corner-1.stl","corner-1"); + loadSTL("/obj/meta-corner-2.stl","corner-2"); + loadSTL("/obj/meta-corner-3.stl","corner-3"); + loadSTL("/obj/meta-corner-4.stl","corner-4"); + SDB['meta-seed'] = new Date().getTime(); + } + } + + function seedWorkspace() { + new Cube(-1, 1, 0).add(); + new Cube(-1,-1, 0).add(); + new Cube( 1,-1, 0).add(); + new Cube( 1, 1, 0).add(); + new Cube( 1, 0, 0).add(); + new Cube(-1, 0, 0).add(); + new Cube( 0, 1, 0).add(); + new Cube( 0,-1, 0).add(); + } + + function newWorkspace() { + if (!confirm("start a new workspace?")) return; + spaceID = genKey(); + selectAll(); + deleteSelection(); + setGridSize(5); + setSpaceName('newspace'); + setSpaceVersion(1); + setSpaceUnits('1 cm'); + updateURL(); + } + + function updateSpaceState() { + if (!SPACES.contains(spaceID)) { + SPACES.push(spaceID); + SPACENAMES[spaceID] = spaceName; + } + SDB['workspace-i'] = spaceID; + SDB['workspace-v'] = spaceVer; + SDB['spaces'] = JSON.stringify(SPACES); + SDB['spaceNames'] = JSON.stringify(SPACENAMES); + updateSpacesList(); + } + + function saveWorkspace() { + var cubes = selectable, + cube, + face, + px = [], + py = [], + pz = [], + marks = [], + files = [], + mesh = [], + mfac = [], + mfro = [], + mmir = [], + manc = [], + coded = 0, + i = 0, k; + + selectable.sort(function(a,b) { + return a.cube.key > b.cube.key ? 1 : -1; + }); + + while (i < cubes.length) { + cube = cubes[i++].cube; + if (cube.isSynthetic()) continue; + if (cube.mesh) { + var mr = cube.meshRotate, + fn = cube.mesh.filename, + fnp = files.indexOf(fn); + if (fnp < 0) { + fnp = files.length; + files.push(fn); + } + mesh.push(fnp); + mfac.append(mr.f); + mfro.append(mr.r); + mmir.append(mr.mx | (mr.my << 1) | (mr.mz << 2)); + manc.append(mr.ax + 1 | ((mr.ay + 1) << 2) | ((mr.az + 1) << 4)); + } else { + mesh.push('-'); + } + for (k = 0; k < FACES.length; k++) { + face = cube.faces[FACES[k]]; + marks.push(face.mark || 1); + } + px.push(cube.pos.x); + py.push(cube.pos.y); + pz.push(cube.pos.z); + coded++; + } + var save = JSON.stringify({ + files: files, + cubes: coded, + px: rle_encode(del_encode(px)), + py: rle_encode(del_encode(py)), + pz: rle_encode(del_encode(pz)), + mark: rle_encode(marks), + mesh: rle_encode(mesh), + mfac: rle_encode(mfac), + mfro: rle_encode(mfro), + mmir: rle_encode(mmir), + manc: rle_encode(manc), + cam: SPACE.view.save(), + grid: gridSize, + name: spaceName, + units: spaceUnits + }); + + SDB['workspace'] = save; + + new moto.Ajax(function(reply) { + if (reply) { + var res = JSON.parse(reply); + if (res && res.ver) { + // server-side fork when save but not owned + if (res.space != spaceID) setSpaceName(spaceName+" new"); + SDB['workspace-i'] = spaceID = res.space; + SDB['workspace-v'] = res.ver; + setSpaceVersion(res.ver); + updateURL(); + updateSpaceState(); + } + } else { + updateSpaceState(); + } + }).request(DATA + spaceID + "/" + spaceVer, save); + } + + function updateSpacesList() { + var models = UI.models, + idx = SPACES.length- 1, + html = [], + id, name, button; + + while (idx >= 0) { + id = SPACES[idx--]; + name = SPACENAMES[id] || id; + html.push('
'); + } + models.innerHTML = html.join(''); + + idx = SPACES.length - 1; + while (idx >= 0) { + id = SPACES[idx--]; + name = SPACENAMES[id] || id; + button = $('smod_'+id); + button.title = name; + button.model = id; + button.onclick = function() { restoreWorkspace(this.model) }; + button = $('dmod_'+id); + button.title = name; + button.model = id; + button.onclick = function() { deleteWorkspace(this.model) }; + } + } + + function deleteWorkspace(id) { + var io = SPACES.indexOf(id); + if (io >= 0) { + if (!confirm("delete workspace "+(SPACENAMES[id] || id))) return; + SPACES.splice(io,1); + delete SPACENAMES[id]; + SDB['spaces'] = JSON.stringify(SPACES); + SDB['spaceNames'] = JSON.stringify(SPACENAMES); + } + updateSpacesList(); + } + + function restoreWorkspaceVersion(ver) { + restoreWorkspace(spaceID, Math.max(ver,1)); + } + + function restoreWorkspace(newID,ver) { + if (newID) { + LOC.hash = ver ? newID + "/" + ver : newID; + spaceID = newID; + spaceVer = ver || spaceVer; + selectAll(); + deleteSelection(); + } else { + SPACE.view.load({scale:0.1}); + } + var hash = LOC.hash; + if (hash.length > 1) { + var x = hash.substring(1).split("/"), + xs = x[0] || '', + xv = x[1] || '', + handled = false; + if (xs && xs.length >= 4 && xs.length <= 8) { + handled = true; + new moto.Ajax(function (json) { + if (json && json.charAt(0) === '{') { + var rec = JSON.parse(json); + loadWorkspace(rec.rec); + spaceID = rec.space; + setSpaceVersion(rec.ver); + SDB['workspace'] = rec.rec; + SDB['workspace-i'] = spaceID; + SDB['workspace-v'] = spaceVer; + updateURL(); + updateFaces(); + } else { + restoreWorkspaceLocal(); + } + }).request(DATA + xs + "/" + xv); + } + } + if (!handled) restoreWorkspaceLocal(); + } + + function restoreWorkspaceLocal() { + spaceID = SDB['workspace-i'] || genKey(); + setSpaceVersion(SDB['workspace-v'] || 1); + loadWorkspace(SDB['workspace']); + } + + function forkWorkspace() { + spaceID = genKey(); + setSpaceVersion(1); + setSpaceName(spaceName+"_fork"); + updateURL(); + alert("workspace forked\nsave to start a new timeline"); + } + + function loadWorkspace(json) { + if (json && json.length > 0) { + var cubes = selectable.slice(), + wrk = JSON.parse(json), + pos = wrk.pos, + px = wrk.px ? del_decode(rle_decode(wrk.px)) : null, + py = wrk.py ? del_decode(rle_decode(wrk.py)) : null, + pz = wrk.pz ? del_decode(rle_decode(wrk.pz)) : null, + marks = rle_decode(wrk.mark), + mesh = wrk.mesh ? rle_decode(wrk.mesh) : null, + mfac = wrk.mfac ? rle_decode(wrk.mfac) : null, + mfro = wrk.mfro ? rle_decode(wrk.mfro) : null, + mmir = wrk.mmir ? rle_decode(wrk.mmir) : [], + manc = wrk.manc ? rle_decode(wrk.manc) : [], + files = wrk.files, + count = wrk.cubes, + cam = wrk.cam, + pid = 0, + fid = 0, + mfr = 0, + mp = 0, + i = 0; + while (i < cubes.length) { + removeCube(cubes[i++]); + } + while (count-- > 0) { + var cube = px ? new Cube(px[pid], py[pid], pz[pid++]) : new Cube(pos[pid++], pos[pid++], pos[pid++]), + mfile = mesh ? mesh[mp++] : null; + for (i = 0; i < FACES.length; i++) { + cube.faces[FACES[i]].mark = marks[fid++]; + } + if (!(mfile === null || mfile === '-') && mfac && mfro) { + (function() { + var mi = {c: cube, f: mfac[mfr], r: mfro[mfr], m: mmir[mfr], a:manc[mfr++]}; + getLibraryGeo(files[mfile], function (geo) { + if (!geo) return; + mi.c.setMesh(geoToMesh(geo)); + mi.c.setMeshRotation(mi.f, mi.r); + mi.c.setMeshMirror(mi.m & 1, mi.m & 2 ? 1 : 0, mi.m & 4 ? 1 : 0); + if (wrk.manc) mi.c.setMeshAnchor((mi.a & 3) - 1, ((mi.a >> 2) & 3) - 1, ((mi.a >> 4) & 3) - 1); + scheduleUpdateFaces(); + }); + })(); + } + if (typeof(cube.pos.x + cube.pos.y + cube.pos.z) === 'number') cube.add(); + } + setSpaceName(wrk.name || 'untitled'); + setSpaceUnits(wrk.units || '1 cm'); + setGridSize(wrk.grid || 5); + SPACE.view.reset(); + if (cam) { + SPACE.view.load(cam); + } else { + SPACE.view.top(); + } + } else { + seedWorkspace(); + } + var spaces = SDB['spaces'], + spaceNames = SDB['spaceNames']; + if (spaceNames) { + SPACENAMES = JSON.parse(spaceNames); + } + if (spaces) { + SPACES = JSON.parse(spaces); + updateSpacesList(); + } + updateURL(); + } + + function watchHash() { + if (hashWatcher) return; + hashWatcher = setTimeout(function() { + if (LOC.hash != HASHMATCH) restoreWorkspace(); + hashWatcher = null; + watchHash(); + }, 100); + } + + function updateURL() { + HASHMATCH = "#"+spaceID+"/"+spaceVer; + LOC.hash = HASHMATCH; + watchHash(); + } + + function genKey() { + while (true) { + var k = Math.round(Math.random() * 9999999999).toString(36); + if (k.length >= 4 && k.length <= 8) return k; + } + } + +})(); diff --git a/js/moto-ajax.js b/js/moto-ajax.js new file mode 100644 index 00000000..3428003d --- /dev/null +++ b/js/moto-ajax.js @@ -0,0 +1,77 @@ +"use strict"; + +var gs_moto_ajax = { + copyright:"stewart allen -- all rights reserved" +}; + +(function() { + if (!self.moto) self.moto = {}; + if (self.moto.Ajax) return; + + self.moto.Ajax = Ajax; + self.moto.callAjax = function(url, handler) { + new Ajax(handler).request(url); + }; + + var STATES = [ + "request not initialized", // 0 + "server connection established", // 1 + "request recieved", // 2 + "processing request", // 3 + "request complete" // 4 + ]; + + function Ajax(callback, responseType) { + this.ajax = new XMLHttpRequest(); + this.ajax.onreadystatechange = this.onStateChange.bind(this); + this.ajax.withCredentials = true; + this.state = STATES[0]; + this.callback = callback; + this.responseType = responseType; + } + + function rnd() { + return Math.round(Math.random()*0xffffffff).toString(36); + } + + var AP = Ajax.prototype, + KV = moto.KV, + KEY = "moto-ajax", + TIME = function() { return new Date().getTime() }, + MOKEY = KV.getItem(KEY) || (TIME().toString(36)+rnd()+rnd()); + + KV.setItem(KEY, MOKEY); + + AP.onStateChange = function() { + this.state = STATES[this.ajax.readyState]; + if (this.ajax.readyState === 4 && this.callback) { + var status = this.ajax.status; + if (status >= 200 && status < 300) { + this.callback(this.ajax.responseType ? this.ajax.response : this.ajax.responseText, this.ajax); + } else { + this.callback(null, this.ajax); + } + } + }; + + /** + * @param {String} url + * @param {Object} [send] + * @param {Object} [headers] + */ + AP.request = function(url, send, headers) { + this.ajax.open(send ? "POST" : "GET", url, true); + if (this.responseType) this.ajax.responseType = this.responseType; + headers = headers || {}; + headers["X-Moto-Ajax"] = MOKEY; + for (var k in headers) { + this.ajax.setRequestHeader(k, headers[k]); + } + if (send) { + this.ajax.send(send); + } else { + this.ajax.send(); + } + }; + +})(); diff --git a/js/moto-ctrl.js b/js/moto-ctrl.js new file mode 100644 index 00000000..7eb9db74 --- /dev/null +++ b/js/moto-ctrl.js @@ -0,0 +1,566 @@ +"use strict"; + +var gs_moto_ctrl = { + copyright:"stewart allen -- all rights reserved" +}; + +/** + * Adapted from OrbitControls + */ + +THREE.CubeControls = function (object, domElement, notify, slider) { + + this.object = object; + this.domElement = ( domElement !== undefined ) ? domElement : document; + + // Set to false to disable this control + this.enabled = true; + + // "target" sets the location of focus, where the control orbits around + // and where it pans with respect to. + this.target = new THREE.Vector3(); + + // center is old, deprecated; use "target" instead + this.center = this.target; + + // This option actually enables dollying in and out; left as "zoom" for + // backwards compatibility + this.noZoom = false; + this.zoomSpeed = 1.0; + this.reverseZoom = false; + + // Limits to how far you can dolly in and out + this.minDistance = 0; + this.maxDistance = Infinity; + + // Set to true to disable this control + this.noRotate = false; + this.rotateSpeed = 1.0; + + // Set to true to disable this control + this.noPan = false; + this.keyPanSpeed = 7.0; // pixels moved per arrow key push + + // How far you can orbit vertically, upper and lower limits. + // Range is 0 to Math.PI radians. + this.minPolarAngle = 0; // radians + this.maxPolarAngle = Math.PI; // radians + + // How far you can orbit horizontally, upper and lower limits. + // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. + this.minAzimuthAngle = -Infinity; // radians + this.maxAzimuthAngle = Infinity; // radians + + // Set to true to disable use of the keys + this.noKeys = false; + + // The four arrow keys + this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; + + // Mouse buttons + this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }; + + var scope = this, + domEl = scope.domElement, + EPS = 0.000001, + rotateStart = new THREE.Vector2(), + rotateEnd = new THREE.Vector2(), + rotateDelta = new THREE.Vector2(), + panStart = new THREE.Vector2(), + panEnd = new THREE.Vector2(), + panDelta = new THREE.Vector2(), + panOffset = new THREE.Vector3(), + offset = new THREE.Vector3(), + dollyStart = new THREE.Vector2(), + dollyEnd = new THREE.Vector2(), + dollyDelta = new THREE.Vector2(), + theta, + thetaDelta = 0, + thetaSet = null, + phi, + phiDelta = 0, + phiSet = null, + scale = 1, + scaleSave = 1, + pan = new THREE.Vector3(), + lastPosition = new THREE.Vector3(), + lastQuaternion = new THREE.Quaternion(), + // so camera.up is the orbit axis + quat = new THREE.Quaternion().setFromUnitVectors(object.up, new THREE.Vector3(0, 1, 0)), + quatInverse = quat.clone().inverse(), + // events + changeEvent = { type: 'change'}, + startEvent = { type: 'start'}, + endEvent = { type: 'end'}; + + var STATE = { NONE: -1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5}, + state = STATE.NONE, + MODE = { PERSPECTIVE: 1, ORTHOGRAPHIC: 2, UNKNOWN: 3}, + mode = isValue(object.fov) ? MODE.PERSPECTIVE : isValue(object.top) ? MODE.ORTHOGRAPHIC : MODE.UNKNOWN; + + // for reset + this.target0 = this.target.clone(); + this.position0 = this.object.position.clone(); + + this.rotateLeft = function (angle) { + thetaDelta -= angle; + }; + + this.rotateUp = function (angle) { + phiDelta -= angle; + }; + + // pass in distance in world space to move left + this.panLeft = function (distance) { + var te = this.object.matrix.elements; + + // get X column of matrix + panOffset.set(te[ 0 ], te[ 1 ], te[ 2 ]); + panOffset.multiplyScalar(-distance); + pan.add(panOffset); + }; + + // pass in distance in world space to move up + this.panUp = function (distance) { + var te = this.object.matrix.elements; + + // get Y column of matrix + panOffset.set(te[ 4 ], te[ 5 ], te[ 6 ]); + panOffset.multiplyScalar(distance); + pan.add(panOffset); + }; + + // pass in x,y of change desired in pixel space, + // right and down are positive + this.pan = function (deltaX, deltaY) { + var element = scope.domElement === document ? scope.domElement.body : scope.domElement; + + switch (mode) { + case MODE.PERSPECTIVE: + var position = scope.object.position; + var offset = position.clone().sub(scope.target); + var targetDistance = offset.length(); + + // half of the fov is center to top of screen + targetDistance *= Math.tan(( scope.object.fov / 2 ) * Math.PI / 180.0); + + // we actually don't use screenWidth, since perspective camera is fixed to screen height + scope.panLeft(2 * deltaX * targetDistance / element.clientHeight); + scope.panUp(2 * deltaY * targetDistance / element.clientHeight); + break; + case MODE.ORTHOGRAPHIC: + scope.panLeft(deltaX * (scope.object.right - scope.object.left) / element.clientWidth); + scope.panUp(deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight); + break; + } + }; + + function isValue(v) { + return v !== null && v !== undefined; + } + + function firstValue(choices) { + for (var i=0; i EPS + // using small-angle approximation cos(x/2) = 1 - x^2 / 8 + if (lastPosition.distanceToSquared(this.object.position) > EPS + || 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS) { + + this.dispatchEvent(changeEvent); + lastPosition.copy(this.object.position); + lastQuaternion.copy(this.object.quaternion); + if (notify) notify(position, true); + } else { + if (notify) notify(position, false); + } + }; + + this.reset = function () { + state = STATE.NONE; + scale = 1; + scaleSave = 1; + this.target.copy(this.target0); + this.object.position.copy(this.position0); + }; + + this.getPolarAngle = function () { + return phi; + }; + + this.getAzimuthalAngle = function () { + return theta + }; + + function getZoomScale() { + return Math.pow(0.95, scope.zoomSpeed); + } + + function onMouseDown(event) { + if (scope.enabled === false) return; + event.preventDefault(); + + switch (event.button) { + case scope.mouseButtons.ORBIT: + state = event.metaKey ? STATE.PAN : STATE.ROTATE; + break; + case scope.mouseButtons.ZOOM: + state = STATE.DOLLY; + break; + case scope.mouseButtons.PAN: + state = STATE.PAN; + break; + } + + switch (state) { + case STATE.ROTATE: + if (scope.noRotate === true) return state = STATE.NONE; + rotateStart.set(event.clientX, event.clientY); + break; + case STATE.DOLLY: + if (scope.noZoom === true) return state = STATE.NONE; + dollyStart.set(event.clientX, event.clientY); + break; + case STATE.PAN: + if (scope.noPan === true) return state = STATE.NONE; + panStart.set(event.clientX, event.clientY); + break; + } + + document.addEventListener('mousemove', onMouseMove, false); + document.addEventListener('mouseup', onMouseUp, false); + scope.dispatchEvent(startEvent); + } + + function onMouseMove(event) { + if (scope.enabled === false) return; + event.preventDefault(); + + var element = scope.domElement === document ? scope.domElement.body : scope.domElement; + + if (state === STATE.ROTATE) { + if (scope.noRotate === true) return; + + rotateEnd.set(event.clientX, event.clientY); + rotateDelta.subVectors(rotateEnd, rotateStart); + + // rotating across whole screen goes 360 degrees around + scope.rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed); + // rotating up and down along whole screen attempts to go 360, but limited to 180 + scope.rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed); + + rotateStart.copy(rotateEnd); + + } else if (state === STATE.DOLLY) { + if (scope.noZoom === true) return; + + dollyEnd.set(event.clientX, event.clientY); + dollyDelta.subVectors(dollyEnd, dollyStart); + + if (dollyDelta.y > 0) { + scope.dollyIn(); + } else { + scope.dollyOut(); + } + + dollyStart.copy(dollyEnd); + + } else if (state === STATE.PAN) { + if (scope.noPan === true) return; + + panEnd.set(event.clientX, event.clientY); + panDelta.subVectors(panEnd, panStart); + scope.pan(panDelta.x, panDelta.y); + panStart.copy(panEnd); + } + + scope.update(); + } + + function onMouseUp(/* event */) { + if (scope.enabled === false) return; + + document.removeEventListener('mousemove', onMouseMove, false); + document.removeEventListener('mouseup', onMouseUp, false); + scope.dispatchEvent(endEvent); + state = STATE.NONE; + } + + function onMouseWheel(event) { + if (scope.enabled === false || scope.noZoom === true) return; + + event.preventDefault(); + event.stopPropagation(); + + if (event.shiftKey && slider) { + slider(event.deltaX || event.wheelDelta || event.detail); + return; + } + + var delta = -(event.deltaY || event.wheelDelta || event.detail || 0); + + if (delta === 0) return; + + if (scope.reverseZoom) { + delta = -delta; + } + + if (delta > 0) { + scope.dollyOut(); + } else if (delta < 0) { + scope.dollyIn(); + } + + scope.update(); + scope.dispatchEvent(startEvent); + scope.dispatchEvent(endEvent); + } + + function onKeyDown(event) { + if (scope.enabled === false || scope.noKeys === true || scope.noPan === true) return; + + switch (event.keyCode) { + + case scope.keys.UP: + scope.pan(0, scope.keyPanSpeed); + scope.update(); + break; + + case scope.keys.BOTTOM: + scope.pan(0, -scope.keyPanSpeed); + scope.update(); + break; + + case scope.keys.LEFT: + scope.pan(scope.keyPanSpeed, 0); + scope.update(); + break; + + case scope.keys.RIGHT: + scope.pan(-scope.keyPanSpeed, 0); + scope.update(); + break; + } + } + + function touchstart(event) { + if (scope.enabled === false) return; + + switch (event.touches.length) { + case 1: // one-fingered touch: rotate + if (scope.noRotate === true) return; + + state = STATE.TOUCH_ROTATE; + + rotateStart.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY); + break; + + case 2: // two-fingered touch: dolly + if (scope.noZoom === true) return; + + state = STATE.TOUCH_DOLLY; + + var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + var distance = Math.sqrt(dx * dx + dy * dy); + dollyStart.set(0, distance); + break; + + case 3: // three-fingered touch: pan + if (scope.noPan === true) return; + + state = STATE.TOUCH_PAN; + + panStart.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY); + break; + + default: + state = STATE.NONE; + } + scope.dispatchEvent(startEvent); + } + + function touchmove(event) { + if (scope.enabled === false) return; + + event.preventDefault(); + event.stopPropagation(); + + var element = scope.domElement === document ? scope.domElement.body : scope.domElement; + + switch (event.touches.length) { + case 1: // one-fingered touch: rotate + if (scope.noRotate === true) return; + if (state !== STATE.TOUCH_ROTATE) return; + + rotateEnd.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY); + rotateDelta.subVectors(rotateEnd, rotateStart); + + // rotating across whole screen goes 360 degrees around + scope.rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed); + // rotating up and down along whole screen attempts to go 360, but limited to 180 + scope.rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed); + + rotateStart.copy(rotateEnd); + scope.update(); + break; + + case 2: // two-fingered touch: dolly + if (scope.noZoom === true) return; + if (state !== STATE.TOUCH_DOLLY) return; + + var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + var distance = Math.sqrt(dx * dx + dy * dy); + + if (reverseZoom) distance = -distance; + + dollyEnd.set(0, distance); + dollyDelta.subVectors(dollyEnd, dollyStart); + + if (dollyDelta.y > 0) { + scope.dollyOut(); + } else { + scope.dollyIn(); + } + + dollyStart.copy(dollyEnd); + scope.update(); + break; + + case 3: // three-fingered touch: pan + if (scope.noPan === true) return; + if (state !== STATE.TOUCH_PAN) return; + + panEnd.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY); + panDelta.subVectors(panEnd, panStart); + scope.pan(panDelta.x, panDelta.y); + panStart.copy(panEnd); + + scope.update(); + break; + + default: + state = STATE.NONE; + } + } + + function touchend(/* event */) { + if (scope.enabled === false) return; + scope.dispatchEvent(endEvent); + state = STATE.NONE; + } + + this.onMouseUp = onMouseUp; + + domEl.addEventListener('contextmenu', function (event) { event.preventDefault() }, false); + domEl.addEventListener('mousedown', onMouseDown, false); + domEl.addEventListener('mousewheel', onMouseWheel, false); + domEl.addEventListener('DOMMouseScroll', onMouseWheel, false); // firefox + domEl.addEventListener('touchstart', touchstart, false); + domEl.addEventListener('touchend', touchend, false); + domEl.addEventListener('touchmove', touchmove, false); + + window.addEventListener('keydown', onKeyDown, false); +}; + +THREE.CubeControls.prototype = Object.create(THREE.EventDispatcher.prototype); diff --git a/js/moto-db.js b/js/moto-db.js new file mode 100644 index 00000000..614ff16f --- /dev/null +++ b/js/moto-db.js @@ -0,0 +1,211 @@ +"use strict"; + +var gs_moto_db = { + copyright:"stewart allen -- all rights reserved" +}; + +(function() { + + if (!self.moto) self.moto = {}; + if (self.moto.Storage) return; + + self.moto.Storage = Storage; + + // https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API + + var IDB = self.indexedDB || self.mozIndexedDB || self.webkitIndexedDB || self.msIndexedDB, + IRR = self.IDBKeyRange, + local = null; + + /** + * @param {String} dbname + * @param {number} [version] + * @constructor + */ + function Storage(dbname, version) { + this.db = null; + this.name = dbname; + this.version = version || 1; + this.queue = []; + this.initCalled = false; + } + + Storage.delete = function(dbname) { + IDB.deleteDatabase(dbname); + }; + + var SP = Storage.prototype; + + /** ****************************************************************** + * indexedDB implementation + ******************************************************************* */ + + SP.keys = function(callback, lower, upper) { + var out = []; + this.iterate(function(k,v) { + if (k) out.push(k); + }, lower, upper, true); + callback(out); + }; + + SP.iterate = function(callback, lower, upper, nullterm) { + if (!this.db) { + this.init(); + return this.queue.push(["iterate", callback, lower, upper]); + } + var range = lower && upper ? IRR.bound(lower,upper) : + lower ? IRR.lowerBound(lower) : + upper ? IRR.upperBound(upper) : undefined; + // iterate over all db values for debugging + this.db.transaction(this.name).objectStore(this.name).openCursor(range).onsuccess = function(event) { + var cursor = event.target.result; + if (cursor) { + callback(cursor.key,cursor.value); + cursor.continue(); + } else if (nullterm) { + callback(null); + } + }; + }; + + SP.deleteStore = function() { + if (this.initCalled) throw "cannot delete ObjectStore after init() called"; + this.version = Number.MAX_SAFE_INTEGER || Number.MAX_VALUE; + return this.init(true); + }; + + SP.init = function(deleteOS) { + if (this.initCalled) return; + + var storage = this, + name = this.name, + request = null; + + function fallback() { + console.log("in private browsing mode or browser lacks support for IndexedDB. unable to setup storage for '" + name + "'."); + local = {}; + storage.runQueue(); + } + + try { + request = IDB.open(name, this.version); + + request.onupgradeneeded = function(event) { + if (deleteOS) { + storage.db.deleteObjectStore(name); + return; + } + storage.db = request.result; + storage.store = storage.db.createObjectStore(name); + setTimeout(function() { storage.runQueue() }); + }; + + request.onsuccess = function(event) { + storage.db = request.result; + storage.runQueue(); + }; + + request.onerror = function(event) { + console.log({error:event}); + fallback(); + }; + } catch (e) { + fallback(); + return; + } + + this.initCalled = true; + return this; + }; + + SP.runQueue = function() { + if (this.queue.length > 0) { + var i = 0, q = this.queue, e; + while (i < q.length) { + e = q[i++]; + switch (e[0]) { + case 'iterate': this.iterate(e[1], e[2], e[3]); break; + case 'put': this.put(e[1], e[2], e[3]); break; + case 'get': this.get(e[1], e[2]); break; + case 'remove': this.remove(e[1], e[2]); break; + case 'clear': this.clear(); + } + } + this.queue = []; + } + }; + + SP.put = function(key, value, callback) { + if (local) { + local[key] = value; + if (callback) callback(true); + return; + } + if (!this.db) { + this.init(); + return this.queue.push(['put', key, value, callback]); + } + try { + var req = this.db.transaction(this.name, "readwrite").objectStore(this.name).put(value, key); + if (callback) { + req.onsuccess = function(event) { callback(true) }; + req.onerror = function(event) { callback(false) }; + } + } catch (e) { + console.log(e); + if (callback) callback(false); + } + }; + + SP.get = function(key, callback) { + if (local) { + if (callback) callback(local[key]); + return; + } + if (!this.db) { + this.init(); + return this.queue.push(['get', key, callback]); + } + try { + var req = this.db.transaction(this.name).objectStore(this.name).get(key); + if (callback) { + req.onsuccess = function(event) { callback(req.result) }; + req.onerror = function(event) { callback(null) }; + } + } catch (e) { + console.log(e); + if (callback) callback(null); + } + }; + + SP.remove = function(key, callback) { + if (local) { + delete local[key]; + if (callback) callback(true); + return; + } + if (!this.db) { + this.init(); + return this.queue.push(['remove', key, callback]); + } + try { + var req = this.db.transaction(this.name, "readwrite").objectStore(this.name).delete(key); + if (callback) { + req.onsuccess = function(event) { callback(true) }; + req.onerror = function(event) { callback(false) }; + } + } catch (e) { + console.log(e); + if (callback) callback(false); + } + }; + + SP.clear = function(key) { + if (!this.db) { + this.init(); + return this.queue.push(['clear']); + } + this.db.transaction(this.name, "readwrite").objectStore(this.name).clear(); + }; + +})(); diff --git a/js/moto-kv.js b/js/moto-kv.js new file mode 100644 index 00000000..2788207a --- /dev/null +++ b/js/moto-kv.js @@ -0,0 +1,56 @@ +"use strict"; + +var gs_moto_kv = { + copyright:"stewart allen -- all rights reserved" +}; + +(function() { + + if (!self.moto) self.moto = {}; + if (self.moto.KV) return; + + try { + self.moto.KV = self.localStorage; + self.moto.KV.setItem('__test',1); + self.moto.KV.getItem('__test'); + } catch (e) { + console.log("in private browsing mode or 3rd party storage blocked. some settings will be lost."); + self.moto.KV = new KV(); + } + + /** + * @param {String} dbname + * @param {number} [version] + * @constructor + */ + function KV() { + this.__data__ = {}; + this.__mem__ = true; + } + + var KP = KV.prototype; + + KP.getItem = function(key) { + return this[key]; + }; + + KP.setItem = function(key, val) { + this.__data__[key] = val; + this[key] = val; + }; + + KP.removeItem = function(key) { + delete this.__data__[key]; + }; + + KP.clear = function() { + var d = this.__data__, key; + for (key in d) { + if (d.hasOwnProperty(key)) { + delete d[key]; + delete this[key]; + } + } + }; + +})(); diff --git a/js/moto-load-obj.js b/js/moto-load-obj.js new file mode 100644 index 00000000..f570cb5f --- /dev/null +++ b/js/moto-load-obj.js @@ -0,0 +1,239 @@ +/** + * adapted from THREE.OBJLoader example + * + * https://en.wikipedia.org/wiki/Wavefront_.obj_file + */ +'use strict'; + +(function() { + + if (!self.moto) self.moto = {}; + + self.moto.OBJ = { + parse : parse + }; + + /** + * @param {String} text + * @returns {Array} + */ + function parse(text) { + + var object, + objects = [], + geometry, + material; + + function parseVertexIndex(value) { + var index = parseInt(value); + return (index >= 0 ? index - 1 : index + vertices.length / 3) * 3; + } + + function parseNormalIndex(value) { + var index = parseInt(value); + return (index >= 0 ? index - 1 : index + normals.length / 3) * 3; + } + + function parseUVIndex(value) { + var index = parseInt(value); + return (index >= 0 ? index - 1 : index + uvs.length / 2) * 2; + } + + function addVertex(a, b, c) { + geometry.vertices.push( + vertices[a], vertices[a + 1], vertices[a + 2], + vertices[b], vertices[b + 1], vertices[b + 2], + vertices[c], vertices[c + 1], vertices[c + 2] + ); + } + + function addNormal(a, b, c) { + geometry.normals.push( + normals[a], normals[a + 1], normals[a + 2], + normals[b], normals[b + 1], normals[b + 2], + normals[c], normals[c + 1], normals[c + 2] + ); + } + + function addUV(a, b, c) { + geometry.uvs.push( + uvs[a], uvs[a + 1], + uvs[b], uvs[b + 1], + uvs[c], uvs[c + 1] + ); + } + + function addFace(a, b, c, d, ua, ub, uc, ud, na, nb, nc, nd) { + + var ia = parseVertexIndex(a); + var ib = parseVertexIndex(b); + var ic = parseVertexIndex(c); + var id; + + if (d === undefined) { + addVertex(ia, ib, ic); + } else { + id = parseVertexIndex(d); + addVertex(ia, ib, id); + addVertex(ib, ic, id); + } + + if (ua !== undefined) { + ia = parseUVIndex(ua); + ib = parseUVIndex(ub); + ic = parseUVIndex(uc); + + if (d === undefined) { + addUV(ia, ib, ic); + } else { + id = parseUVIndex(ud); + addUV(ia, ib, id); + addUV(ib, ic, id); + } + } + + if (na !== undefined) { + ia = parseNormalIndex(na); + ib = parseNormalIndex(nb); + ic = parseNormalIndex(nc); + + if (d === undefined) { + addNormal(ia, ib, ic); + } else { + id = parseNormalIndex(nd); + addNormal(ia, ib, id); + addNormal(ib, ic, id); + } + } + } + + // create mesh if no objects in text + + if (/^o /gm.test(text) === false) { + geometry = { + vertices: [], + normals: [], + uvs: [] + }; + + object = { + name: '', + geometry: geometry + }; + + objects.push(object); + } + + var vertices = [], + normals = [], + uvs = [], + // v float float float + vertex_pattern = /v(+[\d|\.|\+|\-|e|E]+)(+[\d|\.|\+|\-|e|E]+)(+[\d|\.|\+|\-|e|E]+)/, + // vn float float float + normal_pattern = /vn(+[\d|\.|\+|\-|e|E]+)(+[\d|\.|\+|\-|e|E]+)(+[\d|\.|\+|\-|e|E]+)/, + // vt float float + uv_pattern = /vt(+[\d|\.|\+|\-|e|E]+)(+[\d|\.|\+|\-|e|E]+)/, + // f vertex vertex vertex ... + face_pattern1 = /f(+-?\d+)(+-?\d+)(+-?\d+)(+-?\d+)?/, + // f vertex/uv vertex/uv vertex/uv ... + face_pattern2 = /f(+(-?\d+)\/(-?\d+))(+(-?\d+)\/(-?\d+))(+(-?\d+)\/(-?\d+))(+(-?\d+)\/(-?\d+))?/, + // f vertex/uv/normal vertex/uv/normal vertex/uv/normal ... + face_pattern3 = /f(+(-?\d+)\/(-?\d+)\/(-?\d+))(+(-?\d+)\/(-?\d+)\/(-?\d+))(+(-?\d+)\/(-?\d+)\/(-?\d+))(+(-?\d+)\/(-?\d+)\/(-?\d+))?/, + // f vertex//normal vertex//normal vertex//normal ... + face_pattern4 = /f(+(-?\d+)\/\/(-?\d+))(+(-?\d+)\/\/(-?\d+))(+(-?\d+)\/\/(-?\d+))(+(-?\d+)\/\/(-?\d+))?/, + // split text into lines + lines = text.split('\n'); + + for (var i = 0; i < lines.length; i++) { + + var line = lines[i]; + line = line.trim(); + + var result; + + if (line.length === 0 || line.charAt(0) === '#') { + continue; + } else if ((result = vertex_pattern.exec(line)) !== null) { + // ["v 1.0 2.0 3.0", "1.0", "2.0", "3.0"] + vertices.push( + parseFloat(result[1]), + parseFloat(result[2]), + parseFloat(result[3]) + ); + } else if ((result = normal_pattern.exec(line)) !== null) { + // ["vn 1.0 2.0 3.0", "1.0", "2.0", "3.0"] + normals.push( + parseFloat(result[1]), + parseFloat(result[2]), + parseFloat(result[3]) + ); + } else if ((result = uv_pattern.exec(line)) !== null) { + // ["vt 0.1 0.2", "0.1", "0.2"] + uvs.push( + parseFloat(result[1]), + parseFloat(result[2]) + ); + } else if ((result = face_pattern1.exec(line)) !== null) { + // ["f 1 2 3", "1", "2", "3", undefined] + addFace( + result[1], result[2], result[3], result[4] + ); + } else if ((result = face_pattern2.exec(line)) !== null) { + // ["f 1/1 2/2 3/3", " 1/1", "1", "1", " 2/2", "2", "2", " 3/3", "3", "3", undefined, undefined, undefined] + addFace( + result[2], result[5], result[8], result[11], + result[3], result[6], result[9], result[12] + ); + } else if ((result = face_pattern3.exec(line)) !== null) { + // ["f 1/1/1 2/2/2 3/3/3", " 1/1/1", "1", "1", "1", " 2/2/2", "2", "2", "2", " 3/3/3", "3", "3", "3", undefined, undefined, undefined, undefined] + addFace( + result[2], result[6], result[10], result[14], + result[3], result[7], result[11], result[15], + result[4], result[8], result[12], result[16] + ); + } else if ((result = face_pattern4.exec(line)) !== null) { + // ["f 1//1 2//2 3//3", " 1//1", "1", "1", " 2//2", "2", "2", " 3//3", "3", "3", undefined, undefined, undefined] + addFace( + result[2], result[5], result[8], result[11], + undefined, undefined, undefined, undefined, + result[3], result[6], result[9], result[12] + ); + } else if (/^o /.test(line)) { + + geometry = { + vertices: [], + normals: [], + uvs: [] + }; + + object = { + name: line.substring(2).trim(), + geometry: geometry + }; + + objects.push(object) + + } else if (/^g /.test(line)) { + // group + } else if (/^usemtl /.test(line)) { + // material + material.name = line.substring(7).trim(); + } else if (/^mtllib /.test(line)) { + // mtl file + } else if (/^s /.test(line)) { + // smooth shading + } else { + // unhandled + } + } + + objects.forEach(function(object) { + object.geometry.vertices = new Float32Array(object.geometry.vertices); + object.geometry.normals = new Float32Array(object.geometry.normals); + object.geometry.uvs = new Float32Array(object.geometry.uvs); + }); + + return objects; + } + +})(); diff --git a/js/moto-load-stl.js b/js/moto-load-stl.js new file mode 100644 index 00000000..539e666a --- /dev/null +++ b/js/moto-load-stl.js @@ -0,0 +1,250 @@ +/** + * adapted from THREE.STL-LOADER example + * + * https://en.wikipedia.org/wiki/STL_(file_format) + */ +'use strict'; + +(function() { + + /** + * @constructor + */ + function STL() { + this.vertices = null; + this.normals = null; + this.colors = null; + } + + var SP = STL.prototype; + + if (!self.moto) self.moto = {}; + + self.moto.STL = STL; + + SP.load = function(url, callback) { + var stl = this, + xhr = new XMLHttpRequest(); + + function onloaded (event) { + if (event.target.status === 200 || event.target.status === 0) { + stl.parse(event.target.response || event.target.responseText); + if (callback) callback(stl.vertices); + } else { + if (callback) callback(null, event.target.statusText); + } + } + + xhr.addEventListener('load', onloaded, false); + xhr.addEventListener('progress', function (event) { }, false); + xhr.addEventListener('error', function () { }, false); + + if (xhr.overrideMimeType) { + xhr.overrideMimeType('text/plain; charset=x-user-defined'); + } + + xhr.open('GET', url, true); + xhr.responseType = 'arraybuffer'; + xhr.send(null); + }; + + SP.encode = function(vertices, normals) { + if (!(vertices && vertices.length % 3 === 0)) throw "invalid vertices"; + + var vc = vertices.length / 3, + bs = (vc * 16) + (vc * (2/3)) + 84, + bin = new ArrayBuffer(bs), + writer = new DataView(bin), + i = 0, + j = 0, + pos = 80; + + function writeInt16(val) { + writer.setUint16(pos, val, true); + pos += 2; + } + + function writeInt32(val) { + writer.setUint32(pos, val, true); + pos += 4; + } + + function writeFloat(val) { + writer.setFloat32(pos, val, true); + pos += 4; + } + + function writeVertex() { + writeFloat(vertices[i++]); // x + writeFloat(vertices[i++]); // y + writeFloat(vertices[i++]); // z + } + + writeInt32(vc / 3); + while (i < vertices.length) { + writeFloat(normals ? normals[j++] : 0); // norm x + writeFloat(normals ? normals[j++] : 0); // norm y + writeFloat(normals ? normals[j++] : 0); // norm z + writeVertex(); // p1 + writeVertex(); // p2 + writeVertex(); // p3 + writeInt16(0); // attributes + } + + return bin; + }; + + SP.parse = function(data) { + var binData = this.convertToBinary(data); + + var isBinary = function () { + var expect, face_size, n_faces, reader; + reader = new DataView(binData); + face_size = (32 / 8 * 3) + ((32 / 8 * 3) * 3) + (16 / 8); + n_faces = reader.getUint32(80,true); + expect = 80 + (32 / 8) + (n_faces * face_size); + return expect === reader.byteLength; + }; + + return isBinary() + ? this.parseBinary(binData) + : this.parseASCII(this.convertToString(data)); + }; + + SP.parseBinary = function(data) { + var reader = new DataView(data), + faces = reader.getUint32 (80, true), + r, g, b, hasColors = false, colors, + defaultR, defaultG, defaultB, alpha; + + // check for default color in STL header ("COLOR=rgba" sequence). + for (var index = 0; index < 80 - 10; index++) { + if ((reader.getUint32(index, false) == 0x434F4C4F /*COLO*/) && + (reader.getUint8(index + 4) == 0x52 /*'R'*/) && + (reader.getUint8(index + 5) == 0x3D /*'='*/)) { + hasColors = true; + colors = new Float32Array(faces * 3 * 3); + defaultR = reader.getUint8(index + 6) / 255; + defaultG = reader.getUint8(index + 7) / 255; + defaultB = reader.getUint8(index + 8) / 255; + alpha = reader.getUint8(index + 9) / 255; + } + } + + var offset = 0, + dataOffset = 84, + faceLength = 12 * 4 + 2, + vertices = new Float32Array(faces * 3 * 3), + normals = new Float32Array(faces * 3 * 3), + colors = hasColors ? new Uint16Array(faces * 3 * 3) : null; + + for (var face = 0; face < faces; face ++) { + + var start = dataOffset + face * faceLength, + normalX = reader.getFloat32(start, true), + normalY = reader.getFloat32(start + 4, true), + normalZ = reader.getFloat32(start + 8, true); + + if (hasColors) { + var packedColor = reader.getUint16(start + 48, true); + if ((packedColor & 0x8000) === 0) { // facet has its own unique color + r = (packedColor & 0x1F) / 31; + g = ((packedColor >> 5) & 0x1F) / 31; + b = ((packedColor >> 10) & 0x1F) / 31; + } else { + r = defaultR; + g = defaultG; + b = defaultB; + } + } + + var i = 1, vertexstart; + + while (i <= 3) { + vertexstart = start + (i++) * 12; + vertices[offset ] = reader.getFloat32 (vertexstart, true); + vertices[offset + 1] = reader.getFloat32 (vertexstart + 4, true); + vertices[offset + 2] = reader.getFloat32 (vertexstart + 8, true); + normals[offset ] = normalX; + normals[offset + 1] = normalY; + normals[offset + 2] = normalZ; + if (hasColors) { + colors[offset ] = r; + colors[offset + 1] = g; + colors[offset + 2] = b; + } + offset += 3; + } + } + + this.vertices = vertices; + this.normals = normals; + this.colors = colors; + + return vertices; + }; + + SP.parseASCII = function(data) { + var result, + resultText, + patternNormal, + patternVertex, + vertices = [], + normals = [], + patternFace = /facet([\s\S]*?)endfacet/g; + + while ((result = patternFace.exec(data)) !== null) { + resultText = result[0]; + patternNormal = /normal[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g; + patternVertex = /vertex[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g; + while ((result = patternNormal.exec(resultText)) !== null) { + normals.push(parseFloat(result[1])); + normals.push(parseFloat(result[3])); + normals.push(parseFloat(result[5])); + } + while ((result = patternVertex.exec(resultText)) !== null) { + vertices.push(parseFloat(result[1])); + vertices.push(parseFloat(result[3])); + vertices.push(parseFloat(result[5])); + } + } + + var vToFloat32 = new Float32Array(vertices.length), + nToFloat32 = new Float32Array(normals.length), + i; + + for (i=0; i -- all rights reserved" +}; + +(function() { + + var WIN = window, + DOC = document, + SCENE = new THREE.Scene(), + WORLD = new THREE.Group(), + WC = WORLD.children, + PI = Math.PI, + PI2 = PI / 2, + PI4 = PI / 4, + ROUND = Math.round, + panY = 0, + gridZOff = 0, + platformZOff = 0, + perspective = 35, + refreshRequested = false, + selectRecurse = false, + defaultKeys = true, + lightIntensity = 0.3, + initialized = false, + alignedTracking = false, + skyColor = 0xbbbbbb, + skyGridColor = 0xcccccc, + showSkyGrid = true, + showPlatform = true, + hidePlatformBelow = true, + trackcam = addLight(0, 0, 0, lightIntensity/3), + trackDelta = {x:0, y:0, z:0}, + mouse = {x: 0, y: 0}, + mouseMoved = false, + mouseDragPoint = null, + mouseDragStart = null, + mouseDownSelect, + mouseUpSelect, + mouseHover, + mouseDrag, + gridUnitMinor, + gridUnitMajor, + gridView, + viewControl, + trackPlane, + platform, + platformHover, + platformClick, + platformClickAt, + platformOnMove, + platformMoveTimer, + light1, + light2, + light3, + light4, + light5, + camera, + renderer, + container; + + /** ****************************************************************** + * TWEENing Functions + ******************************************************************* */ + + function tweenit() { + TWEEN.update(); + setTimeout(tweenit, 20); + } + + tweenit(); + + function tweenCamPan(x,y,z) { + var pos = viewControl.getPosition(); + pos.panX = x; + pos.panY = y; + pos.panZ = z; + tweenCam(pos); + } + + function tweenCam(pos) { + var tf = function () { + viewControl.setPosition(this); + refresh(); + }; + new TWEEN.Tween(viewControl.getPosition()). + to(pos, 500). + onUpdate(tf). + start(); + } + + function tweenPlatform(w,h,d) { + var from = {x: platform.scale.x, y: platform.scale.y, z: platform.scale.z}, + to = {x:w, y:h, z:d}, + gridMajor = gridUnitMajor, + gridMinor = gridUnitMinor, + start = function() { + setGrid(0); + }, + update = function() { + setPlatformSize(this.x, this.y, this.z); + refresh(); + }, + complete = function() { + setGrid(gridMajor, gridMinor); + }; + new TWEEN.Tween(from). + to(to, 500). + onStart(start). + onUpdate(update). + onComplete(complete). + start(); + } + + /** ****************************************************************** + * Utility Functions + ******************************************************************* */ + + function width() { return WIN.innerWidth } + + function height() { return WIN.innerHeight } + + function aspect() { return width() / height() } + + function addEventListener(el, key, fn) { + el.addEventListener(key, fn); + } + + function addEventHandlers(el, pairs) { + for (var i=0; i= -w && i <= w) { + if (i % unitMajor === 0) majors.append({x:i, y:-h, z:zp}).append({x:i, y:h, z:zp}); + else if (minors && i % unitMinor === 0) minors.append({x:i, y:-h, z:zp}).append({x:i, y:h, z:zp}); + } + } + for (i = -yo; i <= yo; i++) { + if (i >= -h && i <= h) { + if (i % unitMajor === 0) majors.append({x:-w, y:i, z:zp}).append({x:w, y:i, z:zp}); + else if (minors && i % unitMinor === 0) minors.append({x:-w, y:i, z:zp}).append({x:w, y:i, z:zp}); + } + } + gridView.add(makeLinesFromPoints(majors, colorMajor || 0x999999, 1)); + if (minors) gridView.add(makeLinesFromPoints(minors, colorMinor || 0xcccccc, 1)); + Space.scene.add(gridView); + } + + function refresh() { + refreshRequested = false; + viewControl.update(); + } + + /** deferred refresh that collapses multiple requests */ + function requestRefresh(timeout) { + if (refreshRequested === false) { + refreshRequested = true; + setTimeout(refresh, timeout || 10); + } + } + + function onResize() { + camera.aspect = aspect(); + camera.updateProjectionMatrix(); + renderer.setSize(width(), height()); + container.style.width = width(); + container.style.height = height(); + requestRefresh(); + } + + function alignTracking(point, rot, out) { + if (point && rot) { + alignedTracking = true; + trackPlane.position.set(point.x, point.y, point.z); + trackPlane.rotation.set(rot.x, rot.y, rot.z); + trackDelta = out; + } else { + alignedTracking = false; + trackPlane.position.set(0, 0, 0); + trackPlane.rotation.set(PI2, 0, 0); + } + } + + function cca(c) { + return c.charCodeAt(0); + } + + function inputHasFocus() { + return DOC.activeElement && (DOC.activeElement != DOC.body); + } + + function keyHandler(evt) { + if (!defaultKeys || inputHasFocus()) return false; + if (evt.metaKey) return false; + var handled = true; + switch (evt.charCode) { + case cca('z'): + Space.view.reset(); + break; + case cca('h'): + Space.view.home(); + break; + case cca('f'): + Space.view.front(); + break; + case cca('l'): + Space.view.left(); + break; + case cca('r'): + Space.view.right(); + break; + case cca('t'): + Space.view.top(); + break; + case cca('b'): + Space.view.back(); + break; + default: + handled = false; + break; + } + if (handled) evt.preventDefault(); + return false; + } + + /** ****************************************************************** + * ThreeJS Helper Functions + ******************************************************************* */ + + function makeLinesFromPoints(points, color, width) { + if (points.length % 2 != 0) throw "invalid line : "+points.length; + var geo = new THREE.Geometry(), + i = 0, p1, p2, mesh; + while (i < points.length) { + p1 = points[i++]; + p2 = points[i++]; + geo.vertices.push(new THREE.Vector3(p1.x, p1.y, p1.z)); + geo.vertices.push(new THREE.Vector3(p2.x, p2.y, p2.z)); + } + geo.verticesNeedUpdate = true; + mesh = new THREE.LineSegments(geo, new THREE.LineBasicMaterial({ + color: color, + linewidth: width || 1 + })); + return mesh; + } + + function intersect(objects, recurse) { + var lookAt = new THREE.Vector3(mouse.x, mouse.y, 0.0).unproject(camera); + var ray = new THREE.Raycaster(camera.position, lookAt.sub(camera.position).normalize()); + return ray.intersectObjects(objects, recurse); + } + + /** ****************************************************************** + * Mouse Functions + ******************************************************************* */ + + function onMouseDown(event) { + if (event.target === renderer.domElement) { + DOC.activeElement.blur(); + event.preventDefault(); + var selection = null, + trackTo = alignedTracking ? trackPlane : platform; + if (mouseDownSelect) selection = mouseDownSelect(); + if (selection && selection.length > 0) { + trackTo.visible = true; + var int = intersect(selection.slice().append(trackTo), false); + trackTo.visible = false; + if (int.length > 0) { + var trackInt, selectInt; + for (var i=0; i 0 ? int[0].point : null; + } + } else { + viewControl.enabled = false; + } + mouseMoved = false; + } + + function onMouseUp(event) { + if (!viewControl.enabled) { + viewControl.enabled = true; + viewControl.onMouseUp(event); + } + if (!mouseMoved) { + event.preventDefault(); + if (!mouseMoved) { + var refresh = false, + selection = null; + if (mouseUpSelect) selection = mouseUpSelect(); + if (selection && selection.length > 0) { + var int = intersect(selection, selectRecurse); + if (int.length > 0) { + mouseUpSelect(int[0], event); + refresh = true; + } else { + mouseUpSelect(null, event); + } + } + if (!refresh && platformClickAt) { + platformClick(platformClickAt); + } + if (refresh) requestRefresh(); + } + } else if (mouseDrag && mouseDragStart) { + mouseDrag(null,null,true); + } + mouseDragPoint = null; + mouseDragStart = null; + } + + function onMouseMove(event) { + var int, vis; + if (viewControl.enabled) { + event.preventDefault(); + var selection = mouseHover ? mouseHover() : null; + if (selection && selection.length > 0) { + int = intersect(selection, selectRecurse); + if (int.length > 0) mouseHover(int[0], event); + } + if ((!int || int.length == 0) && platformHover) { + vis = platform.visible; + platform.visible = true; + int = intersect([platform], false); + platform.visible = vis; + if (int && int.length > 0) platformHover(int[0].point); + } + } else if (mouseDragPoint && mouseDrag && mouseDrag()) { + event.preventDefault(); + var trackTo = alignedTracking ? trackPlane : platform; + trackTo.visible = true; + int = intersect([trackTo], false); + trackTo.visible = false; + if (int.length > 0 && int[0].object === trackTo) { + var delta = mouseDragPoint.clone().sub(int[0].point); + var offset = mouseDragStart.clone().sub(int[0].point); + mouseDragPoint = int[0].point; + mouseDrag({x: -delta.x, y: delta.z}, offset.multiplyVectors(offset, trackDelta)); + requestRefresh(); + } + } + mouseMoved = true; + mouse = { + x: (event.clientX / width()) * 2 - 1, + y: -(event.clientY / height()) * 2 + 1}; + } + + /** ****************************************************************** + * Space Object + ******************************************************************* */ + + var Space = { + alignTracking: alignTracking, + addEventListener: addEventListener, + addEventHandlers: addEventHandlers, + onEnterKey: onEnterKey, + onResize: onResize, + update: requestRefresh, + + showSkyGrid: function(b) { showSkyGrid = b }, + setSkyColor: function(c) { skyColor = c }, + setSkyGridColor: function(c) { skyGridColor = c }, + + scene: { + add: function (o) { + o.rotation.x = WORLD.rotation.x; + return SCENE.add(o); + }, + remove: function (o) { + return SCENE.remove(o); + } + }, + + platform: { + tweenTo: tweenPlatform, + setSize: setPlatformSizeUpdateGrid, + setColor: setPlatformColor, + setGrid: setGrid, + + add: function(o) { WORLD.add(o) }, + remove: function(o) { WORLD.remove(o) }, + setMaxZ: function(z) { panY = z / 2 }, + isHidden: function() { return !showPlatform }, + setHidden: function(b) { showPlatform = !b; platform.visible = !b }, + setHiding: function(b) { hidePlatformBelow = b }, + setZOff: function(z) { platformZOff = z; updatePlatformPosition() }, + setGZOff: function(z) { gridZOff = z; updatePlatformPosition() }, + opacity: function(o) { platform.material.opacity = o }, + + onMove: function(f) { platformOnMove = f }, + onHover: function(f) { platformHover = f }, + onClick: function(f) { platformClick = f}, + + size: function() { return platform.scale }, + isVisible: function() { return platform.visible }, + + showGrid: function(b) { gridView.visible = b } + }, + + view: { + top: function() { tweenCam({left: 0, up: 0, panX: 0, panY: panY, panZ: 0}) }, + back: function() { tweenCam({left: PI, up: PI2, panX: 0, panY: panY, panZ: 0}) }, + home: function() { tweenCam({left: 0, up: PI4, panX: 0, panY: panY, panZ: 0}) }, + front: function() { tweenCam({left: 0, up: PI2, panX: 0, panY: panY, panZ: 0}) }, + right: function() { tweenCam({left: PI2, up: PI2, panX: 0, panY: panY, panZ: 0}) }, + left: function() { tweenCam({left: -PI2, up: PI2, panX: 0, panY: panY, panZ: 0}) }, + + panTo: function(x,y,z) { tweenCamPan(x,y,z) }, + + setZoom: function(r,v) { viewControl.setZoom(r,v) }, + + reset: function() { viewControl.reset(); requestRefresh() }, + load: function(cam) { viewControl.setPosition(cam) }, + save: function() { return viewControl.getPosition(true) } + }, + + mouse: { + downSelect: function(f) { mouseDownSelect = f }, + upSelect: function(f) { mouseUpSelect = f }, + onDrag: function(f) { mouseDrag = f }, + onHover: function(f) { mouseHover = f } + }, + + useDefaultKeys: function(b) { + defaultKeys = b; + }, + + selectRecurse: function(b) { + selectRecurse = b; + }, + + objects: function() { + return WC; + }, + + init: function(domelement, slider) { + container = domelement; + + WORLD.rotation.x = -PI2; + SCENE.add(WORLD); + + domelement.style.width = width(); + domelement.style.height = height(); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + camera = perspective ? + new THREE.PerspectiveCamera(perspective, aspect(), 1, 100000) : + new THREE.OrthographicCamera(-100 * aspect(), 100 * aspect(), 100, -100, 0.1, 100000); + + camera.position.set(0, 200, 340); + renderer.setSize(width(), height()); + domelement.appendChild(renderer.domElement); + + viewControl = new THREE.CubeControls(camera, domelement, function (position, moved) { + if (platform) platform.visible = hidePlatformBelow ? initialized && position.y >= 0 && showPlatform : showPlatform; + if (trackcam) trackcam.position.copy(camera.position); + renderer.render(SCENE, camera); + if (moved && platformOnMove) { + if (platformMoveTimer) clearTimeout(platformMoveTimer); + platformMoveTimer = setTimeout(platformOnMove, 500); + } + }, slider); + + viewControl.noKeys = true; + viewControl.maxDistance = 1000; + + SCENE.add(new THREE.AmbientLight(0x707070)); + + light1 = addLight( 200, 250, 200, lightIntensity * 1.15); + light2 = addLight(-200, 250, -200, lightIntensity * 0.95); + light3 = addLight( 0, -200, 0, lightIntensity * 0.5); + light4 = addLight( 200, 5, -200, lightIntensity * 0.35); + light5 = addLight(-200, 5, 200, lightIntensity * 0.4); + + platform = new THREE.Mesh( + new THREE.BoxGeometry(1, 1, 1), + new THREE.MeshPhongMaterial({ + color: 0xcccccc, + specular: 0xcccccc, + shininess: 5, + transparent: true, + opacity: 0.6, + side: THREE.DoubleSide + }) + ); + + platform.position.y = platformZOff; + platform.rotation.x = -PI2; + platform.visible = showPlatform; + + trackPlane = new THREE.Mesh( + new THREE.PlaneBufferGeometry(2000, 2000, 1, 1), + new THREE.MeshBasicMaterial( { color: 0x777777, opacity: 0.3, transparent: false, side:THREE.DoubleSide } ) + ); + trackPlane.visible = false; + trackPlane.rotation.x = PI2; + + var sky = new THREE.Mesh( + new THREE.BoxGeometry(50000, 50000, 50000, 1, 1, 1), + new THREE.MeshBasicMaterial({ color: skyColor, side: THREE.DoubleSide }) + ), + skygrid = new THREE.Mesh( + new THREE.BoxGeometry(5000, 5000, 5000, 10, 10, 10), + new THREE.MeshBasicMaterial({ color: skyGridColor, side: THREE.DoubleSide }) + ); + + + SCENE.add(platform); + SCENE.add(trackPlane); + SCENE.add(sky); + + if (showSkyGrid) { + skygrid.material.wireframe = true; + SCENE.add(skygrid); + } + + addEventHandlers(WIN, [ + 'resize', onResize, + 'mousemove', onMouseMove, + 'mousedown', onMouseDown, + 'mouseup', onMouseUp, + 'keypress', keyHandler + ]); + + initialized = true; + } + }; + + /** ****************************************************************** + * Connect to moto + ******************************************************************* */ + + if (!window.moto) window.moto = {}; + window.moto.Space = Space; + +})(); diff --git a/js/moto-ui.js b/js/moto-ui.js new file mode 100644 index 00000000..1473f320 --- /dev/null +++ b/js/moto-ui.js @@ -0,0 +1,323 @@ +"use strict"; + +var gs_moto_ui = { + copyright:"stewart allen -- all rights reserved" +}; + +(function() { + + var moto = self.moto = self.moto || {}; + if (moto.ui) return; + + var SELF = self, + lastGroup = null, + lastDiv = null, + hideAction = null, + inputAction = null, + hasModes = [], + SDB = moto.KV, + DOC = SELF.document, + prefix = "tab"; + + SELF.$ = (SELF.$ || function (id) { return DOC.getElementById(id) } ); + + moto.ui = { + prefix: function(pre) { prefix = pre; return moto.ui }, + hideAction: function(fn) { hideAction = fn; return moto.ui }, + inputAction: function(fn) { inputAction = fn; return moto.ui }, + setMode: setMode, + bound: bound, + toInt: toInt, + toFloat: toFloat, + newLabel: newLabel, + newRange: newRangeField, + newInput: newInputField, + newButton: newButton, + newBoolean: newBooleanField, + newSelectField: newSelectField, + newTable: newTables, + newTableRow: newTableRow, + newRow: newRow, + newBlank: newBlank, + newGroup: newGroup, + setGroup: setGroup + }; + + function setMode(mode) { + hasModes.forEach(function(div) { + div.setMode(mode); + }); + } + + function isControlGroupVisible(label, key) { + var ls = SDB.getItem(key), + dv = true; + return ls ? ls !== 'false' : dv !== undefined ? dv : true; + } + + function setGroup(div) { + lastDiv = div; + return div; + } + + function newGroup(label, div, options) { + lastDiv = div || lastDiv; + return addCollapsableGroup(label, lastDiv, options); + } + + function addCollapsableGroup(label, div, options) { + var row = DOC.createElement('div'), + a = DOC.createElement('a'), + dbkey = prefix+'-show-'+label; + + lastGroup = "ck_"+label; + div.appendChild(row); + row.setAttribute("class", "grouphead noselect"); + row.setAttribute("id", lastGroup); + row.appendChild(a); + a.appendChild(DOC.createTextNode(label)); + a.setAttribute("ck", lastGroup); + a.setAttribute("id", lastGroup+"_label"); + addModeControls(row, options); + return row; + } + + function toInt() { + var nv = this.value !== '' ? parseInt(this.value) : null; + if (nv !== null && this.bound) nv = this.bound(nv); + this.value = nv; + return nv; + } + + function toFloat() { + var nv = this.value !== '' ? parseFloat(this.value) : null; + if (nv !== null && this.bound) nv = this.bound(nv); + this.value = nv; + return nv; + } + + function bound(low,high) { + return function(v) { + return v < low ? low : v > high ? high : v; + }; + } + + function raw() { + return this.value !== '' ? this.value : null; + } + + function newLabel(text) { + var label = DOC.createElement('label'); + label.appendChild(DOC.createTextNode(text)); + label.setAttribute("class", "noselect"); + return label; + } + + function addId(el, options) { + if (options && options.id) { + el.setAttribute("id", options.id); + } + } + + function addModeControls(el, options) { + el.__show = true; + el.__modeSave = null; + el.showMe = function() { + if (el.__show) return; + el.style.display = el.__modeSave; + el.__show = true; + el.__modeSave = null; + }; + el.hideMe = function() { + if (!el.__show) return; + el.__show = false; + el.__modeSave = el.style.display; + el.style.display = 'none'; + }; + el.setVisible = function(show) { + if (show) el.showMe(); + else el.hideMe(); + }; + el.setMode = function(mode) { + el.setVisible(el.modes.contains(mode)); + } + el.hasMode = function(mode) { + return el.modes.contains(mode); + } + if (options && options.modes) { + el.modes = options.modes; + hasModes.push(el); + } + } + + function newDiv(options) { + var div = DOC.createElement('div'); + addModeControls(div, options); + return div; + } + + function newInputField(label, options) { + var row = newDiv(options), + hide = options && options.hide, + size = options ? options.size || 5 : 5, + height = options ? options.height : 0, + ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input'), + action = inputAction; + lastDiv.appendChild(row); + row.appendChild(newLabel(label)); + row.appendChild(ip); + row.setAttribute("class", ["flow-row",lastGroup].join(" ")); + if (height > 1) { + ip.setAttribute("cols", size); + ip.setAttribute("rows", height); + ip.setAttribute("wrap", "off"); + } else { + ip.setAttribute("size", size); + } + ip.setAttribute("type", "text"); + row.style.display = hide ? 'none' : ''; + if (options) { + if (options.disabled) ip.setAttribute("disabled", "true"); + if (options.title) row.setAttribute("title", options.title); + if (options.convert) ip.convert = options.convert.bind(ip); + if (options.bound) ip.bound = options.bound; + if (options.action) action = options.action; + } + if (action) { + ip.addEventListener('keyup', function(event) { + if (event.keyCode === 13) action(event); + }); + ip.addEventListener('blur', function(event) { + action(event); + }); + } + if (!ip.convert) ip.convert = raw.bind(ip); + ip.setVisible = row.setVisible; + return ip; + } + + function newRangeField(label, options) { + var row = newDiv(options), + ip = DOC.createElement('input'), + hide = options && options.hide, + action = inputAction; + lastDiv.appendChild(row); + if (label) row.appendChild(newLabel(label)); + row.appendChild(ip); + row.setAttribute("class", ["flow-row",lastGroup].join(" ")); + ip.setAttribute("type", "range"); + ip.setAttribute("min", (options && options.min ? options.min : 0)); + ip.setAttribute("max", (options && options.max ? options.max : 100)); + ip.setAttribute("value", 0); + row.style.display = hide ? 'none' : ''; + if (options) { + if (options.title) { + ip.setAttribute("title", options.title); + row.setAttribute("title", options.title); + } + if (options.action) action = options.action; + } + ip.setVisible = row.setVisible; + return ip; + } + + function newSelectField(label, options) { + var row = newDiv(options), + ip = DOC.createElement('select'), + hide = options && options.hide, + action = inputAction; + lastDiv.appendChild(row); + row.appendChild(newLabel(label)); + row.appendChild(ip); + row.setAttribute("class", ["flow-row",lastGroup].join(" ")); + row.style.display = hide ? 'none' : ''; + if (options) { + if (options.convert) ip.convert = options.convert.bind(ip); + if (options.disabled) ip.setAttribute("disabled", "true"); + if (options.title) row.setAttribute("title", options.title); + if (options.action) action = options.action; + } + ip.onchange = function() { action() }; + ip.setVisible = row.setVisible; + return ip; + } + + function newBooleanField(label, action, options) { + var row = newDiv(options), + ip = DOC.createElement('input'), + hide = options && options.hide; + lastDiv.appendChild(row); + if (label) row.appendChild(newLabel(label)); + row.appendChild(ip); + row.setAttribute("class", ["flow-row",lastGroup].join(" ")); + row.style.display = hide ? 'none' : ''; + ip.setAttribute("type", "checkbox"); + ip.checked = false; + if (options) { + if (options.disabled) ip.setAttribute("disabled", "true"); + if (options.title) { + ip.setAttribute("title", options.title); + row.setAttribute("title", options.title); + } + } + if (action) ip.onclick = function() { action() }; + ip.setVisible = row.setVisible; + return ip; + } + + function newBlank(options) { + var row = newDiv(options), + hide = options && options.hide; + lastDiv.appendChild(row); + row.setAttribute("class", ["flow-row",lastGroup].join(" ")); + row.style.display = hide ? 'none' : ''; + ip.setVisible = row.setVisible; + return ip; + } + + function newButton(label, action, options) { + var b = DOC.createElement('button'), + t = DOC.createTextNode(label); + b.appendChild(t); + b.onclick = function() { action() }; + addModeControls(b, options); + addId(b, options); + return b; + } + + function newTableRow(arrayOfArrays, options) { + return newRow(newTables(arrayOfArrays), options); + } + + function newTables(arrayOfArrays) { + var array = []; + for (var i=0; i= 0; +}; + +Array.prototype.appendAll = function(a) { + this.push.apply(this,a); + return this; +}; + +function log(o) { + if (!o.time) o.time = time(); + console.log(obj2string(o)); +} + +function promise(resolve, reject) { + return new Promise(resolve, reject); +} + +function rval() { + return (Math.round(Math.random()*0xffffffff)).toString(36); +} + +function guid() { + return time().toString(36)+rval()+rval()+rval(); +} + +function time() { + return new Date().getTime(); +} + +/** + * @param {String[]} path + */ +function mkdirs(path) { + var root = ""; + path.forEach(seg => { + if (root) { + root = root + "/" + seg; + } else { + root = seg; + } + lastmod(root) || fs.mkdirSync(root); + }); +} + +/** + * @param {String} o + */ +function obj2string(o) { + return JSON.stringify(o); +} + +function string2obj(s) { + return JSON.parse(s); +} + +/** + * sync return mtime for file or 0 for no such file + * @param {String} path + * @returns {number} + */ +function lastmod(path) { + try { + return fs.statSync(path).mtime.getTime(); + } catch (e) { + return 0; + } +} + +/** + * @param {String} filePath + * @param {Function} fn + * @returns {*} + */ +function getCachedFile(filePath, fn) { + var cachePath = "cache/"+filePath.replace(/\//g,"_"), + cached = fileCache[filePath], + now = time(); + + if (cached) { + if (now - cached.lastcheck > 60000) { + var smod = lastmod(filePath), + cmod = cached.mtime; + + if (!smod) throw "missing source file"; + if (smod > cmod) cached = null; + + cached.lastcheck = now; + } + } + + if (!cached) { + var smod = lastmod(filePath), + cmod = lastmod(cachePath), + cacheData; + + if (cmod >= smod) { + cacheData = fs.readFileSync(cachePath); + } else { + console.log({update_cache:filePath}); + cacheData = fn(filePath); + fs.writeFileSync(cachePath, cacheData); + } + + cached = { + data: cacheData, + mtime: cmod || now, + lastcheck: now + }; + + fileCache[filePath] = cached; + } + + return cached.data; +} + +/** + * mangle, cache and concatenate scripts + */ +function prepareScripts() { + code.kiri = concatCode(script.kiri); + code.meta = concatCode(script.meta); + code.work = concatCode(script.work); + code.worker = concatCode(script.worker); + inject.kiri_local = htmlScript(codePrefix, script.kiri); + inject.meta_local = htmlScript(codePrefix, script.meta); + inject.kiri = htmlScript("code/", ["kiri"]); + inject.meta = htmlScript("code/", ["meta"]); + fs.readdir("./web/kiri/filter/FDM", function(err, files) { + filters_fdm = files || filters_fdm; + }); + fs.readdir("./web/kiri/filter/CAM", function(err, files) { + filters_cam = files || filters_cam; + }); +} + +/** + * @param {String} name + * @oaram {Array} list + * @returns {String} + */ +function htmlScript(prefix, list) { + var code = []; + + list.forEach(file => { + code.push('\t'); + }); + + return code.join("\n"); +} + +/** + * @param {Array} array + * @returns {String} + */ +function concatCode(array) { + var code = [], + cached, + path; + + array.forEach(file => { + path = file.charAt(0) === '/' ? file : codePrefix + file + ".js"; + cached = getCachedFile(path, function(path) { + return uglify.minify(path).code; + }); + code.push(cached); + }); + + return code.join(''); +} + +/** + * @param {String} cookie + * @param {String} [key] + * @returns {String | null} + */ +function getCookieValue(cookie,key) { + if (!cookie) return null; + key = (key || "key") + "="; + var kpos = cookie.lastIndexOf(key); + if (kpos >= 0) { + return cookie.substring(kpos+key.length).split(';')[0]; + } else { + return null; + } +} + +/** + * @param {String} ip + */ +function isNotLocal(ip) { + return ipLocal.contains(ip) ? null : ip; +} + +/** + * @param {Object} req + * @returns {String} + */ +function remoteIP(req) { + var ip = isNotLocal(req.headers['x-forwarded-for']) || + req.socket.remoteAddress || + req.connection.remoteAddress, + ipa = ip.split(','); + // if (ipa.length > 1) console.log({remote:ipa}); + return ipa[0]; +} + +/** + * @param {Object} res + * @param {number} code + * @param {String} msg + */ +function quickReply(res, code, msg) { + res.writeHead(code); + res.end(msg+"\n"); +} + +/** + * @param {Object} req + * @param {Object} res + */ +function reply404(req, res) { + log({"404":req.url, ip:req.gs.ip}); + res.writeHead(404); + res.end("[404]"); +} + +/** + * @param {Object} res + * @param {String} url + */ +function redirect(res, url) { + res.writeHead(302, { "Location": url }); + res.end(); +} + +/** + * @param {Array} array + * @param {number} length + * @param {number} timespan + * @returns {number} + */ +function limit(array, length, timespan) { + var now = time(), + limit = 0; + // age out entries older than timespan + while (array.length > 0 && now-array[0] > timespan) { + array.shift(); + } + // count elements over the limit + while (array.length > length) { + limit++; + array.shift(); + } + return limit; +} + +/** + * @param {Object} req + * @param {Object} res + * @param {Function} next + */ +function setup(req, res, next) { + var parsed = url.parse(req.url, true), + ipaddr = remoteIP(req), + dbikey = db.key(["ip",ipaddr]), + path = parsed.pathname, + time = new Date().getTime(), + rec = ipCache[ipaddr] || { + saved : 0, + first: time, + last: [], + hits: 0, + api: [], + ip: ipaddr, + }, + ua = 'unknown'; + + try { + ua = agent.parse(req.headers['user-agent'] || ''); + } catch (e) { + console.log("ua parse error on : "+req.headers['user-agent']); + } + + // cache it + if (rec.saved === 0) { + ipCache[ipaddr] = rec; + if (!rec.host) { + // prevent overlapping lookups + // for the same address + rec.host = 'unknown'; + dns.reverse(ipaddr,(err,addr) => { + rec.host = addr; + // if (addr) log({ip:ipaddr, addr:addr}); + }); + } + } + + // grid.space request state + req.gs = { + ua: ua, + ip: ipaddr, + iprec: rec, + local: ipLocal.contains(ipaddr), + port: req.socket.address().port, + url: parsed, + path: parsed.pathname, + query: parsed.query, + }; + + // track clients & show first instance of IP + rec.last.push(time); + rec.hits++; + + // update db ip record + if (time - rec.saved > ipSaveDelay) db.get(dbikey) + .then(dbrec => { + // only on the first pull from disk + if (dbrec && rec.saved === 0) { + rec.first = dbrec.first; + rec.hits += dbrec.hits; + rec.last = dbrec.last.appendAll(rec.last); + rec.api = dbrec.api.appendAll(rec.api); + } + rec.saved = time; + return rec; + }) + .then(dbrec => { + if (rec.putTO) clearTimeout(rec.putTO); + rec.putTO = setTimeout(() => { + rec.putTO = null; + db.put(dbikey, dbrec); + }, ipSaveDelay); + }); + + // absolute limit on client requests per minute + if (limit(rec.last, 300, 60000) && !req.gs.local) { + res.writeHead(503); + res.end("rate limited"); + return log({rate_limit:ipaddr, len:rec.last.length}); + } + + if (req.method === 'OPTIONS') { + addCorsHeaders(req, res); + res.end(); + } else { + next(); + } +} + +function addCorsHeaders(req, res) { + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader('Access-Control-Allow-Headers', 'X-Moto-Ajax, Content-Type'); + res.setHeader("Access-Control-Allow-Origin", req.headers['origin'] || '*'); + res.setHeader("Allow", "GET,POST,OPTIONS"); +} + +/** + * @param {Object} req + * @param {Object} res + * @param {Function} next + */ +function handleData(req, res, next) { + addCorsHeaders(req, res); + res.setHeader('Cache-Control', 'private, no-cache, max-age=0'); + + var tok = req.url.split('/'), + muid = req.headers['x-moto-ajax'], + space = tok[2] || null, + version = tok[3], + valid = space && space.length >= 4 && space.length <= 8; + + function genKey() { + while (true) { + var k = Math.round(Math.random() * 9999999999).toString(36); + if (k.length >= 4 && k.length <= 8) return k; + } + } + + function countKey(space) { + return db.key(['meta/counter',space]); + } + + function ownerKey(space) { + return db.key(['meta/owner',muid,'space',space]); + } + + function recordKey(space, version) { + return db.key(["meta/space",space,version]); + } + + // retrieve latest space data + if (valid && req.method === 'GET' && valid) { + function send(rec, version) { + if (rec) { + res.write(obj2string({space:space,ver:version,rec:rec})); + res.end(); + } else { + res.end(); + } + } + + function retrieve(version) { + return db.get(recordKey(space,version)) + .then(record => { + send(record || null, version); + }) + } + + if (version) { + retrieve(version) + } else { + db.get(countKey(space)).then(version => retrieve(version)); + } + + return; + + } else if (valid && req.method === 'POST') { + + var dbOwner = null, + dbVersion = null, + postBody = null, + iprec = req.gs.iprec, + spacein = space, + version = 0, + body = ''; + + function checkDone() { + if (!(dbVersion && postBody)) return; + // if not owner, assign new space id + if (dbVersion > 1) { + if (!dbOwner) { + space = genKey(); + version = 1; + log({forked:space,from:spacein,by:muid}); + } + } + // log what we have + log({ + space: space, + ver: dbVersion, + uid: muid, + ip: iprec.ip, + hits: iprec.hits, + size: postBody.length + }); + if (muid && muid.length > 0) { + level.put(recordKey(space, dbVersion), body); + level.put(ownerKey(space), {ip: iprec.ip, time: time(), ver: dbVersion}); + level.put(countKey(space), dbVersion); + } + res.end(obj2string({space: space, ver: dbVersion})); + } + + // accumulate post body + req.on('data', data => { body += data }); + req.on('end', () => { + postBody = body; + checkDone(); + }); + + // fetch owner and version information + db.get(ownerKey(space)) + .then(owner => { + dbOwner = owner; + return db.get(countKey(space)); + }) + .then(version => { + dbVersion = parseInt(version || "0") + 1; + checkDone(); + }); + + return; + } +} + +/** + * @param {Object} req + * @param {Object} res + * @param {Function} next + */ +function handleJS(req, res, next) { + if (!req.gs.local) return reply404(req, res); + + var spath = req.gs.path.substring(1), + jspos = spath.indexOf(".js"), + fpath = jspos > 0 ? spath.substring(0,jspos+3) : spath, + cached = fileCache[fpath]; + + fs.stat(fpath, (err, f) => { + if (err || !f) return reply404(req, res); + + var mtime = f.mtime.getTime(); + + if (!cached || cached.mtime != mtime) { + if (clearJS) { + fs.readFile(fpath, null, function(err, code) { + if (err) return reply404(req,res); + serveCode(req, res, fileCache[fpath] = { + clear: true, + mtime: mtime, + code: code + }); + }); + return; + } else { + var start = new Date().getTime(), + code = uglify.minify([fpath]).code, + end = new Date().getTime(); + + log({minify:fpath, in:f.size, out:code.length, time:(end-start)}); + + cached = fileCache[fpath] = { + clear: false, + mtime: mtime, + code: code + }; + } + } + + serveCode(req, res, cached); + }); +} + +/** + * @param {Object} req + * @param {Object} res + * @param {Function} next + */ +function handleCode(req, res, next) { + var cookie = getCookieValue(req.headers.cookie), + key = req.gs.path.split('/')[2].split('.')[0], + js = code[key]; + + if (!js) return reply404(req, res); + + addCorsHeaders(req, res); + serveCode(req, res, { + code: js, + mtime: startTime + }); +} + +function ifModifiedDate(req) { + var ims = req.headers['if-modified-since']; + if (ims) { + // because sys time has a higher resolution than + // seconds converted from IMS header. so give it + // an extra second + return new Date(ims).getTime() + 1000; + } + return 0; +} + +/** + * @param {Object} req + * @param {Object} res + * @param {Obejct} code + */ +function serveCode(req, res, code) { + if (code.deny) return reply404(req, res); + + var imd = ifModifiedDate(req); + if (imd && code.mtime <= imd && !code.nocache) { + res.writeHead(304, "Not Modified"); + res.end(); + return; + } + + var cacheControl = code.nocache ? + 'private, max-age=0' : + 'public, max-age=600'; + + res.writeHead(200, { + 'Content-Type': 'application/javascript', + 'Cache-Control': cacheControl, + 'Last-Modified': new Date(code.mtime).toGMTString(), + }); + res.end(code.code); +} + +/** + * @param {Object} req + * @param {Object} res + * @param {Function} next + */ +function rewriteHTML(req, res, next) { + function sendHTML(entry) { + var imd = ifModifiedDate(req); + if (imd && entry.mtime <= imd) { + res.writeHead(304, "Not Modified"); + res.end(); + return; + } + res.writeHead(200, { + 'Last-Modified': new Date(entry.mtime).toGMTString(), + 'Cache-Control': 'public, max-age=600', + 'Content-Type': 'text/html' + }); + res.write(entry.data); + res.end(); + } + + if (req.url.indexOf(".html") > 0) { + var local = req.gs.local, + key = local ? "local_" + req.url : req.url, + path = "web" + req.gs.path, + mtime = lastmod(path), + cached = fileCache[key], + replaced = false, + magic; + + if (mtime === 0) return next(); + if (cached && mtime === cached.mtime) return sendHTML(cached); + + fs.readFile(path, (err, data) => { + if (!data) return next(); + data = data.toString(); + + injectKeys.forEach(key => { + if (replaced) return; + magic = ""; + if (data.indexOf(magic) > 0) { + data = data.replace(magic, inject[local ? key + "_local" : key]); + replaced = true; + } + }); + + if (replaced) { + cached = fileCache[key] = { + data: data, + mtime: mtime + }; + sendHTML(cached); + } else { + next(); + } + }); + } else { + next(); + } +} + +/* ********************************************* + * Setup / Global + ********************************************* */ + +var clearJS = false, + noLocal = false + port = 8080, + args = process.argv.slice(2); + +args.forEach((arg, index) => { + switch (arg) { + case 'nolocal': noLocal = true; break; + case 'debug': clearJS = true; break; + case 'port': port = process.argv[index+3]; break; + } +}); + +var ver = require('../js/license.js'), + fs = require('fs'), + url = require('url'), + dns = require('dns'), + util = require('util'), + valid = require('validator'), + agent = require('express-useragent'), + spawn = require('child_process').spawn, + level = require('levelup')('./persist/', {valueEncoding:"json"}), + https = require('https'), + uglify = require('uglify-js'), + connect = require('connect'), + request = require('request'), + serveStatic = require('serve-static'), + compression = require('compression')(), + querystring = require('querystring'), + ipLocal = noLocal ? [] : ["127.0.0.1", "::1", "::ffff:127.0.0.1"], + currentDir = process.cwd(), + ipSaveDelay = 2000, + startTime = time(), + codePrefix = "js/", + fileCache = {}, + filters_fdm = [], + filters_cam = [], + modPaths = [], + ipCache = {}, + script = { + kiri : [ + "license", + "ext-n3d", + "ext-clip", + "ext-tween", + "ext-fsave", + "add-array", + "add-three", + "geo", + "geo-debug", + "geo-render", + "geo-point", + "geo-slope", + "geo-line", + "geo-bounds", + "geo-polygon", + "geo-polygons", + "moto-kv", + "moto-ajax", + "moto-ctrl", + "moto-space", + "moto-load-stl", + "moto-db", + "moto-ui", + "kiri-db", + "kiri-slice", + "kiri-slicer", + "kiri-driver-fdm", + "kiri-driver-cam", + "kiri-driver-laser", + "kiri-pack", + "kiri-layer", + "kiri-widget", + "kiri-print", + "kiri-codec", + "kiri-work", + "kiri", + "kiri-serial" + ], + meta : [ + "license", + "ext-n3d", + "ext-tween", + "ext-fsave", + "add-array", + "add-three", + "moto-kv", + "moto-ajax", + "moto-ctrl", + "moto-space", + "moto-load-stl", + "moto-db", + "moto-ui", + "kiri-db", + "meta" + ], + work : [ + "license", + "ext-n3d", + "ext-clip", + "add-array", + "add-three", + "geo", + "geo-debug", + "geo-point", + "geo-slope", + "geo-line", + "geo-bounds", + "geo-polygon", + "geo-polygons", + "kiri-slice", + "kiri-slicer", + "kiri-driver-fdm", + "kiri-driver-cam", + "kiri-driver-laser", + "kiri-pack", + "kiri-widget", + "kiri-print", + "kiri-codec" + ], + worker : [ + "license", + "kiri-work" + ] + }, + code = {}, + inject = {}, + injectKeys = ["kiri", "meta"]; + +/* ********************************************* + * Promises-based leveldb interface + ********************************************* */ +const db = { + // -------- + key: arr => arr.join("/"), + // -------- + get: key => { + if (Array.isArray(key)) key = db.key(key); + return promise((resolve,reject) => { + level.get(key,(err,record) => { + resolve(record,err); + }); + }); + }, + + // -------- + put: (key, value) => { + if (Array.isArray(key)) key = db.key(key); + return promise((resolve,reject) => { + level.put(key,value,(err) => { + if (err) reject(err); + else resolve(); + }); + }); + }, + // -------- + del: key => { + return promise((resolve,reject) => { + level.del(key, (err) => { + if (err) reject(err); + else resolve(); + }); + }); + } +}; + +/* ********************************************* + * REST API (extensible in modules) + ********************************************* */ +const api = { + + rateLimit: (req, res, next) => { + req.gs.iprec.api.push(time()); + + // allow 60 calls in 60 seconds + if (limit(req.gs.iprec.api, 60, 60000) && !req.gs.local) { + quickReply(res, 503, "rate limited"); + try { log({rate_limit_api: req.gs.ip}); } catch (e) { console.log(e) } + } else { + next(); + } + }, + + "filters-fdm": (req, res, next) => { + res.end(obj2string(filters_fdm)); + }, + + "filters-cam": (req, res, next) => { + res.end(obj2string(filters_cam)); + } + +}; + +/* ********************************************* + * Dispatch Helpers + ********************************************* */ + +// dispatch for path prefixs +function prepath(pre) { + + function handle(req, res, next) { + pre.uid = pre.uid || guid(); + req.ppi = req.ppi || {}; + + var path = req.gs.path, + key, fn, i = req.ppi[pre.uid] || 0; + + while (i < pre.length) { + key = pre[i][0]; + fn = pre[i++][1]; + if (path.indexOf(key) === 0) { + return fn(req, res, () => { + req.ppi[pre.uid] = i; + handle(req, res, next); + }); + } + } + + next(); + } + + return handle; +} + +// dispatch full fixed paths +function fullpath(map) { + return (req, res, next) => { + var fn = map[req.gs.path]; + if (fn) fn(req, res, next); + else next(); + }; +} + +// dispatch full paths based on a prefix and a function map +function fixedmap(prefix, map) { + return (req, res, next) => { + var path = req.gs.path; + if (path.indexOf(prefix) != 0) return next(); + var fn = map[path.substring(prefix.length)]; + if (fn) fn(req, res, next); + else next(); + }; +} + +// HTTP 302 redirect +function redir(path) { + return (req, res, next) => redirect(res, path); +} + +// mangle request path +function remap(path) { + return (req, res, next) => { + req.url = req.gs.path = path; + next(); + } +} + +// load module and call returned function with helper object +function loadModule(dir) { + const modjs = dir + "/init.js"; + lastmod(modjs) && require(modjs)({ + api: api, + const: { + script: script, + moddir: dir, + rootdir: currentDir, + args: args + }, + util: { + log: log, + time: time, + guid: guid, + mkdirs: mkdirs, + lastmod: lastmod, + obj2string: obj2string, + string2obj: string2obj, + getCookieValue: getCookieValue + }, + db: { + api: db, + level: level + }, + path: { + any: arg => { modPaths.push(arg) }, + pre: arg => { modPaths.push(prepath(arg)) }, + map: arg => { modPaths.push(fixedmap(arg)) }, + full: arg => { modPaths.push(fullpath(arg)) }, + static: arg => { modPaths.push(serveStatic(arg)) }, + code: (endpoint, path) => { + if (clearJS) { + code[endpoint] = fs.readFileSync(path); + } else { + code[endpoint] = uglify.minify(path).code; + } + } + }, + handler: { + static: serveStatic, + redirect: redirect, + reply404: reply404, + reply: quickReply + } + }); +} + +/* ********************************************* + * Start it up + ********************************************* */ + +// load modules +lastmod("mod") && fs.readdirSync(currentDir + "/mod").forEach(dir => { + loadModule(currentDir + "/mod/" + dir); +}); + +// create cache dir if missing +lastmod("cache") || mkdirs(["cache"]); + +// precache responses +prepareScripts(); + +// create web handler chain +var handler = connect().use(setup); + +// add path handlers registered by modules +modPaths.forEach(fn => { + handler = handler.use(fn); +}); + +// add the rest of the handler chain +handler.use(fullpath({ + "/meta/index.html" : redir("/meta/"), + "/kiri/index.html" : redir("/kiri/"), + "/kiri)" : redir("/kiri/"), + "/meta" : remap("/meta/index.html"), + "/meta/" : remap("/meta/index.html"), + "/kiri" : remap("/kiri/index.html"), + "/kiri/" : remap("/kiri/index.html") + })) + .use(prepath([ + [ "/space", redir("/meta/")], + [ "/api/", api.rateLimit ], + [ "/data/", handleData ], + [ "/code/", handleCode ], + [ "/js/", handleJS ] + ])) + .use(fixedmap("/api/", api)) + .use(rewriteHTML) + .use(compression) + .use(serveStatic(currentDir + "/web/")) + .listen(port); + +console.log("------------------------------------------"); +console.log(new Date()); +console.log("port="+port+" debug="+clearJS+" nolocal="+noLocal); +console.log("version="+ver.VERSION); diff --git a/license.md b/license.md new file mode 100644 index 00000000..27f21952 --- /dev/null +++ b/license.md @@ -0,0 +1,8 @@ +Copyright (C) Stewart Allen All Rights Reserved + +Unauthorized copying, modification and re-distribution are +strictly prohibited without prior written consent. + +This public repository exists to permit inspection by parties interested +in obtaining a licensing to the code or individual persons wishing to run +host-local instances for personal use only. diff --git a/notes.md b/notes.md new file mode 100644 index 00000000..a2c5968b --- /dev/null +++ b/notes.md @@ -0,0 +1,112 @@ +--( global )-- + +* prevent text selection of non-input +* widget general add-ons (fdm supports, cam tabs) +* bail on decimation if it's proving ineffective +* improve decimation speed by avoiding in/out of Point +* dismissable transient message/alert +* modal non-alert-based dialog +* modal spinner +* ability to cancel slice operations! +* server-side processing +* saving workspace doesn't preserve widget positions +* move more kiri code (like printing) into modules like serial +* include device profile name in exported gcode comments +* when deleting last selected part, turn off bottom param editor +* frame api * https://plus.google.com/u/0/+JakobFlierl/posts/hn6eirr6fXC +* refactor / simplify POLY.expand (put onus on collector) +* add simple solid (tube-like) rendering in place of lines +* extend mesh object to store raw + annotations (rot,scale,pos), share raw data w/ dups, encode/decode +* cloned objects share same slices data unless rotated +* remember object's original position/orientation for reset/multi-object import alignment +* gcode import break up "layers" on z move with no x/y move + +--( onshape )-- + +* popup warning when detect 3rd party storage blocked (chrome) +* watch for changed part version to prompt re-import +* re-used disk cached version of parts if not changed +* remap mouse/kbd to match onshape when running inside? +* assembly import + +--( cam )-- + +* ease-in and ease-out especially on tab cut-out start/stop +* import options: unify bodies. +* milling order option: by operation or by part +* store tab and camshell polys in widget.topo to minimize z on edge moves +* trimming linear finishing to tabs +* improve 'clockwise' setting to take into account spindle direction, etc +* linear finishing cutting out tabs +* linear finishing going back to z top too often +* fix ease down and re-enable +* warn when part > stock or cuts go outside bed +* uncheck origin center for carvey +* option to skip milling holes that would be drilled +* sender speed control slider (0%-200%) ? +* add M03 tool feedrate support (https://forum.grid.space/index.php?p=/discussion/14/s-parameter#latest) +* fails in pancaking (clone) when there are no sliced layers (like z bottom too high) +* crossing open space check point is outside camshell before returning max z +* compensate for leave-stock in outside roughing (w/ tabs) +* fix zooming, workspace thickness for larger workspaces +* only show toolchange alert/pause after the first M6 +* raise z by leave-stock in roughing? if so, see next +* if (raise z) above, add clear-flats to finishing +* revisit tabs - just cut polys instead +* try chunking topo until smaller blocks for processing +* linear x/y scan overflow (y) w/ topo model +* linear x/y not obeying inset from pocket only +* check normals for downward facing facets. mark top for slice skirt/pancake +* detect holes thru bottom and cutout outline vs mill out entire void + +--( fdm )-- + +* implement gyroid infill * https://en.wikipedia.org/wiki/Gyroid +* fan / layer control * update forum +* infill rendering as moves instead of extrusions (firefox?) +* run line through center of series of short fills (thin fill optimization) +* add rafts, thin wall detection, manual supports +* add skirt to raft option as a simpler way to do rafts +* fill before shell option (request) ? +* separate shell speed control +* wipe on infill should follow the closest enclosing shell poly +* TAZ from https://code.alephobjects.com/diffusion/P/browse/master/cura/TAZ_flexy_dually_v2/PLA-PVA-support_medium-quality_TAZ_FlexyDually-v2_0.6noz_cura.ini +* add control of shortest line/fill line before culling +* add retraction distance/speed to gcode profiles +* add min layer time (slowdown or cool-off wait) +* tops should print inside/out (add odds w/ poly2poly ...) +* extend support to interior spaces when 0% infill? +* fix multiple part layout export offset (resend position @ print time) +* first/last/outline/finish speed settings +* check for support / brim intersections on first layer +* bottom layer of a bridge: underextrude/stretch? +* dual extruder support + +--( laser )-- + +* overcuts, radii for drag knives +* sla :: svg modified from http://garyhodgson.github.io/slic3rsvgviewer/?file=examples/belt_pulley3.svg + +--( reference )-- + +* shader examples to enable object-clipping +* ----- +* http://jsfiddle.net/LK84y/9/ +* http://www.html5rocks.com/en/tutorials/webgl/shaders/ + +* more reading +* ----- +* http://lcamtuf.coredump.cx/gcnc/full/ +* http://www.tcs.fudan.edu.cn/rudolf/Courses/Algorithms/Alg_cs_07w/Webprojects/Zhaobo_hull/ +* https://en.wikipedia.org/wiki/Graham_scan +* http://www.cambam.info/doc/0.9.7/cam/Pocket.aspx +* http://hackaday.com/2016/01/22/pack-your-plywood-cuts-with-genetic-algortihms/ +* http://wiki.imal.org/howto/cnc-milling-introduction-cutting-tools +* http://www.twak.co.uk/2011/01/degeneracy-in-weighted-straight.html + +* grbl & universal json serial port sender +* ----- +* https://github.com/johnlauer/serial-port-json-server +* https://github.com/grbl/grbl/wiki/Interfacing-with-Grbl +* https://github.com/synthetos/TinyG/wiki/TinyG-Command-Line +* https://github.com/synthetos/TinyG/wiki/Tinyg-Communications-Programming diff --git a/package.json b/package.json new file mode 100644 index 00000000..38ef820b --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "gridspace", + "version": "1.0.0", + "description": "gridspace 3d slice & modeling tools", + "author": "Stewart Allen ", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/stewartoallen/gridspace.git" + }, + "keywords": ["gridspace", "kiri", "kirimoto", "3d", "slicer", "CAM", "laser", "gcode"], + "scripts": { + "start": "node js/web-server debug", + "start-web": "node js/web-server nolocal" + }, + "dependencies": { + "connect": "3.3.x", + "compression": "1.4.x", + "serve-static": "1.10.x", + "n3d-threejs": "72.0.x", + "tween.js": "0.14.x", + "uglify-js": "2.7.x", + "leveldown": "latest", + "levelup": "latest", + "validator": "4.2.x", + "pixi.js": "3.0.x", + "moment": "2.11.x", + "request": "2.67.x", + "express-useragent": "0.2.x" + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 00000000..08ab2db8 --- /dev/null +++ b/readme.md @@ -0,0 +1,21 @@ +# KiriMoto + +`KiriMoto` is a unique multi-modal, extensible slicer, gcode generation framework + +## Getting Started + +``` +npm update +npm start +``` + +to start a local testing instance of KiriMoto on port 8080 + +## Other Start Options + +``` +npm run-script start-web +``` +serves code as obfuscated, compressed bundles. this is the mode used to run on a public +web site, so you can't use "localhost" to test. to accomodate this, alias "debug" to 127.0.0.1 +then access KiriMoto on http://debug:8080 diff --git a/web/kiri/favicon-mobile.png b/web/kiri/favicon-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..f8acc4c9b9c13df4976d2b37bc88f228c62873a6 GIT binary patch literal 6142 zcmZ`+1yq#Xx*kC30ThPLQE3>OfuTDG1SF)T9O>>xx^$$69!cpADHRYd{%D$!_D8(l9ZDqCI zJ-YjP%mvZWA@`HDLGh8aMBvz$%y}Vqzkp?BV!E+QO6G7@d*?I+PGN_`zU$fh!NcCy z^BITOGH!OPti2zw0S{etnx){4ZnE7hZq4R^2onVhY@zgbs|s`wi1*K`259UN*i*}X z{Kxm-*EM$@fRRQ388duA!=AVee@uap_5KUAUX z?kw3O+{fTobvGM<@eP4qDt6$h#GPm?2Qr{e@>OE+uLDzeHLR_Wm?H&nEX-}X;yUq1 zsx!jAl5F?HfTxrWFe;&79Xy^M3-FM_3u!%jq8kt%?@k?}9{x_xX{NQ&?+6fn2|Ekr zBf}GrxmSYClSdjKDprk4(gpItmL?-^1W*b%V+%YH@5lC+gLzR zXY%yypnmlCdbm$yfg_~aY#yN2yFgjee1hkcemH>zNw)x1BwvoG=0v?jB!1=h2s2rK$ z!cGfzYL0l^UgDK7f}U#rt#lI&f+EV%*p@_^ZVgMy8bfZNdS9kRc1`z`;SPxd*2%-h zyR!)lJ@98#E>IK{j$;>k_>te0quFGRV2)-EG{@&ec*>~NGq`;GEAfm>0xvfK(k;3S zS&8~JdS(*_kj=1A`uR|StdX3MKsbW8$FzsSGJ{={Q*hEFyW9-t0> zMWIsk&{>BVL0bC8BhdpXBzqx42`)$O_UEta9k94Dv}9c-U#80CmuSD^V0l9^z@mn; zM!UwbCdrhdl{Nel66P#wE@n>mo$otW5O$|PC-8>yTI2fuHPNN_`Nkc5Y)))1Y>iM; z>^1CcY%6S{Fg;mGS*`9n-Pqkq-Ns>pa!?i!3tm2cK7IbE+++l6DSH`sJaM34uW&-U zO1C(orpLpQYRPaZV5zPLr{@qX2aW_|foZ^N;4}pvAqvfx>vD0$Ak5cMB?Ob=TSZ5O z2a2SzB5@>fyfLb=MsbRS1~Y@U4HC7xHju4>DzA5rLP zbjw3^0j-1f)dw7gYNi9qFQUc}r9q{45i|(4Erl&|%p`il`nkY_K#jmBfixRio6_3U zTK?KkwFcJa{W1O1{htO?295{5461*I4Hgd=d?xLOaE*X2LA9W#6glkoK{TZ`#k)&-rNl*yv*I;uOe^n~2*6 z6el*LdqJ&4nNoG2MR7LBhVk`o67>r9dy4tq(sTwWub4QDae? zCL}Y6Pf$diALe}0){^Rr@+~|c!o7&>2MW`%@=$Q6Q$C@TP!}8I8{Fsi)j!bJD1T9I zHxWB=Z=zvhROE$7jfkCt+4S~|o1?K~+Ro69*No?E;ZF9>;SL^s1pOt$0DX=)C$yF! zi+&?J9_0n~gju2HQAyCG7L~SY6IzpXQ%w`^)|1v(^8xb%b2%;LZSM1J5*DNeNrj9n zbSa{4B8~poT2#Y@Ia=+TW~ye=zE08seF3_VJTW-d zI^HK(B*h0l$2TObC*>wwy}Na1m$;Rj`yM^{+`TX&0{l{-(bQ`7)>KiccHzj}$hR*? zUud$N9bZk|?_9&^UsnuOQ1$RxhFT_A=BDQiy;zrPR`*dnlNe0&L>&wCh!;SgiD1CB z>~7)%;4=0Xk&fL`133wevAi)kaW!!qalQ;c=#6RGC^l#esG9CGQGb6OqJ^)%xr*<) z41GUq-Du-Lr#7ZCq`CSi;}L^;=0>ZlJu-jw%V!3Tp8mc%=_kU+Sz|FmgY^gXr(0NC zR04Mdp)^;s<4sI+QrTPS=T{_0Bom1_+~@D!8`uoRizSc! zWBgZ6u2B_Prf;sC%ojT<>9y|F!cfTp-}l<;usYHr46Mf<1utbpW6Us zaJe@%Vb!EYtj?K4o>a`ZkQBUki!;^BoG*oX69Cp>tnpd>%-+KydxZiNzkoyDb+3uHnv4D$=j8rP^ z?O|=^Xh583Y-V)Vo!G%L8#2qEKIb{#=xP~rhXmpOwmfQ|Jh?zrjN;mG|* z$S;{$Q)C-Mcu}~iJY6I&sWH&}yX{tvB8gNgYGSz1lRh%)Q-oKI*o4@q?v@Ik28P!t!Z`}|7!BwC;ooS z?ZYEss$pniHsBAr?zqNTk^kP;l{7lImFtqLCS4x5cd>k86Y$8==FzLqHkFE46t#;b z?0|(aEExi&+7HaHU!F|}PBQu8UZ~feL&8Bj2G{rNat0>`s6)i<0hgzM#YF2jy8Svz z>G5+04+?UHLx9BBCc%1cZ$hsx1p7@MX11Ff`EmgPl|&)Kb15lnNG`5i`(#Td?2+uF zV7uRRRU6&oJz$QOXx_3!9)^Aq+~BGkz#jAaRO>0aq1%%ofs2ZfI{-jL{%2qTGEvk3 z05&(OnAQ$yU!*^$@M+WDCcuaBe4EgArj@DabAI@)+xGW$3>IJt}aNP_=Dh~Lit zh#_F+zaSp=l3+tkZDx6AHydVQULjsSuoNLPGqZ%7wXL|0g3`a}w<}5Ta}N&}@!Qj& zw>Pi1Ag{BV9fV&@Obo&&01*)2xkd1}`#O18`tUfpKlt0o|JqToakp|qxOgC(otXdF zwS4C6=^+UQ|8ey1=kGXed=UTfor^*GBl*I2b7)9BrK3Z$pz3;FI{v%YP~U6X!pOhW{W6{FC?(#XpFD;t253CO?uD@EA$4J3ZMEs|6fWc7Toc1?Dj z=9)o|j)fU#RYRQa+l``u$yW{Rgm2WVv`hPK$$y@O;;s4mi!#TJN2)<&I^S(Yn4GJi5;Q z^tFJOF>7oupd_p9w<8iY?&zpd%XilCQ%zY-THg*G8!cx%S^Le}H}h?9qhnom<9?mE z7$2hLbR7ztx|$rFHawh8PZl_#$8ixb?kve%HP*058h(mnfNVj`5OPwHc54Bxhw^_+ z6{PLp(#wi)lr`ooR`%8{2sU0b@`EQ1o>PTez1w1!>B|~@Hlqh~%nq2HI%ylq+%Tuh z>Vyw*w64$q=;}7CzXioC6O|><#!t7_GL-TKTJKWS=xL5#8$ODuLDY$I{a{j>jpQTL z_R_ay!mL&zvidS=hWB?QrZ2=r*PRc`*vka<9OlDmY9xMUi`FnJZ<{IC&k^sR=MxdF z4g}!1KX;OPQv{Wo3+#BFnX8dCmu(T0&_Ktt7;#B-!Sb${RMS`j7aE*Rdq@VH`{W^_ zY5YN0E_^~>=Bp>Sd33yzg|K{63E6@!rODHq2QS{;d%E-k8XCg%wWF>#^HkM|Wa((Q z`KCem+%n5zp!&0}exk|akh4aCI^wRTL;2a94)$_?7+?dTlUa3uaVRVjBI%=3yzAS` zufY90nL3mvoD)Lbv*It2NBh>6L&g!-SutYlT<%l#f>V6~ zPbZy4g+saw(-Lq#8nk3jrhiq7FE%pEnV&5)c86qVNn@dR#|m7BynG~BWa@upk=H9M zXBKW;xW0btO!2~l+cHop$QkdPX!}&L68mZ;>V12NE)m)|{pVqA1dGN%14n)}w1Ul& zi2Klhg1JgI=_^k+OQC3c`u4K!0r+h-*UD9%#cIdsbu}n+t$=U5qk;g&#gv};3vz}*hO6iG1I#NhqO$cTrfXmuGUfV5h^dhzV4lS;VPa22OiMBDX)v=n#5_Pe(#bb3hxUrkE*kuV9F9j^0E$YmLFEI*1sz=9P0 zN1hcLI*5+@S3>DVuYU#wpJoNdS5Bg?61wI1H@U4AKFzi_M|^uX!)W}5B|vCib!eTz zesC_6iEUzUx=}L6(wx$rlW-(^)Ch5~95VQekSk|KNc#P{eqn>VjPAh4x^0VRW@gHj zrWwD*?Bj5V%S)Gn9!w899!laaNr_g}uoeJZ)BL}`@dv1;u9&z!Q&KPc$O|ATl8{(` zBDIwsHJMVLq(Ppw-uS7P8Nn@5UId@FJr>I9%lx*zF(}`P^IO14^37g4!Tb__QpKV0 zP1QrSff22ajADXK5{>&PIjCBtiuXGLK~YY>9%9JeqT33n{s$Gm4|bnHvL}D8u{jCW zQ(0KB5V^Yhh+i}hY0aaMuni_SCKw-1%Z(sxbQnhoAtn|3RBvLRSa}-3#k($arjxx% zxGZE{-*VNHPk_Y45JYfsBL=pFm&-(pV3da^2!5gVSXPX9z06boDxG0i)#~QuF1a^8 z*9Mv?6uCeJf7(c!Z#+5Jhv(;PVDRz1bvj+$y$M(6y{rxNjZ}|nRM5zewcnNxKfrp0 zNBT5XAi#zwZ>w}|`P~{+6=LB#JKI&>3Od&lbtJj-tN3}wZ|T|?>zyOQ(`xtxgol2O zoL!ftIptl4ils5u$k;Ryr;Nq>)YO7@UDc6141?Vw`ui(nugMW%St4?M*zn7-*w#%L z@>;nCz?PlSi3UNpTBduP*#g8H@cMllh?A{sQHPh5%|g!-8q_nAS9NO?L-ZK1%iq>F z-XV)cU%_7n)Vp9tVBHX3XO<>mAD-KMR}#)QcO%(iKG~ju=HCqj*>|Z9cd2T1six&1 zSqs?#`O0DzC_70NV*PI}UrwaY)rH{f<9S>KWXyOn=26>vsf5+sKgH;tDrh}4!-M0p z(QSe@*NKv8XMch&#T&X+!=0(%nGVNio-HZ$Njl+*Wz(UiY{U$t<0e}8oFz0*FnW4E zg<&ILq7MR(%`Py0*)MXVb0D6!EOE{)g_0#3Xy935;|AK2_d~e{d;sKoIl^+7TpwT6 zae6dtG-yItzh!wg)OzVdicK#Ceu~d^zbubT5ffDW`dRbA2QeKeLXe+{k2K|_kHFol z8d~@T78G&N+@oqm%#@$ylxN@;UGppGHObOFwpDdlUIfPZO`25-OD~~WXWaX!ytsDJ z`EK$TGR3-ani#aHKYS((*;u}W4vw_CZHU%zWg+?NLnnWxaP6;lzpJGxt6FDL7jm1n z9S~yvVpwd&PYKzwd0a7<9QU*&+!Ujx-ijfPA7Z-pd07h<`UM5XxszPjwk@U+ee2Wio`Csr)y$IIxA))) zln3A^Szew+wkQ4>RbCsz4u$7uS6Fo{@$Jun%{_st54m@Db7ljLkXntx*{MQ3O1LO( zd=r*(!6n7^MSe9m0G`QS;%=a1jqdSuv^;5k<&GJ*`Kqe?dr2iP*LbywR^aUdH`^7v z8{E%9M4By}Io8U6ulEPAl*B<|mO7hg<(XzT#cv(_@Z#+sU#EKfTEa((mQWVhxQ6BCGlQCgwe1n z-UhmRqeK3fc%|hday3Bg?k#1L+nqR(ehUe1wb=WS4#2OH?~C}!xU z<-(F^5l2vo9e~(2;Pr{4Pl(#hovN}zz!m+Z(jEqo;HIBKu0V->fnbOJ|Rww z&t7GrUE#F4INz;vkc}eQVaSv5G=awvYhz#zuJz@P!dKp~(AL>x#lFaYIf0UmEr|4$Bvxdp1%5=fJR v|D)+A*$puL15J|?b|5`4caMhOX!?Vt1ybq%z*N-0$iT4wFN6m1Nf848x5iYg literal 0 HcmV?d00001 diff --git a/web/kiri/filter/CAM/Any.Generic.Grbl b/web/kiri/filter/CAM/Any.Generic.Grbl new file mode 100644 index 00000000..439356cf --- /dev/null +++ b/web/kiri/filter/CAM/Any.Generic.Grbl @@ -0,0 +1,22 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 400, + "bed_depth": 400 + } +} diff --git a/web/kiri/filter/CAM/Any.Generic.LinuxCNC b/web/kiri/filter/CAM/Any.Generic.LinuxCNC new file mode 100644 index 00000000..7ff81803 --- /dev/null +++ b/web/kiri/filter/CAM/Any.Generic.LinuxCNC @@ -0,0 +1,24 @@ +{ + "file-ext": "ngc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)", + "M03 S20000 ; spindle on" + ], + "post":[ + "M05 ; spindle off", + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 400, + "bed_depth": 400 + } +} diff --git a/web/kiri/filter/CAM/Any.Generic.TinyG b/web/kiri/filter/CAM/Any.Generic.TinyG new file mode 120000 index 00000000..078a9115 --- /dev/null +++ b/web/kiri/filter/CAM/Any.Generic.TinyG @@ -0,0 +1 @@ +Any.Generic.Grbl \ No newline at end of file diff --git a/web/kiri/filter/CAM/Carbide3D.Nomad.883Pro b/web/kiri/filter/CAM/Carbide3D.Nomad.883Pro new file mode 100644 index 00000000..4bd60a1a --- /dev/null +++ b/web/kiri/filter/CAM/Carbide3D.Nomad.883Pro @@ -0,0 +1,22 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 200, + "bed_depth": 200 + } +} diff --git a/web/kiri/filter/CAM/Carbide3D.Shapeoko.3 b/web/kiri/filter/CAM/Carbide3D.Shapeoko.3 new file mode 100644 index 00000000..439356cf --- /dev/null +++ b/web/kiri/filter/CAM/Carbide3D.Shapeoko.3 @@ -0,0 +1,22 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 400, + "bed_depth": 400 + } +} diff --git a/web/kiri/filter/CAM/Carbide3D.Shapeoko.XL b/web/kiri/filter/CAM/Carbide3D.Shapeoko.XL new file mode 100644 index 00000000..94b5cf86 --- /dev/null +++ b/web/kiri/filter/CAM/Carbide3D.Shapeoko.XL @@ -0,0 +1,22 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 830, + "bed_depth": 400 + } +} diff --git a/web/kiri/filter/CAM/Carbide3D.Shapeoko.XXL b/web/kiri/filter/CAM/Carbide3D.Shapeoko.XXL new file mode 100644 index 00000000..5c5ef0d7 --- /dev/null +++ b/web/kiri/filter/CAM/Carbide3D.Shapeoko.XXL @@ -0,0 +1,22 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 830, + "bed_depth": 830 + } +} diff --git a/web/kiri/filter/CAM/Inventables.Carvey.Any b/web/kiri/filter/CAM/Inventables.Carvey.Any new file mode 100644 index 00000000..b8ee3925 --- /dev/null +++ b/web/kiri/filter/CAM/Inventables.Carvey.Any @@ -0,0 +1,24 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "origin_center": false, + "spindle_max": 12000, + "bed_width": 290, + "bed_depth": 200 + } +} diff --git a/web/kiri/filter/CAM/Inventables.XCarve.1000mm b/web/kiri/filter/CAM/Inventables.XCarve.1000mm new file mode 100644 index 00000000..db059b0d --- /dev/null +++ b/web/kiri/filter/CAM/Inventables.XCarve.1000mm @@ -0,0 +1,22 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 1000, + "bed_depth": 1000 + } +} diff --git a/web/kiri/filter/CAM/Inventables.XCarve.500mm b/web/kiri/filter/CAM/Inventables.XCarve.500mm new file mode 100644 index 00000000..e84a19e3 --- /dev/null +++ b/web/kiri/filter/CAM/Inventables.XCarve.500mm @@ -0,0 +1,22 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 500, + "bed_depth": 500 + } +} diff --git a/web/kiri/filter/CAM/Sienci.Mill.One b/web/kiri/filter/CAM/Sienci.Mill.One new file mode 100644 index 00000000..e80f5026 --- /dev/null +++ b/web/kiri/filter/CAM/Sienci.Mill.One @@ -0,0 +1,22 @@ +{ + "file-ext": "nc", + "token-space": " ", + "strip-comments": true, + "pre":[ + "G21 ; set units to MM (required)", + "G90 ; absolute position mode (required)" + ], + "post":[ + "M30 ; program end" + ], + "tool-change":[ + "M6 T{tool} ; change tool to '{tool_name}'" + ], + "dwell":[ + "G4 P{time} ; dwell for {time}ms" + ], + "settings": { + "bed_width": 235, + "bed_depth": 185 + } +} diff --git a/web/kiri/filter/FDM/Any.Generic.Marlin b/web/kiri/filter/FDM/Any.Generic.Marlin new file mode 100644 index 00000000..c5ce6ce3 --- /dev/null +++ b/web/kiri/filter/FDM/Any.Generic.Marlin @@ -0,0 +1,34 @@ +{ + "pre":[ + "M104 S{temp} T0 ; set extruder temperature", + "M140 S{bed_temp} T0 ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 ; home axes", + "G29 ; level bed", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 X0 Y0 Z0.25 F180 ; move xy to 0,0 and z 0.25mm over bed", + "G92 E0 ; zero the extruded", + "M190 S{bed_temp} T0 ; wait for bed to reach target temp", + "M109 S{temp} T0 ; wait for extruder to reach target temp", + "G1 E15 F200 ; purge 15mm from extruder", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T0 ; turn off right extruder", + "M104 S0 T1 ; turn off left extruder", + "M140 S0 T0 ; turn off bed", + "G1 Z{z_max} F1200 ; drop bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false + } +} diff --git a/web/kiri/filter/FDM/Any.Generic.Sailfish b/web/kiri/filter/FDM/Any.Generic.Sailfish new file mode 120000 index 00000000..40bf98a6 --- /dev/null +++ b/web/kiri/filter/FDM/Any.Generic.Sailfish @@ -0,0 +1 @@ +Any.Generic.Marlin \ No newline at end of file diff --git a/web/kiri/filter/FDM/Atom.v2 b/web/kiri/filter/FDM/Atom.v2 new file mode 100644 index 00000000..dfc61222 --- /dev/null +++ b/web/kiri/filter/FDM/Atom.v2 @@ -0,0 +1,26 @@ +{ + "pre":[ + "M104 S{temp} ; set extruder temperature", + "M140 S{bed_temp} ; set bed temperature", + "G28 ; Home", + "G1 F5000 Z50 ; lift nozzle", + "G0 X0 Y-100 Z50 ; parking", + "G0 Z20 ; parking", + "G92 E0 ; zero the extruded length", + "G1 F200 E3 ; extrude 3mm of feed stock" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 ; turn off right extruder", + "M140 S0 ; turn off bed" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": true, + "bed_width": 210, + "bed_depth": 210, + "build_height": 320 + } +} diff --git a/web/kiri/filter/FDM/DeltaMaker.2T b/web/kiri/filter/FDM/DeltaMaker.2T new file mode 100644 index 00000000..bd299b60 --- /dev/null +++ b/web/kiri/filter/FDM/DeltaMaker.2T @@ -0,0 +1,30 @@ +{ + "pre":[ + "M104 S{temp} T0 ; set extruder temperature", + "G21 ; Make sure we're in metric mode", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "G28 ; home axes", + "G92 E0 ; zero the extruded", + "M109 S{temp} T0 ; wait for extruder to reach target temp", + "G1 F200 E3.5 ; set feed speed, prime nozzle", + "G92 E0 ; zero the extruded", + "G1 Z30 ; position near the bed before first position" + ], + "post":[ + "G28 ; home axes", + "M104 S0 T0 ; turn off right extruder", + "M140 S0 T0 ; turn off bed", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "nozzle_size": 0.5, + "origin_center": true, + "bed_width": 240, + "bed_depth": 240, + "build_height": 480 + } +} diff --git a/web/kiri/filter/FDM/DeltaMaker.Original b/web/kiri/filter/FDM/DeltaMaker.Original new file mode 100644 index 00000000..2407cdd7 --- /dev/null +++ b/web/kiri/filter/FDM/DeltaMaker.Original @@ -0,0 +1,30 @@ +{ + "pre":[ + "M104 S{temp} T0 ; set extruder temperature", + "G21 ; Make sure we're in metric mode", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "G28 ; home axes", + "G92 E0 ; zero the extruded", + "M109 S{temp} T0 ; wait for extruder to reach target temp", + "G1 F200 E3.5 ; set feed speed, prime nozzle", + "G92 E0 ; zero the extruded", + "G1 Z30 ; position near the bed before first position" + ], + "post":[ + "G28 ; home axes", + "M104 S0 T0 ; turn off right extruder", + "M140 S0 T0 ; turn off bed", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "nozzle_size": 0.35, + "origin_center": true, + "bed_width": 240, + "bed_depth": 240, + "build_height": 260 + } +} diff --git a/web/kiri/filter/FDM/Leapfrog.CreatrHS b/web/kiri/filter/FDM/Leapfrog.CreatrHS new file mode 100644 index 00000000..99343606 --- /dev/null +++ b/web/kiri/filter/FDM/Leapfrog.CreatrHS @@ -0,0 +1,36 @@ +{ + "pre":[ + "M104 S{temp} T0 ; set extruder temperature", + "M140 S{bed_temp} T0 ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 X0 Y0 ; home XY axes", + "G28 Z0 ; home Z axis", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 Z0.25 F180 ; move z to 0.25mm over bed", + "G92 E0 ; zero the extruded", + "M109 S{temp} T0 ; wait for extruder to reach target temp", + "G1 E15 F300 ; purge 15mm from extruder", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T0 ; turn off right extruder", + "M104 S0 T1 ; turn off left extruder", + "M140 S0 T0 ; turn off bed", + "G1 Z{z_max} F1200 ; drop bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "bed_width": 270, + "bed_depth": 280, + "build_height": 180 + } +} diff --git a/web/kiri/filter/FDM/MakerGear.M2 b/web/kiri/filter/FDM/MakerGear.M2 new file mode 100644 index 00000000..0ca191b6 --- /dev/null +++ b/web/kiri/filter/FDM/MakerGear.M2 @@ -0,0 +1,38 @@ +{ + "pre":[ + "M108 S255 ; set extruder speed", + "M104 S{temp} T0 ; set extruder temperature", + "M140 S{bed_temp} T0 ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 XY ; home XY axes", + "G28 Z ; home Z axis", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 Z0.25 F180 ; move z to 0.25mm over bed", + "G92 E0 ; zero the extruded", + "M109 S{temp} T0 ; wait for extruder to reach target temp", + "G1 E15 F300 ; purge 15mm from extruder", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "post":[ + "M108 S0 ; set extruder speed", + "M107 ; turn off filament cooling fan", + "M104 S0 T0 ; turn off right extruder", + "M104 S0 T1 ; turn off left extruder", + "M140 S0 T0 ; turn off bed", + "G1 Z200 F1200 ; drop bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "bed_width": 200, + "bed_depth": 250, + "build_height": 200 + } +} diff --git a/web/kiri/filter/FDM/Makerbot.Replicator2 b/web/kiri/filter/FDM/Makerbot.Replicator2 new file mode 100644 index 00000000..2dba559e --- /dev/null +++ b/web/kiri/filter/FDM/Makerbot.Replicator2 @@ -0,0 +1,44 @@ +{ + "pre":[ + "M73 P0 ; set progress to 0%", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M104 S{temp} T0 ; set extruder temperature for T0 (tool 0)", + "M127 ; close extruder valve (if it has one)", + "G162 X Y F3000 ; move X,Y axis to maximum position at speed 3000 (top/right of table)", + "G161 Z F1200 ; move Z axis to minimum position as speed 1200 (bottom)", + "G92 Z-5 ; set Z axis value to -5 (no move)", + "G1 Z0 ; move Z axis to 0 (lifting it 5mm off endstop)", + "G161 Z F100 ; move Z axis to minimum position at speed 100", + "M132 X Y Z A B ; load EEPROM home offsets for all axis", + "G1 X{left} Y{bottom+10} Z30 F9000 ; move to wait position off table", + "G130 X20 Y20 Z20 A20 B20 ; lower stepper Vrefs to give more juice to the extruder heater", + "M133 T0 ; wait for extruder T0 temperature to stabilize", + "G130 X127 Y127 Z40 A127 B127 ; default stepper Vrefs now that extruder is hot", + "G92 A0 ; set extruder position value to 0 (no move)", + "G1 Z0.4 ; move nozzle just above the bed height", + "G1 E25 F300 ; purge extruder for 25mm at speed 300", + "G1 X{left+10} Y{bottom+10} Z0.15 F1200 ; slow wipe", + "G1 X{left+15} Y{bottom+15} Z0.5 F1200 ; lift", + "G92 A0 ; set extruder position value to 0 (no move)", + "M135 T0 ; set toolhead 0" + ], + "post":[ + "M127 ; stop filament cooling fan", + "M73 P100 ; set build progress to 100%", + "G1 Z150 F1000 ; send Z axis to bottom (engaging end stop)", + "G162 X Y F3000 ; move X,Y axis to maximum position at speed 3000 (top/right of table)", + "M18 ; disable all stepper motors", + "M72 P1 ; play song 1 (tada)" + ], + "cmd":{ + "fan_power": "M126 S{fan_speed}", + "progress": "M73 P{progress}" + }, + "settings":{ + "origin_center": true, + "bed_width": 300, + "bed_depth": 175, + "build_height": 150 + } +} diff --git a/web/kiri/filter/FDM/TypeA.Series1 b/web/kiri/filter/FDM/TypeA.Series1 new file mode 100644 index 00000000..846b1c40 --- /dev/null +++ b/web/kiri/filter/FDM/TypeA.Series1 @@ -0,0 +1,37 @@ +{ + "pre":[ + "G1 Z15.0 F12000 ; move the platform down 15mm", + "G1 X150 Y5 F12000 ; center extruder on X", + "M107 ; turn off filament cooling fan", + "M83 ; use relative positioning for extruder", + "M104 S{temp} ; set extruder temperature", + "G21 ; use metric values", + "G90 ; use absolute positioning mode", + "G28 ; move to endstops", + "G29 ; perform auto-levelling", + "M109 S{temp} ; wait for extruder to reach target temp", + "G1 X150 Y5 Z0.3 ; move the platform to purge extrusion", + "G92 E0 ; zero the extruded length", + "G1 F200 X250 E30 ; extrude 30mm of feed stock", + "G92 E0 ; zero the extruded length again", + "G1 X150 Y150 Z25 F12000 ; center head for print", + "G1 F12000 ; set head seek rate" + ], + "post":[ + "G1 E-1 F300 ; retract the filament to release pressure", + "G1 Z{z_max} E-5 F1200 ; drop bed, retract more filament", + "G28 X0 Y0 ; move X/Y to min endstops", + "M107 ; turn off filament cooling fan", + "M104 S0 ; turn off extruder", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "bed_width": 305, + "bed_depth": 305, + "build_height": 305 + } +} diff --git a/web/kiri/filter/FDM/TypeA.Series1Pro b/web/kiri/filter/FDM/TypeA.Series1Pro new file mode 100644 index 00000000..c282a151 --- /dev/null +++ b/web/kiri/filter/FDM/TypeA.Series1Pro @@ -0,0 +1,40 @@ +{ + "pre":[ + "G1 Z15.0 F12000 ; move the platform down 15mm", + "G1 X150 Y5 F12000 ; center extruder on X", + "M107 ; turn off filament cooling fan", + "M83 ; use relative positioning for extruder", + "M104 S{temp} ; set extruder temperature", + "M140 S{bed_temp} ; set bed temperature", + "G21 ; use metric values", + "G90 ; use absolute positioning mode", + "G28 ; move to endstops", + "G29 ; perform auto-levelling", + "M109 S{temp} ; wait for extruder to reach target temp", + "M190 S{bed_temp} ; wait for bed to reach target temp", + "G1 X150 Y5 Z0.3 ; move the platform to purge extrusion", + "G92 E0 ; zero the extruded length", + "G1 F200 X250 E30 ; extrude 30mm of feed stock", + "G92 E0 ; zero the extruded length again", + "G1 X150 Y150 Z25 F12000 ; center head for print", + "G1 F12000 ; set head seek rate" + ], + "post":[ + "G1 E-1 F300 ; retract the filament to release pressure", + "G1 Z{z_max} E-5 F1200 ; drop bed, retract more filament", + "G28 X0 Y0 ; move X/Y to min endstops", + "M107 ; turn off filament cooling fan", + "M104 S0 ; turn off extruder", + "M140 S0 ; turn off bed", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "bed_width": 305, + "bed_depth": 305, + "build_height": 305 + } +} diff --git a/web/kiri/filter/FDM/Ultimaker.Ultimaker2 b/web/kiri/filter/FDM/Ultimaker.Ultimaker2 new file mode 100644 index 00000000..43ece2c8 --- /dev/null +++ b/web/kiri/filter/FDM/Ultimaker.Ultimaker2 @@ -0,0 +1,36 @@ +{ + "pre":[ + "M104 S{temp} T0 ; set extruder temperature", + "M140 S{bed_temp} T0 ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 X0 Y0 ; home XY axes", + "G28 Z0 ; home Z", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 Z0.25 F180 ; move z to 0.25mm over bed", + "G92 E0 ; zero the extruded", + "M109 S{temp} T0 ; wait for extruder to reach target temp", + "G1 E15 F200 ; purge 15mm from extruder", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T0 ; turn off right extruder", + "M104 S0 T1 ; turn off left extruder", + "M140 S0 T0 ; turn off bed", + "G1 Z205 F1200 ; drop bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "bed_width": 223, + "bed_depth": 223, + "build_height": 205 + } +} diff --git a/web/kiri/filter/FDM/Wanhao.Duplicator_4s b/web/kiri/filter/FDM/Wanhao.Duplicator_4s new file mode 100644 index 00000000..d7f56204 --- /dev/null +++ b/web/kiri/filter/FDM/Wanhao.Duplicator_4s @@ -0,0 +1,36 @@ +{ + "pre":[ + "M104 S{temp} T0 ; set extruder temperature", + "M140 S{bed_temp} T0 ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 X0 Y0 ; home XY axes", + "G28 Z0 ; home Z", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 Z0.25 F180 ; move z to 0.25mm over bed", + "G92 E0 ; zero the extruded", + "M109 S{temp} T0 ; wait for extruder to reach target temp", + "G1 E15 F200 ; purge 15mm from extruder", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T0 ; turn off right extruder", + "M104 S0 T1 ; turn off left extruder", + "M140 S0 T0 ; turn off bed", + "G1 Z{z_max} F1200 ; drop bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "bed_width": 225, + "bed_depth": 145, + "build_height": 150 + } +} diff --git a/web/kiri/filter/FDM/Wanhao.Duplicator_5s b/web/kiri/filter/FDM/Wanhao.Duplicator_5s new file mode 100644 index 00000000..9d646869 --- /dev/null +++ b/web/kiri/filter/FDM/Wanhao.Duplicator_5s @@ -0,0 +1,36 @@ +{ + "pre":[ + "M104 S{temp} T0 ; set extruder temperature", + "M140 S{bed_temp} T0 ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 X0 Y0 ; home XY axes", + "G28 Z0 ; home Z", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 Z0.25 F180 ; move z to 0.25mm over bed", + "G92 E0 ; zero the extruded", + "M109 S{temp} T0 ; wait for extruder to reach target temp", + "G1 E15 F200 ; purge 15mm from extruder", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T0 ; turn off right extruder", + "M104 S0 T1 ; turn off left extruder", + "M140 S0 T0 ; turn off bed", + "G1 Z{z_max} F1200 ; drop bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "bed_width": 305, + "bed_depth": 205, + "build_height": 575 + } +} diff --git a/web/kiri/filter/FDM/Wanhao.Duplicator_6 b/web/kiri/filter/FDM/Wanhao.Duplicator_6 new file mode 100644 index 00000000..ad758613 --- /dev/null +++ b/web/kiri/filter/FDM/Wanhao.Duplicator_6 @@ -0,0 +1,36 @@ +{ + "pre":[ + "M104 S{temp} T0 ; set extruder temperature", + "M140 S{bed_temp} T0 ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 X0 Y0 ; home XY axes", + "G28 Z0 ; home Z", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 Z0.25 F180 ; move z to 0.25mm over bed", + "G92 E0 ; zero the extruded", + "M109 S{temp} T0 ; wait for extruder to reach target temp", + "G1 E15 F200 ; purge 15mm from extruder", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T0 ; turn off right extruder", + "M104 S0 T1 ; turn off left extruder", + "M140 S0 T0 ; turn off bed", + "G1 Z{z_max} F1200 ; drop bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "bed_width": 200, + "bed_depth": 200, + "build_height": 180 + } +} diff --git a/web/kiri/filter/FDM/Wanhao.Duplicator_i3 b/web/kiri/filter/FDM/Wanhao.Duplicator_i3 new file mode 100644 index 00000000..ad758613 --- /dev/null +++ b/web/kiri/filter/FDM/Wanhao.Duplicator_i3 @@ -0,0 +1,36 @@ +{ + "pre":[ + "M104 S{temp} T0 ; set extruder temperature", + "M140 S{bed_temp} T0 ; set bed temperature", + "G90 ; set absolute positioning mode", + "M83 ; set relative positioning for extruder", + "M107 ; turn off filament cooling fan", + "G28 X0 Y0 ; home XY axes", + "G28 Z0 ; home Z", + "G92 X0 Y0 Z0 E0 ; reset all axes positions", + "G1 Z0.25 F180 ; move z to 0.25mm over bed", + "G92 E0 ; zero the extruded", + "M109 S{temp} T0 ; wait for extruder to reach target temp", + "G1 E15 F200 ; purge 15mm from extruder", + "G92 E0 ; zero the extruded", + "G1 F225 ; set feed speed" + ], + "post":[ + "M107 ; turn off filament cooling fan", + "M104 S0 T0 ; turn off right extruder", + "M104 S0 T1 ; turn off left extruder", + "M140 S0 T0 ; turn off bed", + "G1 Z{z_max} F1200 ; drop bed", + "G28 X0 Y0 ; home XY axes", + "M84 ; disable stepper motors" + ], + "cmd":{ + "fan_power": "M106 S{fan_speed}" + }, + "settings":{ + "origin_center": false, + "bed_width": 200, + "bed_depth": 200, + "build_height": 180 + } +} diff --git a/web/kiri/help-kiri.html b/web/kiri/help-kiri.html new file mode 100644 index 00000000..9740b678 --- /dev/null +++ b/web/kiri/help-kiri.html @@ -0,0 +1,27 @@ + + +
Quick Start
+ +

+Kiri:Moto +is an extensible, multi-purpose slicing
and visualization engine that produces output for: + +

  • : 3 axis CNC toolpaths +
  • : GCode for 3D printers +
  • : DXG / SVG cut paths +
  • : Directly Integrated +
  • : Thing App +

    + +

    + Files are loaded via drag/drop or 'load' menu +

  • Loaded files are cached in the lower-left menu +
  • Check device mode (FDM, CAM, Laser) +
  • Select a GCode profile for your device +
  • Check slicing parameters in right menu +
  • Slice can be found under 'function' +

    + +

    Refer to the Grid.Space Wiki for more information

    + +

    diff --git a/web/kiri/index.html b/web/kiri/index.html new file mode 100644 index 00000000..a54589c8 --- /dev/null +++ b/web/kiri/index.html @@ -0,0 +1,251 @@ + + + + + + + + + + + + + + + Kiri:Moto + + + + + + + + + + + +
    + +
    + +
    +
    + + +
    + direct device control +
    + + + + + + + + + +
    + +
    +   + +
    +
    +
    +
    + + + + + + +
    + + + + +
    +   +   + + +
    +
    +
    +
    + + + + +
    + + +
    + + +
    + + +
    +
    + + + + +
    +
    + + + + +
    +
    +
    +
    + + + +
    + +
    + + + + : +
    +
    +
    +
    +
    +
    +
    +
    + + + +
    + +
    status
    + +
    +
    + local cache +
    +
    +
    + +
    +
    + saved settings +
    +
    +
    + +
    +
    +
    + device + settings +
    +
    +
    + +
    +
    +
    +
    +
    +
    + + + + + +
    +
    +
    + +
    +
    +
    + tools + details +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    flute
    +
    +
    +
    shaft
    +
    +
    +
    +
    +
    + + + + + +
    +
    +
    + +
    + + + + +    + + + + + +    + + + + + +
    + +
    + + +
    + + + + +
    +
    + + diff --git a/web/kiri/output-gcode.html b/web/kiri/output-gcode.html new file mode 100644 index 00000000..6a518d71 --- /dev/null +++ b/web/kiri/output-gcode.html @@ -0,0 +1,47 @@ + + +output options + +
    + + + + + +
    + + + + + + + + diff --git a/web/kiri/output-laser.html b/web/kiri/output-laser.html new file mode 100644 index 00000000..616a1471 --- /dev/null +++ b/web/kiri/output-laser.html @@ -0,0 +1,18 @@ + + +output options + +
    + + + + +
    + + diff --git a/web/kiri/style-atom3dp.css b/web/kiri/style-atom3dp.css new file mode 100644 index 00000000..a826e547 --- /dev/null +++ b/web/kiri/style-atom3dp.css @@ -0,0 +1,31 @@ +/** atom3dp css */ + +button { + color: #000; + border: 1px solid rgba(20,20,20,0.5); +} +label { + border-bottom: 1px solid rgba(200,200,0,0.75); +} +.grouphead { + background-color: rgba(0,0,0,0.85); +} +#control-left { + background-color: rgba(240,240,20,0.5); +} +#control-right { + color: #000; + background-color: rgba(240,240,20,0.5); +} +#appid span { + background-color: rgba(240,240,20,0.5) !important; +} +#selection span { + background-color: rgba(240,240,20,0.5) !important; +} +#layer-view span { + background-color: rgba(240,240,20,0.5) !important; +} +#modeCAM { + display: none !important; +} diff --git a/web/kiri/style.css b/web/kiri/style.css new file mode 100644 index 00000000..ae1417bf --- /dev/null +++ b/web/kiri/style.css @@ -0,0 +1,578 @@ +th, tr, td, span, div, label, button { + user-select: none; +} +input[type=range]::-moz-focus-outer { + border: 0; +} +.dialog { + display: none; + color: white; + padding: 10px; + border: 1px solid rgba(255,255,255,0.75); + background-color: rgba(20,20,20,0.75); + border-top-left-radius: 5px; + border-bottom-right-radius: 5px; + position: fixed; + top: 25px; + xleft: 25px; +} +.dialog span { + padding: 2px 5px 2px 5px; + margin: 0 5px 5px 5px; + background-color: #333; + border-top-left-radius: 5px; + border-bottom-right-radius: 5px; + border: 1px solid #555; + text-align: center; +} +.helper { + position: relative; + display: none; +} +.mono { + unicode-bidi: embed; + font-family: monospace; + white-space: pre; +} +#selection { + display: none; + text-align: center; + position: fixed; + width: 100%; + bottom: 3px; +} +#selection span { + border: 1px solid #ccc; + border-radius: 5px; + padding: 3px 8px 8px 8px; + background-color: rgba(170,221,255,0.75); + color: rgba(0,0,0,0.5); +} +#selection label { + border: 0; +} +#selection .value { + font-weight: bold; + font-style: italic; + color: black; +} +#selection button { + margin-left: 1px; + border: 1px solid rgba(255,255,255,0.75); +} +#appid { + text-align: center; + position: fixed; + width: 100%; + top: 3px; +} +#appid span { + border: 1px solid #ccc; + border-radius: 5px; + padding: 5px 8px 3px 8px; + background-color: rgba(170,221,255,0.75); + color: rgba(0,0,0,0.5); +} +#appid a { + color: rgba(0,0,0,0.75); + font-weight: bold; +} +#appid a:hover { + color: rgba(0,50,50,0.75); +} +#container { + width: 100%; + height: 100%; + overflow: hidden; +} +#loading { + position: fixed; + top: 30px; + left: 20%; + right: 20%; + border: 2px solid rgba(10,10,10,0.25); + border-radius: 5px; + margin: 2px; + background-color: rgba(200,200,200,0.75); + text-align: center; + display: none; +} +#progress { + position: relative; + width: 1%; + height: 100%; + background-color: #f00; + text-align: left; +} +#progress > span { + color: white; + padding-left: 10px; +} +#modal { + z-index: 20000; + position: fixed; + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; + background-color: rgba(0,0,0,0.2); + display: none; +} + +/** PRINT DIALOG */ + +#print { + position: fixed; + -webkit-transform: translate(-50%, 0); + transform: translate(-50%, 0); + text-align: center; + top: 50px; + left: 50%; + color: white; + border: 1px solid white; + background-color: rgba(20,20,20,0.75); + padding: 10px; +} +#print-close { + position: absolute; + z-index: 200; + right: 3px; + top: 3px; +} +#print > div { + border: 1px solid #888; + border-top-left-radius: 5px; + border-bottom-right-radius: 5px; + padding: 2px; + margin: 6px 0 6px 0; +} +#print .print-sel:hover { + border: 1px solid yellow; + border-top-left-radius: 5px; + border-bottom-right-radius: 5px; +} +#print table { + width: 100%; +} +#print th { + text-align: left; +} +#print td { + text-align: right; +} +#print input { + text-align: right; +} +#print input[disabled] { + background-color: #bbb; + border-color: #ccc; + color: black; +} +#print-density { + disabled: false; +} +#grid-hint { + font-style: italic; + font-weight: lighter; + font-size: 10px; +} +#octo-hint { + font-style: italic; + font-weight: lighter; + font-size: 10px; +} +#print a { + color: #8ae; +} +#print a:hover { + color: #fff; + background-color: #888; +} + +/** sender dialog */ + +#sender { + position: absolute; + text-align: center; + top: 25px; + right: 0px; + color: white; + border: 1px solid white; + background-color: rgba(20,20,20,0.75); + padding: 10px; + display: none; +} +#sender-bar { + border: 0 !important; + margin: 2px 2px 0 0 !important; + padding: 0 !important; + position: absolute; + z-index: 200; + right: 3px; + top: 3px; +} +#sender-bar button { + margin: 0; +} +#sender > div { + border: 1px solid #888; + border-top-left-radius: 5px; + border-bottom-right-radius: 5px; + padding: 2px; + margin: 6px 0 6px 0; +} +#sender .sender-sel:hover { + border: 1px solid yellow; + border-top-left-radius: 5px; + border-bottom-right-radius: 5px; +} +#sender table { + width: 100%; +} +#sender th { + text-align: left; +} +#sender td { + text-align: right; +} +#sender input[disabled] { + background-color: #bbb; + border-color: #ccc; + color: black; +} +#v a { + color: #8ae; +} +#sender a:hover { + color: #fff; + background-color: #888; +} +#sender-log { + width: 100%; + height: 100px; + padding: 5px; + overflow: auto; + text-align: left; + font-size: 11px; + max-width: 100em; +} +#sender-set-zero, #sender-goto-zero, #sender-connect, #sender-port-close { + width: 100%; +} +#sender-jog { + flex-grow: 1; +} +#sender label { + color: #ddd; + background-color: #555; + border: 1px solid #aaa; + padding-top: 1px; + padding-left: 3px; + padding-right: 3px; + padding-bottom: 1px; +} +#sender span { + font-weight: normal; +} +#sender-control td { + text-align: justify; +} +#sender-control input { + margin-left: 2px; +} +#sender-control-buttons { + float:right; +} +#sender-status { + text-align: center; + xbackground-color: #555 !important; + xborder: 1px solid black !important; + xmargin: 2px 2px 2px 0; + xpadding: 2px 2px 2px 0; +} +#sender-quick button { + flex-grow: 1; +} + +/** help dialog */ + +#help { + position: fixed; + -webkit-transform: translate(-50%, 0); + transform: translate(-50%, 0); + text-align: left; + top: 50px; + left: 50%; + color: white; + border: 1px solid white; + background-color: rgba(20,20,20,0.75); + padding: 10px; +} +#help-close { + position: absolute; + z-index: 200; + right: 3px; + top: 3px; +} +#help a { + color: #8ae; +} +#help a:hover { + color: #fff; + background-color: #888; +} +#help p { + font-weight: normal; +} +button.selected { + background-color: white; +} + +/** tool dialog */ + +#tools { + text-align: center; +} +#tools button { + margin-left: 1px; + margin-right: 2px; +} +#tools span { + border: 0; + background: transparent; +} +#tools-body { + height: 100%; +} +#tool-labels span { + width: 200px; + padding: 2px 5px 2px 5px; + margin: 0 5px 5px 5px; + background-color: #333; + border-top-left-radius: 5px; + border-bottom-right-radius: 5px; + border: 1px solid #555; + text-align: center; +} +#tool-cols > div { + width: 200px; + padding: 5px; + margin: 5px; + border: 1px solid #aaa; + border-top-left-radius: 5px; + border-bottom-right-radius: 5px; +} +#tool-info > div { + margin-top: 2px; + margin-bottom: 2px; + flex-basis: auto; + flex-shrink: 0; +} +#tool-action > button { + text-align: center; +} +#tool-action > span { + flex-grow: 1; + width: 100%; +} +#tools label { + text-align: left; +} +#tools input { + background-color: #ddd; + margin-bottom: 1px; + text-align: right; +} +#tools input:focus { + background-color: #dee; +} +#tool-list > select { + flex-grow: 1; + width: 100%; + height: 100%; + color: white; + border: 0; + margin-bottom: 2px; + padding-bottom: 2px; + background-color: transparent; + overflow-y: auto; + outline: none; +} +#tool-list select::-webkit-scrollbar { + display: none; +} + +/** device dialog */ + +#devices { + text-align: center; +} +#devices button { + margin-left: 1px; + margin-right: 2px; +} +#devices span { + border: 0; + background: transparent; +} +#devices-body { + height: 100%; +} +#device-labels span { + width: 200px; + padding: 2px 5px 2px 5px; + margin: 0 5px 5px 5px; + background-color: #333; + border-top-left-radius: 5px; + border-bottom-right-radius: 5px; + border: 1px solid #555; + text-align: center; +} +#device-cols > div { + width: 200px; + padding: 5px; + margin: 5px; + border: 1px solid #aaa; + border-top-left-radius: 5px; + border-bottom-right-radius: 5px; +} +#device-info > div { + margin-top: 2px; + margin-bottom: 2px; + flex-basis: auto; + flex-shrink: 0; + position: relative; +} +#device-action > button { + text-align: center; +} +#device-action > span { + flex-grow: 1; + width: 100%; +} +#devices label { + text-align: left; +} +#devices input { + background-color: #ddd; + margin-bottom: 1px; + text-align: right; +} +#devices input:focus { + background-color: #dee; +} +#device-list select { + flex-grow: 1; + width: 100%; + height: 100%; + color: white; + border: 0; + margin-bottom: 2px; + padding-bottom: 2px; + background-color: transparent; + overflow-y: auto; + outline: none; +} +#device-list select::-webkit-scrollbar { + display: none; +} +#device-list option[local="1"] { + color: #ff5; +} +#device label { + white-space: nowrap; +} +#device textarea { + overflow: hidden; + font-family: monospace; +} + +/** file catalog */ + +#import { + width: 100%; +} +button[import="1"] { + width: 5px; + padding-left: 2px; + padding-right: 2px; +} +#catalog { + text-align: center; + min-height: 25%; + max-height: 75%; + height: 1000px; +} +#catalog div { + position: relative; +} +#catalogBody { + height: 100%; +} +#catalogBody div::-webkit-scrollbar { + display: none; +} +#catalogList { + overflow-y: scroll; + overflow-x: hidden; +} +#catalogList button { + margin: 0px 0px 2px 2px; + overflow-x: hidden; + text-overflow: ellipsis; +} + +/** saved settings list */ +#settings button { + margin: 1px; +} + +/** layer slider and controls */ + +#layer-view { + display: none; + position: fixed; + left: 0; + right: 0; + bottom: 0; + height: 25px; + text-align: center; + vertical-align: middle; +} +#layer-view span { + position: relative; + border-radius: 5px; + padding: 3px 8px 8px 8px; + background-color: rgba(170,221,255,0.75); + color: rgba(0,0,0,0.5); +} +#layer-view input { + vertical-align: middle; +} +#layer-view button { + position: relative; + margin-left: 1px; + border: 1px solid rgba(255,255,255,0.75); +} +#layer-slider { + border: 0; + margin: 0; + padding: 0; + height: 100%; + width: 50%; +} +#layers { + display: none; + position: fixed; + bottom: 30px; + background-color: rgba(0,0,0,0.2); + border: 1px solid rgba(0,0,0,0.5); + border-radius: 3px; + padding: 6px; + -webkit-transform: translate(-10px, 0); + transform: translate(-10px, 0); +} +#layers label { + text-align: left; + color: rgba(0,0,0,0.85); +} diff --git a/web/meta/favicon-mobile.png b/web/meta/favicon-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..66119682e118fafee829b1598f368de8e30e85bc GIT binary patch literal 6035 zcmZ`+byQSsw;w_p2I-z*KvEb&Vu*nOq)X`%=?>|xL0URxXe2~XKw3Z=X%UnZDG6y9 zVhAt3?{~lNUF*B|to5AddG>GrcAh`>iPhFrA|+xV0ssJ{D#{9vZ+GQCivaKTt+!jR z1OO1>JIKpxtH{fPwLRQy9h_}$;n?Sy_}aP~3{k&58^z|jCj*TFpv21i8UfT9HsP;o z-s#?Dq|gNy$HatDBx_SAL@^LU;+}&G!|sei5VEmd*cBBEIBWe2>OyCd$K(D#^Y%l> zeQy?Xj^M03d$0lQ?vI<{nvEWaE>@3bb3ml2!c*)A*>Bbq7@<(#gSQRhaVHRO zEvM;UOP4pbxNML#BY^A^0%6101*%9Ocqc1AwRvcjfY>voh&gI5CJ$#(fP9D zC7n;X=(rQ``bkzJ2)o}2Ze^RQ6PD3@iEBxu?^3s*sWrS$sy6WQX_4aZMb%@Z(;=XWY%E3xo^w3)Bk&u0&@nTHV7d=)=Twaw+_R7tk)T z73ga8;g@rp2mm7Isp0{<0(m1P5ur#Vf45mTl|>Ghaw#QWOh~Wc$`ecO+8n<;am^q# zND7s58N1tK5(i*qx>urC+5-eemIhLlwmVp~rhCNd!Q7I2m3);cS5%?>hMP5=YKT=8 zXPsf4dtDlop_M!S92(&!W)3rFToPE~4Z-dd>LmS5bEAGkaYKCNd$EB_fX##LgRLHJ zhP{rRhi!#T9HEPlMrd{6c42oZc9}#7%Zam6v*H&K6fqTjk^2_OQOWfhGMzY7yjSv7 z`>jrSWNo*X1?{roa?o;pH%|94L=F-K!Gh33)*)F6eh;WLp8u4KFQ=Xy`=aOodid(0 zivrt2V4P??Sv-HNN}N&r!?^X>hL{&I!Bkn4zT`=iGLY~5HwxH%Mf}TrDg4h=S6X{f zrSD2x*wuuzjy|lhaT}_d4JkQBPdQYERNir*ci`Mo*rJ^LR=QzrFZ5NYR;W)X%f{BG zvM#euu&%Gpz}kE;c5rsEZ#ZKJJv2J3_DOQMe8}Jva1hEnL48GCM{P`%&qYB^ANMG( zFHSn{ShY@-Mb$`ELp3R#aEN5xetmT)cu0-MD?>KpnrD|s*h{dLSUT;y0?6Q1g>fZE zrGC|SgPzxdQ!7)NuSs9`Y%6Ym-d^AK6?sZ(^@8q28iV{xIt?9-MB9d+*)&GOio6R%242uH+asf2~vjJuSZYR@dBg{JHjFby6 z5wDF@g5(LKBb_2czEIFp1(73xIkzdRli8WxY^MchDW_s<3fspPLjB~neu6}FVj6;F zjb&{b(3c?s!lG(|l5VGMEt&o(|B{OlJWSLesR$znAJzSA8a)~*HQ2Dg@cw;&{Ud$# zD#t3juW?`Rer@>rMbuHWR@Bb<$?W!=hl`0z*3QU|&z$#s$xhzR@eV#yB-0i15L3Pc zk9Zw(F4IO{0?J3+Tha=(h)NPqYEf=`XUbsu(@ewExAn9&Wie=RXd%C)s?Bq;P3kGo zAgP39l`%uiL$omvTZ?wQBwy>p<`b1CGX7TW?H`8Q3obV=tS?S39tSsJL^1rBSqwEs z`qTieh2AIp0VE)`Com*>54=ybc4rHBm!y^Q{#_=@g}V{NgannOMl);gwr0vIwM!-z zCO(gzjMC@2xunccbgob8-&BuO({>A3gj>9@D9Fwqar`OQtmgOdTxvMc8-*6?mM9jt z6rF_Bad}7#L0)q?M!9sshw@)E#_`AI$JfSl$NMw?Vlts`quQW1plzZ6(JiHhX%VPx zt`R(25pSQjZnSY`RGm^D(OA2ebB|f=r?jHA(NE0W-Gc-5GI}ED+^N_H!|#vY zpKW1n(F)-Ti_>4vPd9-U;CWlw7uRGbWM32W?_a!WH=>ku@zLLn@Oi$!JzL!@X?Hfo zQZ9`Sj162py+Kvynx$X6n*ZpiVcJ4T!_t<-gTJH?XAO@s&%1SW9sw}2BjMplJbW#t zt5n()UrBSZPWInASXDnxn*x?HL)tvJwkKqZo-J4Q%=DmoN|fFtPSFz5*wAROpvC9( z?;D^DuJ&fWS~aPXsPQCGCY7^nWVB}aptL2Po%fd4ZPulD%pYYERmLHy)i~KS$qMS= z)27^JIp!CKZQ1)Qx-h*4y(---RDZsvn`PwgaMHoUAdw&%Op_}DV#2@N5*v^e=Nf00j|Ks+kL^V{&V zs>`Soa;~vyv`MMySRz6)O2SMsuyv%(ICS!+a`->zEUk?hS%x%|XN=+pnvj~e+lB7;2WMUSDvBnOyuc*E>4=$ErDOaAjs%|k4z zx*ru>fbUaSvV@9tJ>b;m=U;`tf&B3>YIPS-B=wHL4Mlza@Yf-_FbOBX)fwPNqIJ5? z;A6$?gare(;sTK{Qj#0fP+gDo@EeTqpeg&y59UVxyrh5{;;`|BjEwa(Ufu$yWD5}X zMBYiL-6iAOjjri#2zN^i|La6P=0Q=s(6_$lwz8+sTm7UD?PJ03fFPv#yCvkgpvGKA1`?)x~dP?|7L;li` zxIO<7Lm}Y5RJ@#|A%+^-V0kwW8?Xrf1AYMroCpjCOLE`Vv4T1b|^zY~IIBonK{^QBj^IvJ*CJ6n5Kn3{)p#K-m%fa^lq5VPrru~)I z-|nRT7?aR;@Uw9?QgCpwarL|n4K5@o^_Q3b0{#={KZu6^APW4G_z&P8#6NLJXn8o; z+?ME1A>g;p{!8{ReJSXlD*dBsf4Af>>1|KoL{iXy_ZLpYVcW8B`&qE7C?ItGuy#!s zU03_*^H{l6k7~@oGd|&c9AaB~djaN!{?U#8dA8=9xgX>UAO2u+ar`rte>L!1Rx1IiMuC2OxUOUyC zHh@)acn=DCS7%VNU|L-D5ExC+9Cz>B=gGMC(WvllBs!sg z?KWxcS<|z2En#eSbBrd6v=-s*N72ammZcSbGrtnRhe4;=LmCLt!}AZ?lr?BJr%H2( zM0u*c%R|V2Mo&wK(^lLUvY?8ZvmTfCq7Qcx(st0E2v6UQDMOlY}J08Sa zm$$4Bvcgo$L$RO*TxspyAAyK5RXf`ktn91}v%1rRX~uM2Ib@bDliUo+zj$YEj9=9T z2&#m%>%LZ;;JR{2N@t0Ia!3#wqefT#8$%o>f256McPbi(Ccp$fhqsCu%hqmmx$gp4a>^SC>F6kQ2FY*VG^aQF8gj|&gvfzMR{a52-H6x0n z*;`4G#EDF)o;T&+#`6bMNA-V+Kar`3c}Ar_i0G29T^&It6p!HrezGoODWmkx&GtL| z5-RiEde(OcmsY1&?3r?-%lUYij9U*U|2u{Qnp}7t!Hv*B=VJK2G51M*i2xXh0#sz{ zb8x^wqZyc0>tcSP5?d_LgKW`**D*DuKpejTnfwshSGn%R-Pxu$_0uz5N6O(C=g(q6>Geo+Ktw0^Pk3LEsyF?@+*+aE%q2K-ZX{g zPqxoF_oNlaK+0oF(2$hIK$p(eYuW4f#>l{i-ZXFOAO2-_^T!RO1#4GUR5y7TN7nN) zT5}15&`0U@Ge?ieJRYkr;#%2!a_~;6<`FraZ&#*#Xu_pNQXQ$Ka5ksiWU!J;6!Q4( z`+?8?tvm;_P5eQEiz><0VFT}%nYu}BM{>~LXmZ<4<_h^F_a8g&R>=-XX7ok3V1v*~ zG@2S+O~Jo<@H2h0j<=*IrD?@bd}oHoB7>sQ2jM_6gRuRsCNTs_RHk!n=SC*h^d0ZW zsqWdu{u$bDuD**M3@GxN^X1FCk%@-*#H-jsLAm2I9^yYL>*|>(Ku_gcmz5^QSk4V* zLY`bSNk1G_;;caF`;$oYeqioq+DmRz2@yOvBupJs4nQPIH{!@;MvM4Y@4O9=h`?OW zbx5$gJ2&x19$R@ec8hB=MLBi6jjDp309hN0%;H9vE-g#A4rGc zV38OJ_e1gaIJoRkinA@X&%?$N%t_w)U65LC^wW%0u%5RO4h2KBU=Jzk)gAgKi)QC8OVyVw;(fB)g|EEy>Ctx+_>qYZwvYYx zEFRb0O(UjsVz7?DGFM#w)YcVS`kDXsCEw3Gpbz3# zeq}R0&PIhmLT6UCxp=5}-UYY@UIC6zw#vF}w zFxdj{%;NJ3(k340VQ)oma42%nhBV64yPMiXHGe$O^?>O2WKn$qJ^SWw+y&y7?wg(O#}vHLO_zzZZ#xg-R`zHX2}(`} zq;s2}yovw;3Yy-vdr`d4>C4@i#Y9yB>FzBpT=`vW6y&IWN{;OblYOh^%9H5QrOYR} z9GbVzC$wtm-Y#iWefGeM?Z1V3G2D#>ojnUUx)T5YUMgLVbfq)S{z*0pOuHPvtrJ@K`ng* z1|%3infL?yp?AI|lLyAtweAFO>dNLRgOa@{f)2~uvZh~|ze#yUAH_?$K3SiAuypiE z_rA0hk!QLepL7AX%)UMnys#l;JD4wnW94}C>qY~9KU99@4xDg!g^Qb+?=4d(a4iVa z6pDYv8zTXCjQ!!N36nkI^^O?Y)(V8-Xz8d&MX(>-XS-0NsSzPaotx1|p6ttkPDVoD z)WBFXOerc(GeV&NIdH}M;qBQk2ol#x z9!7v7nQ$Hw>_~lKPIYxB%QUZ5PZ5$j_%MxsM>z+Of+;k^phT~d!pt|*&I=J6u;iSh zr_UHCSBTyx-$4MIn*s1I%=bdVN_>J-XExa@)C!Jgm4nAj);zO?YK_9fF@=YiQc1Lp z2B@@9OVtFeB1evoaH`z$d~kxu*58;oP2iiN`%L4yNg>v(Q=qf5I-GYtLG@@}xDiDe zs?lDt*GG%ejBvgq#axKn13W=&jxup0YoF&Ry9Foq;d2}!wh$*OG)w3$cT6EFYp=Id z#)af1zp6ltA5A5!P$kR864z5T8x(YRe`LO#vSKWXPJ$)L?I3^jB({&{pCbv5HU}P6kq_aQ3- z7Ds0rlcaWGeJ}M_S|BbrCjmN)GTK8Gm~)S2G|(pk3cQJ>^} zprVeFJEQCc?{<(r=)+g1PvA41SyBVxUahmDRW@lA>xKs^$bu=_mZmK}Z+uiJ0u~3^ zI2Vytjl)vlF%0D;o5*ZJ>$+G>;cNH+S+Jy=a-NBATDamV4B$U|?@gJ$m4ZpLfHJ68 zRJ}3yP^Mh#C8Yn0Ph6~)RJ=ndUG-4*!(j6GOZ;H6&KjWOH_jG)Y5Kl0_y!H85HvBg z?O6!4#Dvd$Eg#Y+8O~&{)zkxdwMD}#qO^j^tGlsMp>TpS=h$5qf{#k-nwI5S5!G?< z3bO)sQ3>HVjo>kC4EV~V=lTR;ok|%J2Djr35ra-PVSH}qsXg}1+7m=BOR#npoUq;r z7)+iYfpelFNk|bqCWuBbZX%1BF@;tuXIp8Jg;J59ENo270^rZ#8&l^nYSIYuPwXps zD>AhHR;wKtzUvK+3|6FB85k~h-gFf5b{XBzmJ}2FDDAh1>w@w;eAH!0W@-Dfk?oEB zIN4?P$zf3^8R3uvYhz#zuJz@P!dKp~(AL>x#lFaYIf0UmEr|BnsB?7}7o5<`=> z1d1W!ktUB3hG^k~Qy!*nplBlF7pME7`jO)v#2$@*XoM09UrOTrKQNRV8yOh(?nT8Q JF_0V~c>uX + + + + + + + + + Meta:Moto + + + + + + + + + + +
    + + diff --git a/web/meta/style.css b/web/meta/style.css new file mode 100644 index 00000000..2bc67ff8 --- /dev/null +++ b/web/meta/style.css @@ -0,0 +1,56 @@ +.ck_space button { + margin: 2px 0px 1px 2px !important; +} + +.ck_spaces { + padding-top: 2px; +} +.ck_spaces div { + -webkit-flex-direction: row; + -webkit-justify-content: flex-start; + -webkit-align-items: center; + display: -webkit-flex; + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + margin-bottom: 1px; +} +.ck_spaces button { + margin: 0px 0px 1px 2px !important; +} + +.ck_meshes { + padding-top: 2px; +} +.ck_meshes div { + -webkit-flex-direction: row; + -webkit-justify-content: flex-start; + -webkit-align-items: center; + display: -webkit-flex; + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + margin-bottom: 1px; +} +.ck_meshes button { + margin: 0px 0px 1px 2px !important; +} + +button.load { + -webkit-flex-basis: 1px; + -webkit-flex-grow: 1; + flex-basis: 1px; + flex-grow: 1; + text-align: left; +} +button.del { +} + +#dropbutton { + width: 100%; + height: 50px; + background-color: #ccc; + border-width: 1px; +} diff --git a/web/moto/style.css b/web/moto/style.css new file mode 100644 index 00000000..96931887 --- /dev/null +++ b/web/moto/style.css @@ -0,0 +1,277 @@ +/** ****************************************************************** + * Element + ******************************************************************* */ + +a, a:hover, a:visited { + border: none; + outline: none; + color: inherit; + text-decoration: none; +} +body { + overflow: hidden; +} +body,div { + margin: 0px; + padding: 0px; + border: 0px; + font-weight: normal; + font-family: sans-serif; +} +label { + font-size: 14px; + border-bottom: 1px solid #888; + margin-right: 2px; +} +canvas { + width: 100%; + height: 100%; + overflow: hidden; +} +button { + outline: none; + max-width: 25ex; + border: 1px solid rgba(220,220,220,0.8); + border-top-left-radius: 3px; + border-bottom-right-radius: 3px; + background-color: rgba(210,210,210,0.8); +} +button:hover { + background-color: #eee; +} +button[load] { + width: 100%; + text-align: left; +} +button[del] { + margin-left: 5px; +} + +/** ****************************************************************** + * Class + ******************************************************************* */ + +.noselect { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.title { + text-align:center; + border-radius: 4px; + border-bottom: 2px; + color: #222; + padding: 4px 5px 3px 5px; + margin-bottom: 2px; + background-color: rgba(170,221,255,0.75); + border: 1px solid rgba(255,255,255,0.5); + font-size: 12pt !important; + font-weight: bold !important; +} +.tablerow { + -webkit-flex-direction: row; + -webkit-justify-content: center; + -webkit-align-items: center; + display: -webkit-flex; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; +} +.tablerow button { + -webkit-flex-basis: auto; + -webkit-flex-grow: 1; + flex-grow: 1; + flex-basis: auto; + width: 33.33%; +} +.grouphead { + color: #fff; + border-radius: 4px; + padding: 4px 5px 3px 5px; + margin-top: 3px; + margin-bottom: 1px; + text-align: center; + text-transform: uppercase; + font-size: 10pt; + font-weight: normal; + background-color: rgba(0,0,0,0.75); +} +.grouphead:hover { + cursor: default; +} +.ck_catalog { + padding-top: 2px; +} +.ck_catalog div { + -webkit-flex-direction: row; + -webkit-justify-content: center; + -webkit-align-items: center; + display: -webkit-flex; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + margin-bottom: 1px; +} +.ck_catalog button { + margin: 0px 0px 1px 2px !important; +} +.buton { + font-weight: bold; + background-color: #fff; +} + +.flow-row { + position: relative; + -webkit-flex-direction: row; + -webkit-justify-content: flex-end; + display: -webkit-flex; + display: flex; + flex-direction: row; + justify-content: flex-end; +} +.flow-row label { + -webkit-flex-basis: auto; + -webkit-flex-grow: 1; + flex-basis: auto; + flex-grow: 1; +} +.flow-col { + position: relative; + -webkit-flex-direction: column; + -webkit-justify-content: flex-start; + display: -webkit-flex; + display: flex; + flex-direction: column; + justify-content: flex-start; +} +.flow-left { + -webkit-justify-content: flex-start; + justify-content: flex-start; +} +.flow-grow { + -webkit-flex-grow: 1; + flex-grow: 1 +} +.flow-space-between { + -webkit-justify-content: space-between; + justify-content: space-between; +} + +.control input { + background-color: #ddd; + margin-bottom: 1px; + text-align: right; +} +.control input:focus { + background-color: #dee; +} +.control input[disabled] { + background-color: #aaa; + border-color: #bbb; + color: #000; + border-width: 1px; +} +.control input[type="range"] { + width: 85px; + background-color: transparent; +} +.control button { + margin: 1px; +} + +/** ****************************************************************** + * ID + ******************************************************************* */ + +#container { + width: 100%; + height: 100%; + xoverflow: hidden; +} +#control-left { + position: fixed; + z-index: 10000; + color: #eee; + top: 0px; + left: 0px; + bottom: 0px; + padding: 5px; + border-top: 1px solid #bbb; + border-right: 1px solid #bbb; + border-bottom: 1px solid #bbb; + background-color: rgba(20,20,20,0.5); + overflow-y: scroll; +} +#control-left::-webkit-scrollbar { + display: none; +} +#control-right::-webkit-scrollbar { + display: none; +} +#control-right { + position: fixed; + z-index: 10000; + top: 0px; + right: 0px; + bottom: 0px; + padding: 5px; + border-top: 1px solid #bbb; + border-left: 1px solid #bbb; + border-bottom: 1px solid #bbb; + background-color: rgba(20,20,20,.5); + color: #eee; + overflow-y: scroll; + overflow-x: visible; +} +#loading { + position: fixed; + top: 5px; + left: 20%; + right: 20%; + border: 1px solid white; + margin: 2px; + background-color: #ddd; + text-align: center; + display: none; +} +#modal { + display: none; + position: fixed; + z-index: 20000; + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; + background-color: rgba(0,0,0,0); +} +#progress { + position: relative; + width: 1%; + height: 100%; + background-color: #f00; +} +#welcome { + color: white; + padding: 10px; + border: 1px solid white; + background-color: rgba(20,20,20,0.75); + position: fixed; + top: 50px; + left: 0; + right: 0; + bottom: 100px; + margin: 0 auto; + width: 50%; + max-width: 800px; + min-width: 500px; + display: block; + unicode-bidi: embed; + font-family: monospace; + white-space: pre; + overflow-y: scroll +} diff --git a/web/obj/cube.stl b/web/obj/cube.stl new file mode 100644 index 0000000000000000000000000000000000000000..501c236d792d9a96a033d282da6586f39eeb444c GIT binary patch literal 16084 zcmbuGzl)z)6~^CzO;U)3h{85Y2qv8{A-FDBOftbGViPO`n-uOE5k(P+$X3G^He(Tj z2DY-AkYd|FKwFrdw*v2Z=leeQG5bKV~l!GR%{_uS7tzwSNveBbZn z7eD*SkKR51%gei8KmX31@BiTZI2%&mF}@RB$26tHiETeB zkrwj#^S;}cIwi8IU-^F(cCQu}XOLxpU=qjjj>M znlhCcRAm@BA4HWTvF%4C(kDJ=J+6LKNfN7mTtv*I9aYBT<8d20IziicIvE9wWi4xB z&qpJq{l)FSkJ=7>vUW*S32Qs+AbsN4bu0TI^y2wJ|=OdJs&!fm?&|Ax|5i7OR+9v5Ca(}G|@vzmIP_*mf{|a z8z9z&hw=$ySw+tmL};HA@|*-CE4w1M!*-786LNm0`?Mg2WJ#1=65gX%#7%!LtWuwV zv3jj@kq`7*{6V~ufP_0y1oD6Gdg;g(1a~55v8!3rQ6;%9cct{XRZjRUJulr-v&tn- z$U;_(H62`wR&iFwu!pMct&G*2sB!D{>L~t=z8&3*JWL(hSEXKc0=9Ui3_RyC+lZ7n!7(E;dWNneiB?y-h3E`QbWI1crc7l9RT)Ok2T>(SsE#fD zs6<-Wo`;1SI))^PRX;8wFVc=GY!%Ac= z;ThC^<*9{*&5xl&CkN40WuQL4OI(LeC)RSJT6-!n;O_R9X|PY?`v=E-?{RyG3N7a0+LFQ#`E4S#qB&@qKE8|83m5>`8M&m z_qcPqTZK6FpF^|#F!rx}((`Qsl1jMeSpML1?99*`d>rw47*^ptzHH;xcO9IKy8AHm zkwlkN;=TR1Z@=~m0=r$aT8Fc$qe_zCu9Srg68&((XR(W-e?6$(bC);)3-4U{TaUs* zM~_vVl`*V3*u6@7u3O{Qt4~z9NAZb5-Xen_JpXMmKXa*WpBt zKf0#QV05oTM_To`*In)KTm8(>b)ZJ+JSY9o>f>odT7_9!3Hf)=UE;*5AK}ZR_^g&Y z+^x!rYF2eZwYGYm^l3z{%jn|WYl-g6Dy)?jHlN~~l$~j=qe_xUeWw%YQ?6vuZB|uD z5{Pc`asKn>wL!;d8NjS{1P%9nn4Z3m$fl~%zL>A={DYOkW(dz2A%Cw%|Z9j-Gt z*U@&#sQRitwLU__jR(BT??!a_34;Bgf10>y?`kUHk|i;^zDv`D_0eZ(zas9k9pjtU zuY_4fxTF%<)r3_+9PGXP%{yOdenb?h9iLd9_wOCU+VQ*+E?E+zLylP8Jz~6Q2|q#5 zCysBny})t!VT4O6f!!_wZ#WUFtKrX$*6;A0)s4u(+4e3A&(Uk?Elwp;GABL`ir?Lx zLPU?kSvOiIL*2PUXcc;#VU-c=2KiR4>X;LJ_eMYTR;?27gA&X_AB1)3liu8&LbTqH z1a9heKT_iBa?B`v@1H+>&?`{n1(u{$N%ItNzg5B|p3R$<*=-_Elx|MG8(0u#Ra)r# zZBF3iaL2h%ZtXVVv-}iP!X-&mKYn}k?IYFq#cm%voPO~0U?p5aKX@iW-=MdNtR=FF z9l)H(Jl727`|CN8l1gN~+9up5=AYH45-v%i`VslK#0~)84|+^$tnxGNoJffiUKyN# z_Pil$ozEKVgetXeoYdzvf1B`G@K1HV5-xFK%MZp%YxykDs(g_Tyt|6N1aAj@Ry|Q! z+bcm!R?px#&D%uYABs9F!;+A_ZXqMWdNn8VhOET4A4#}RyhYb^xFm_{N4;@!mi94k z2JZZ|n{%FXMZrCv>&VlIS-M}TTW_QM#)(<|4K1w7D#7RK8PxZnZ@_yv?JJHMpL_N^ zJ0%b(dUG`C|SJdZq#fe&@oN#Nm z37@rP6{4G!%!zH!o$y&6k;q5;_6qyA)shh(-{GMzY9gah371qN?~-jItEj(|ou&n0 zR(S^Vwwn_vac|w)ZNg{yO;ib&BvJi{oZBZakB{$m-d(&6D&dk!WL?=N^8S!jtQd15 zGvyZ2C!XdxkvC)|@{Zaj+$ZLLO@~X8sD9K+owHbpRCjp_bl3XLnOOsoQE<=aI_eqB zT9W&1PGm)fCAGGXo*|kUet)O8YOq)DNHNamyHUgVGbeTnku(X;CYhbL-d{03dcI9S z;-88&#`FID;4yUQ&E2p)>c{Y@jkk$idfSQS`5+=rZGHNYJ6tuGe&~7kV}*dk??H`8 zaCPQ{Zyl%Ut$lsRhOypXVyxNt_h8Q@PU!o#|AAeBBtgw|H~U7sQyAF|4tIA3S$hE7$e7jc@zne!`Dpf=p=)KlomJn(!Q-XL+MV zmW#xRyD{@+{6e=As4{p5fNGiag_xyXg0T}WaU#catJVq3l8$YnpO5$y25)QB*R86v zlIS|nx%TL01;~B8JKOD;okT`7^Wl|fuEXn6jxR)QR$&aA>( zX<=6IxsEDHBK4h4q))k$MYma1B}pv&IF5KV!gqC!v19x;z-$pNsf7FFMD0JcZoi%T zeKjgWx9a`YJJ)*&-iq$A8dkNMG1kgi`FxvzBni*vmQ^6!TaEENf6zN*UCLO^^*K@P z&2=-gOC6c#y2Et_=R{kp*Fmp7weG^I$RbW|~F=X(2jR6@`>+Z+>bQ^YdN7Yby#%wBpEuIx4j#PJn6m9$YAV5 zW^k1lboU|rz~2-lQQw#G`S2WG7lIPzfR@biu^0Jh-~RBkv%Yu4y2{O|M6k;mISL;Ft{@-_OEA9duf>)&Bnhq zAM}=8m`A!3%_jJ2Hb=z@S{@ymC}ZTGzywsV(I z2kCM?3PFhz9P@c(uw{=q&xm?%IF^5w^YFuM$GV#3`fRuNSUv0CvM1sCFn{E|SD!6H G`u+&~h`T#h#k@~jH2#Zx!D4#m4IJtNZPH9$ literal 0 HcmV?d00001 diff --git a/web/obj/meta-corner-2.stl b/web/obj/meta-corner-2.stl new file mode 100644 index 0000000000000000000000000000000000000000..7ae11cc4ea3a8e66c68fbbca3a280917a3732674 GIT binary patch literal 484 zcmb`Du?>JQ3g4Z^y|iv{(w*7|_Qh}@+qGQ1l28ITpU0@lgS;(rux}oAs3=33%x*CvlKwW}t2eJ;3 V9iM@I0b-y(knKQL1=9`k9{^RWFaZDn literal 0 HcmV?d00001 diff --git a/web/obj/meta-corner-4.stl b/web/obj/meta-corner-4.stl new file mode 100644 index 0000000000000000000000000000000000000000..b1b189412e7f69e9ef3b543132b1b212306ca28c GIT binary patch literal 284 zcmZQzpe|qm8`ofuFmHx`L=tj};VJQPc d!+unAvFQexWPMh6zZC=nU5B9x%~c@R0st{9A`t)p literal 0 HcmV?d00001