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 1.0) this.dump();
+ if (c2 > 1.0) poly.dump();
+ }
+
+ if (recurse) {
+ var i, ai = this.inner, bi = poly.inner;
+ if (ai !== bi) {
+ if (ai === null || bi === null || ai.length != bi.length) return false;
+ for (i=0; i < ai.length; i++) {
+ if (!ai[i].isEquivalent(bi[i])) return false;
+ }
+ }
+ }
+
+ var exit = true,
+ pointok,
+ dist,
+ min;
+
+ this.forEachPoint(function(i2p) {
+ pointok = false;
+ poly.forEachSegment(function(i1p1, i1p2) {
+ // if point is close to poly, terminate search, go to next point
+ if ((dist = i2p.distToLine(i1p1, i1p2)) < CONF.precision_poly_merge) return pointok = true;
+ // otherwise track min and keep searching
+ min = Math.min(min, dist);
+ });
+ // fail poly if one point is bad
+ if (!pointok) {
+ if (DBUG.get('equiv')) DBUG.log({min:min, prec:CONF.precision_poly_merge});
+ exit = false;
+ // terminate search
+ return true;
+ }
+ });
+ return exit;
+
+ }
+
+ return false;
+ };
+
+ /**
+ * find the point of this polygon closest to
+ * the provided point. assist generating optimal
+ * print paths.
+ *
+ * @param {Point} target
+ * @return {Object} {point:point, distance:distance}
+ */
+ PlP.findClosestPointTo = function(target) {
+ var dist,
+ index,
+ closest,
+ mindist = Infinity;
+
+ this.forEachPoint(function(point, pos) {
+ dist = SQRT(point.distToSq2D(target));
+ if (dist < mindist) {
+ index = pos;
+ mindist = dist;
+ closest = point;
+ }
+ });
+
+ return {point:closest, distance:mindist, index:index};
+ };
+
+ /**
+ * @param {Polygon[]} out
+ * @returns {Polygon[]}
+ */
+ PlP.flattenTo = function(out) {
+ out.push(this);
+ if (this.inner) out.appendAll(this.inner);
+ return out;
+ };
+
+ /**
+ * @param {Polygon} poly clipping mask
+ * @returns {?Polygon[]}
+ */
+ PlP.diff = function(poly) {
+ var fillang = this.fillang && this.area() > poly.area() ? this.fillang : poly.fillang,
+ clib = self.ClipperLib,
+ ctyp = clib.ClipType,
+ ptyp = clib.PolyType,
+ cfil = clib.PolyFillType,
+ clip = new clib.Clipper(),
+ ctre = new clib.PolyTree(),
+ sp1 = this.toClipper(),
+ sp2 = poly.toClipper();
+
+ clip.AddPaths(sp1, ptyp.ptSubject, true);
+ clip.AddPaths(sp2, ptyp.ptClip, true);
+
+ if (clip.Execute(ctyp.ctDifference, ctre, cfil.pftEvenOdd, cfil.pftEvenOdd)) {
+ poly = POLY().fromClipperTree(ctre, poly.getZ());
+ poly.forEach(function(p) {
+ p.fillang = fillang;
+ })
+ return poly;
+ } else {
+ return null;
+ }
+ };
+
+ /**
+ * @param {Polygon} poly clipping mask
+ * @returns {?Polygon[]}
+ */
+ PlP.mask = function(poly) {
+ var fillang = this.fillang && this.area() > poly.area() ? this.fillang : poly.fillang,
+ clib = self.ClipperLib,
+ ctyp = clib.ClipType,
+ ptyp = clib.PolyType,
+ cfil = clib.PolyFillType,
+ clip = new clib.Clipper(),
+ ctre = new clib.PolyTree(),
+ sp1 = this.toClipper(),
+ sp2 = poly.toClipper();
+
+ clip.AddPaths(sp1, ptyp.ptSubject, true);
+ clip.AddPaths(sp2, ptyp.ptClip, true);
+
+ if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftEvenOdd, cfil.pftEvenOdd)) {
+ poly = POLY().fromClipperTree(ctre, poly.getZ());
+ poly.forEach(function(p) {
+ p.fillang = fillang;
+ })
+ return poly;
+ } else {
+ return null;
+ }
+ };
+
+ /**
+ * return logical OR of two polygons' enclosed areas
+ *
+ * @param {Polygon} poly
+ * @returns {?Polygon} intersected polygon or null if no intersection
+ */
+ PlP.union = function(poly) {
+ if (!this.overlaps(poly)) return null;
+
+ var fillang = this.fillang && this.area() > poly.area() ? this.fillang : poly.fillang,
+ clib = self.ClipperLib,
+ ctyp = clib.ClipType,
+ ptyp = clib.PolyType,
+ cfil = clib.PolyFillType,
+ clip = new clib.Clipper(),
+ ctre = new clib.PolyTree(),
+ sp1 = this.toClipper(),
+ sp2 = poly.toClipper();
+
+ clip.AddPaths(sp1, ptyp.ptSubject, true);
+ clip.AddPaths(sp2, ptyp.ptClip, true);
+
+ if (clip.Execute(ctyp.ctUnion, ctre, cfil.pftEvenOdd, cfil.pftEvenOdd)) {
+ poly = POLY().fromClipperTree(ctre, poly.getZ());
+ if (poly.length === 1) {
+ poly = poly[0];
+ poly.fillang = fillang;
+ return poly;
+ }
+ }
+
+ return null;
+ };
+
+ /**
+ * rotate such that first and last points are the points
+ * furthest apart and lowest when there is a tie breaker
+ */
+ PlP.spread = function() {
+ var poly = this,
+ points = poly.points,
+ plen = points.length,
+ i, pp, np, p, mdelta, newmax, max, shift, maxd;
+
+ max = {d:0};
+ maxd = 0;
+ // find two most distance points
+ for (i=0; i maxd;
+ maxd = MAX(maxd,p);
+ // select lowest points (corner case = square)
+ if (newmax || (mdelta < 0.001 && max.p1 && MIN(pp.z,np.z) < MIN(max.p1.z, max.p2.z))) {
+ max = {p1:pp, p2:np, i:(i+1)%plen, d:p};
+ }
+ }
+
+ if (max.i > 0) {
+ // shift array to start at "leftmost" Point
+ shift = points.slice(max.i);
+ shift.appendAll(points.slice(0,max.i));
+ return newPolygon(shift);
+ }
+
+ return poly;
+ }
+
+ /** ******************************************************************
+ * Connect to base and Helpers
+ ******************************************************************* */
+
+ function newPolygon(points) {
+ return new Polygon(points);
+ }
+
+})();
diff --git a/js/geo-polygons.js b/js/geo-polygons.js
new file mode 100644
index 00000000..d591425f
--- /dev/null
+++ b/js/geo-polygons.js
@@ -0,0 +1,710 @@
+"use strict";
+
+var gs_base_polygons = {
+ copyright:"stewart allen -- 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(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;ggv};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@*NKv8XMchT&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
+
+
+
+
+
+
+ |
+
+
+ |
+ |
+
+
+ |
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ device
+ settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
z2m%>%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