initial commit

This commit is contained in:
Stewart Allen 2017-04-18 17:09:33 -04:00
commit bd51ac38f0
90 changed files with 29458 additions and 0 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.DS_Store
node_modules
persist
cache
mod
*.log
obj/*.stl

110
js/add-array.js Normal file
View file

@ -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('');
};
})();

119
js/add-three.js Normal file
View file

@ -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 = [];
};
})();

6986
js/ext-clip.js Executable file

File diff suppressed because it is too large Load diff

270
js/ext-fsave.js Normal file
View file

@ -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;
});
}

1
js/ext-n3d.js Symbolic link
View file

@ -0,0 +1 @@
../node_modules/n3d-threejs/index.js

1
js/ext-pixi.js Symbolic link
View file

@ -0,0 +1 @@
../node_modules/pixi.js/bin/pixi.js

1
js/ext-tween.js Symbolic link
View file

@ -0,0 +1 @@
../node_modules/tween.js/index.js

141
js/geo-bounds.js Normal file
View file

@ -0,0 +1,141 @@
"use strict";
var gs_base_bounds = {
copyright:"stewart allen <stewart@neuron.com> -- 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();
}
})();

221
js/geo-debug.js Normal file
View file

@ -0,0 +1,221 @@
"use strict";
var gs_base_debug = {
copyright:"stewart allen <stewart@neuron.com> -- 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<f.length; i++) flags[f[i]] = (value || 1);
} else {
flags[f] = (value || 1);
}
}
function clear(f) {
if (Array.isArray(f)) {
for (var i = 0; i < f.length; i++) delete flags[f[i]];
} else {
delete flags[f];
}
}
function test(isSet, isNotSet) {
var i;
if (isNotSet) {
if (Array.isArray(isNotSet)) {
for (i=0; i<isNotSet.length; i++) {
if (flags[isNotSet[i]]) return null;
}
} else {
if (flags[isNotSet]) return null;
}
}
if (isSet) {
if (Array.isArray(isSet)) {
for (i=0; i<isSet.length; i++) {
if (!flags[isSet[i]]) return null;
}
} else {
if (!flags[isSet]) return null;
}
}
return view() || true;
}
function nextDebugColor() {
return debug_colors[next_debug_color++ % debug_colors.length];
}
/**
* render point array as discrete points
*
* @param {Point[]} points
* @param {number} [color]
* @param {number} [opacity]
* @param {number} [size]
* @returns {THREE.PointCloud}
*/
function points(points, color, opacity, size) {
view().points(points, color, size);
}
/**
*
* @param {Point[]} points
* @param {number} color
* @returns {THREE.Line}
*/
function lines(points, color) {
view().lines(points, color);
}
/**
*
* @param {Polygon} poly
* @param {number} color
* @param {boolean} [recurse]
* @returns {THREE.Object}
*/
function polygon(poly, color, recurse) {
return poly.render(view(), color, recurse);
}
/**
* debug polygon
*
* @param {Polygon} poly
* @param {number} [z]
* @param {Layer} [v]
* @param {boolean} [deep]
* @param {boolean} [creep]
*/
function xray(poly, z, v, deep, creep) {
var colors = [
0xaa0000,
0x0,
0xaaaa00,
0x444444,
0x00ff00,
0x888888,
0x00aaaa,
0x0,
0x0000aa,
0x444444,
0xaa00aa,
0x888888
],
cidx = 0,
point,
next,
layer = v || view();
if (typeof(z) === 'number') poly.setZ(z);
poly.forEachPoint(function(next) {
if (!point) return next = point;
if (creep) next.z = (z = z + 0.1);
layer.lines([point.clone(), next.clone()], colors[cidx++ % colors.length]);
point = next;
});
layer.lines([poly.last().clone(), poly.first().clone()], 0xffffff);
layer.points(poly.points, 0x000000, 0.1);
layer.points([poly.first()], 0xffffff, 0.4);
layer.points([poly.last()], 0x555555, 0.45);
if (deep && poly.inner) {
poly.inner.forEach(function(p) { xray(p, z, v, false, creep) });
}
}
/** ******************************************************************
* Connect to base
******************************************************************* */
base.debug = {
on : on,
enable : enable,
disable : disable,
history : history,
last : last,
get : get,
set : set,
clear : clear,
log : log,
trace : trace,
test : test,
view : null,
setView : setView,
color : nextDebugColor,
xray : xray,
points : points,
lines : lines,
polygon : polygon,
slice : null, // todo temp
};
})();

114
js/geo-line.js Normal file
View file

@ -0,0 +1,114 @@
"use strict";
var gs_base_line = {
copyright:"stewart allen <stewart@neuron.com> -- 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);
}
})();

645
js/geo-point.js Normal file
View file

@ -0,0 +1,645 @@
"use strict";
var gs_base_point = {
copyright:"stewart allen <stewart@neuron.com> -- 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<pl; i++) {
p1 = p[i];
p2 = p[(i+1)%pl];
if ((p1.y >= 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<poly.length; i++) {
if (point.isInPolygon(poly[i])) return true;
}
return false;
}
var holes = poly.inner;
if (point.inPolygon(poly) || point.nearPolygon(poly, CONF.precision_merge_sq)) {
for (i=0; holes && i < holes.length; i++) {
if (point.inPolygon(holes[i]) && !point.nearPolygon(holes[i], CONF.precision_merge_sq)) return false;
}
return true;
}
return false;
};
/**
* 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.isInPolygonOnly = function(poly) {
var point = this, i;
if (Array.isArray(poly)) {
for (i=0; i<poly.length; i++) {
if (point.isInPolygonOnly(poly[i])) return true;
}
return false;
}
var holes = poly.inner;
if (point.inPolygon(poly)) {
for (i=0; holes && i < holes.length; i++) {
if (point.inPolygon(holes[i])) return false;
}
return true;
}
return false;
};
/**
* checks if point is near polygon edge. distance is squared.
*
* @param {Polygon} poly
* @param {number} dist2
* @param {boolean} [inner] process inner polygons
* @returns {boolean}
*/
PoP.nearPolygon = function(poly, dist2, inner) {
// throw new Error("nearPolygon");
for (var i=0, p=poly.points, pl=p.length ; i<pl; i++) {
if (this.withinDist2(p[i], p[(i+1)%pl], dist2)) {
return true;
}
}
if (inner && poly.inner) {
for (var i=0; i<poly.inner.length; i++) {
if (this.nearPolygon(poly.inner[i], dist2)) return true;
}
}
return false;
};
/**
* returns true if point will not be trimmed later
*
* @param {Polygon} poly
* @param {number} offset
* @param {number} mindist2
* @returns {boolean}
*/
PoP.insideOffset = function(poly, offset, mindist2) {
return this.inPolygon(poly) === (offset > 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<points.length; i++) {
p = points[i];
if (p === this || p.del) continue;
d = this.distToSq2D(p);
if (d < max && d < mind) {
mind = d;
minp = p;
}
}
return minp;
};
/**
* @param {Point[]} points
* @return {number} average square dist to cloud of points
*/
PoP.averageDistTo = function(points) {
var sum = 0.0, count = 0, i;
for (i = 0; i < points.length; i++) {
if (points[i] != this) {
sum += this.distToSq2D(points[i]);
count++;
}
}
return sum / count;
};
/**
* dist to point in 2D
*
* @param {Point} p
* @returns {number}
*/
PoP.distTo2D = function(p) {
var dx = this.x - p.x,
dy = this.y - p.y;
return Math.sqrt(dx * dx + dy * dy);
};
/**
* square of distance in 2D
*
* @param {Point} p
* @returns {number}
*/
PoP.distToSq2D = function(p) {
var dx = this.x - p.x,
dy = this.y - p.y;
return dx * dx + dy * dy;
};
PoP.distTo3D = function(p) {
var dx = this.x - p.x,
dy = this.y - p.y,
dz = this.z - p.z;
return Math.sqrt(dx * dx + dy * dy + dz * dz);
};
/**
* square of distance in 3D
*
* @param {Point} p
* @returns {number}
*/
PoP.distToSq3D = function(p) {
var dx = this.x - p.x,
dy = this.y - p.y,
dz = this.z - p.z;
return dx * dx + dy * dy + dz * dz;
};
/**
* returns true if point is inside triangle described by three points
*
* @param {Point} a
* @param {Point} b
* @param {Point} c
* @returns {boolean}
*/
PoP.inTriangle = function(a, b, c) {
var as_x = this.x - a.x,
as_y = this.y - a.y,
s_ab = (b.x - a.x) * as_y - (b.y - a.y) * as_x > 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);
}
})();

1192
js/geo-polygon.js Normal file

File diff suppressed because it is too large Load diff

710
js/geo-polygons.js Normal file
View file

@ -0,0 +1,710 @@
"use strict";
var gs_base_polygons = {
copyright:"stewart allen <stewart@neuron.com> -- 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<setA.length; i++) {
if (setA[i].contains(poly)) return true;
}
return false;
}
function flatten(polys, to, crush) {
if (!to) to = [];
polys.forEach(function(poly) {
poly.flattenTo(to);
if (crush) poly.inner = null;
});
return to;
}
/**
* Diff two sets of polygons and return A-B, B-A.
* no polygons in a given set can overlap ... only between sets
*
* @param {Polygon[]} setA
* @param {Polygon[]} setB
* @param {Polygon[]} outA
* @param {Polygon[]} outB
* @param {number} [z]
* @param {number} [minArea]
* @returns {Polygon[]} out
*/
function subtract(setA, setB, outA, outB, z, minArea) {
var clib = self.ClipperLib,
ctyp = clib.ClipType,
ptyp = clib.PolyType,
cfil = clib.PolyFillType,
clip = new clib.Clipper(),
ctre = new clib.PolyTree(),
sp1 = toClipper(setA),
sp2 = toClipper(setB),
min = minArea || 0.1,
out = [];
function filter(from, to) {
from.forEach(function(poly) {
if (poly.area() >= 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<out.length; i++) {
if (!out[i]) continue;
for (j=i+1; j<out.length; j++) {
if (!out[j]) continue;
union = out[i].union(out[j]);
if (union) {
out[i] = null;
out[j] = null;
out.push(union);
continue outer;
}
}
}
for (i=0; i<out.length; i++) {
if (out[i]) uset.push(out[i]);
}
return uset;
}
/**
* @param {Polygon} poly clipping mask
* @returns {?Polygon[]}
*/
function doDiff(setA, setB, z) {
var clib = self.ClipperLib,
ctyp = clib.ClipType,
ptyp = clib.PolyType,
cfil = clib.PolyFillType,
clip = new clib.Clipper(),
ctre = new clib.PolyTree(),
sp1 = toClipper(setA),
sp2 = toClipper(setB);
clip.AddPaths(sp1, ptyp.ptSubject, true);
clip.AddPaths(sp2, ptyp.ptClip, true);
if (clip.Execute(ctyp.ctDifference, ctre, cfil.pftEvenOdd, cfil.pftEvenOdd)) {
return fromClipperTree(ctre, z);
} else {
return null;
}
};
/**
* Slice.doProjectedFills()
*
* @param {Polygon[]} setA source set
* @param {Polygon[]} setB mask set
* @returns {Polygon[]}
*/
function trimTo(setA, setB) {
// handle null/empty slices
if (setA === setB || setA === null || setB === null) return null;
var out = [], tmp;
UTIL.doCombinations(setA, setB, {}, function(a, b) {
if (tmp = a.mask(b)) {
out.appendAll(tmp);
}
});
return out;
}
/**
* @param {Polygon[]} polys
* @param {number} distance offset
* @param {number} [z] defaults to 0
* @param {Polygon[]} [out] optional collector
* @param {number} [count] offset passes (0 == until no space left)
* @param {number} [distance2] after first offset pass
* @param {Function} [collector] receives output of each pass
* @returns {Polygon[]} last offset
*/
function expand(polys, distance, z, out, count, distance2, collector) {
// prepare alignments for clipper lib
alignWindings(polys);
polys.forEach(function(poly) {
if (poly.inner) setWinding(poly.inner, !poly.isClockwise());
});
var fact = CONF.clipper,
clib = self.ClipperLib,
clip = clib.Clipper,
cpft = clib.PolyFillType,
cjnt = clib.JoinType,
cety = clib.EndType,
coff = new clib.ClipperOffset(),
ctre = new clib.PolyTree();
polys.forEach(function(poly) {
var clean = clip.CleanPolygons(poly.toClipper(), CONF.clipperClean);
var simple = clip.SimplifyPolygons(clean, cpft.pftNonZero);
coff.AddPaths(simple, cjnt.jtMiter, cety.etClosedPolygon);
});
coff.Execute(ctre, distance * fact);
polys = fromClipperTree(ctre, z);
if (out) out.appendAll(polys);
if (collector) collector(polys, count);
if ((count === 0 || count > 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<newouter.length; j++) {
trace2count(newouter[j], traces, offset, count - 1, depth + 1, last);
}
}
} else if (last) {
last.append(poly);
}
}
/**
* todo use clipper opne poly clipping?
*
* @param {Polygon[]} polys
* @param {number} angle (-90 to 90)
* @param {number} spacing
* @param {Polygon[]} [output]
* @param {numer} minLen
* @returns {Point[]} supplied output or new array
*/
function fillArea(polys, angle, spacing, output, minLen) {
var i = 1,
p0 = polys[0],
zpos = p0.getZ(),
bounds = p0.bounds.clone(),
raySlope;
// ensure angle is in the -90:90 range
angle = angle % 180;
while (angle > 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;
}
})();

99
js/geo-render.js Normal file
View file

@ -0,0 +1,99 @@
"use strict";
var gs_base_render = {
copyright:"stewart allen <stewart@neuron.com> -- 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);
}
})();

134
js/geo-slope.js Normal file
View file

@ -0,0 +1,134 @@
"use strict";
var gs_base_slope = {
copyright:"stewart allen <stewart@neuron.com> -- 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);
}
})();

394
js/geo.js Normal file
View file

@ -0,0 +1,394 @@
"use strict";
var gs_base = {
copyright:"stewart allen <stewart@neuron.com> -- 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
};
})();

223
js/kiri-codec.js Normal file
View file

@ -0,0 +1,223 @@
"use strict";
var gs_kiri_codec = {
copyright:"stewart allen <stewart@neuron.com> -- 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;
});
})();

164
js/kiri-db.js Normal file
View file

@ -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<store.listeners.length; i++) {
store.listeners[i](store.files);
}
}
DP.decimate = function(vertices, callback) {
if (vertices.length < 500000) return callback(vertices);
kiri.work.decimate(vertices, function(reply) {
callback(reply);
});
};
DP.setDeferredHandler = function(handler) {
this.deferredHandler = handler;
};
DP.putDeferred = function(name, mark) {
// triggers refresh callback
this.files[name] = {
deferred: mark
};
saveFileList(this);
};
/**
* @param {String} name
* @param {Float32Array} vertices
* @param {Function} [callback]
*/
DP.putFile = function(name, vertices, callback) {
var store = this;
store.db.put('file-'+name, vertices, function(ok) {
if (ok) {
store.files[name] = {
vertices: vertices.length/3,
updated: new Date().getTime()
};
saveFileList(store);
if (store.autodec) {
store.decimate(vertices, function(decimated) {
store.db.put('fdec-'+name, decimated);
if (callback) callback(decimated);
});
} else if (callback) callback(ok);
} else if (callback) callback(ok);
});
};
/**
* @param {String} name
* @param {Function} callback
*/
DP.getFile = function(name, callback) {
var store = this,
rec = store.files[name];
if (rec && rec.deferred) {
if (store.deferredHandler) return store.deferredHandler(rec.deferred, name, callback);
return callback();
}
if (!this.autodec) {
store.db.get('file-'+name, callback);
return;
}
this.db.get('fdec-'+name, function(vertices) {
if (vertices) {
callback(vertices);
} else {
store.db.get('file-'+name, function(vertices) {
if (vertices) {
store.decimate(vertices, function(decimated) {
store.db.put('fdec-'+name, decimated);
callback(vertices);
});
} else {
return callback();
}
});
}
});
};
/**
* @param {String} name
* @param {Function} callback
*/
DP.deleteFile = function(name, callback) {
var store = this;
if (store.files[name]) {
delete store.files[name];
store.db.remove('fdec-'+name);
store.db.remove('file-'+name, function(ok) {
saveFileList(store);
if (callback) callback(ok);
});
return;
}
if (callback) callback(false);
};
})();

1573
js/kiri-driver-cam.js Normal file

File diff suppressed because it is too large Load diff

462
js/kiri-driver-fdm.js Normal file
View file

@ -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<device.gcodePre.length; i++) {
append(constReplace(device.gcodePre[i], consts));
}
function moveTo(newpos, rate, comment) {
var o = ['G1'];
if (typeof newpos.x === 'number') {
pos.x = UTIL.round(newpos.x,decimals);
o.append(" X").append(pos.x);
}
if (typeof newpos.y === 'number') {
pos.y = UTIL.round(newpos.y,decimals);
o.append(" Y").append(pos.y);
}
if (typeof newpos.z === 'number') {
pos.z = UTIL.round(newpos.z,decimals);
o.append(" Z").append(pos.z);
}
if (typeof newpos.e === 'number') {
outputLength += newpos.e;
o.append(" E").append(UTIL.round(newpos.e,decimals));
}
if (rate && rate != pos.f) {
o.append(" F").append(Math.round(rate));
pos.f = rate
}
if (comment) {
o.append(" ; ").append(comment);
}
append(o.join(''));
}
// find total distance traveled by head as approx for progress
var allout = [],
totaldistance = 0;
layers.forEach(function(outs) { allout.appendAll(outs) });
allout.forEachPair(function (o1, o2) {
totaldistance += o1.point.distTo2D(o2.point);
},1);
while (layer < layers.length) {
append("; --- layer "+layer+" ---");
// second layer fan on
if (layer === 1 && fan_power) append(constReplace(fan_power,consts));
// first layer 50% underspeed
shortMMM = shortFact * maxPrintMMM;
printMMM = maxPrintMMM;
if (layer === 0) {
printMMM *= layer1speed;
shortMMM *= layer1speed;
}
path = layers[layer];
moveTo({z:zpos}, seekMMM);
zpos += zinc;
for (pidx=0; pidx<path.length; pidx++) {
out = path[pidx];
// if no point in output, it's a dwell command
if (!out.point) {
append("G4 P" + out.speed);
continue;
}
var x = out.point.x,
y = out.point.y;
if (process.outputInvertX) x = -x;
if (process.outputInvertY) y = -y;
if (offset) {
x += offset.x;
y += offset.y;
}
dist = lastp ? lastp.distTo2D(out.point) : 0;
distance += dist;
progress = Math.round((distance / totaldistance) * 100);
if (lastp && out.emit) {
var outMMM = printMMM,
emitMM = emitPerMM * out.emit * dist;
if (layer === 0) {
emitMM = emitPerMMLayer1 * out.emit * dist;
}
if (dist < shortDist) {
outMMM = shortMMM + ((outMMM - shortMMM) * (dist / shortDist));
} else {
// approximate compensation for acceleration & deceleration
time += (shortDist * 2) / outMMM / 10 * 60;
}
// print time
time += (dist / outMMM) * 60;
moveTo({x:x, y:y, e:emitMM}, outMMM);
emitted += emitMM;
} else {
if (retOver > 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<device.gcodePost.length; i++) {
append(constReplace(device.gcodePost[i], consts));
}
append("; --- filament used: "+UTIL.round(emitted,decimals)+"mm ---");
// force emit of buffer
append();
print.distance = emitted;
print.lines = lines;
print.bytes = bytes + lines - 1;
print.time = time;
return online ? null : output.join("\n");
};
})();

354
js/kiri-driver-laser.js Normal file
View file

@ -0,0 +1,354 @@
/** Copyright 2014-2017 Stewart Allen -- All Rights Reserved */
"use strict";
var gs_kiri_laser = exports;
(function() {
if (!self.kiri) self.kiri = { };
if (!self.kiri.driver) self.kiri.driver = { };
if (self.kiri.driver.LASER) return;
var KIRI = self.kiri,
BASE = self.base,
UTIL = BASE.util,
DBUG = BASE.debug,
LASER = KIRI.driver.LASER = { },
SLICER = KIRI.slicer,
newPoint = BASE.newPoint;
/**
* DRIVER SLICE CONTRACT
*
* @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)
*/
LASER.slice = function(settings, widget, onupdate, ondone) {
var proc = settings.process;
if (proc.laserSliceHeight < 0) {
DBUG.log("invalid slice height");
return ondone(false);
}
SLICER.sliceWidget(widget, {height: proc.laserSliceHeight}, function(slices) {
widget.slices = slices;
slices.forEach(function(slice, index) {
slice.doShells(1, -proc.laserOffset);
onupdate(0.80 + (index/slices.length) * 0.20);
});
ondone(true);
}, function(update) {
onupdate(0.0 + update * 0.80)
});
};
function sliceEmitObjects(print, slice, objects) {
var start = newPoint(0,0,0);
function laserOut(poly, object) {
if (!poly) return;
if (Array.isArray(poly)) {
poly.forEach(function(pi) {
laserOut(pi, object);
});
} else {
print.polyPrintPath(poly, start, object, 1);
}
}
slice.tops.forEach(function(top) {
var object = [];
laserOut(top.traces, object);
laserOut(top.innerTraces(), object);
objects.push(object);
});
};
/**
* DRIVER PRINT CONTRACT
*
* @param {Object} print state object
* @param {Function} update incremental callback
*/
LASER.printSetup = function(print, update) {
var widgets = print.widgets,
settings = print.settings,
device = settings.device,
process = settings.process,
mode = settings.mode,
output = print.output,
totalSlices = 0,
slices = 0;
// find max layers (for updates)
widgets.forEach(function(widget) {
totalSlices += widget.slices.length;
});
// emit objects from each slice into output array
widgets.forEach(function(widget) {
widget.slices.forEach(function(slice) {
sliceEmitObjects(print, slice, output);
update(slices++ / totalSlices);
});
});
// compute tile width / height
output.forEach(function(layerout) {
var min = {w:Infinity, h:Infinity}, max = {w:-Infinity, h:-Infinity}, p;
layerout.forEach(function(out) {
p = out.point;
min.w = Math.min(min.w, p.x);
max.w = Math.max(max.w, p.x);
min.h = Math.min(min.h, p.y);
max.h = Math.max(max.h, p.y);
});
layerout.w = max.w - min.w;
layerout.h = max.h - min.h;
});
// do object layout packing
var i, m, e,
MOTO = self.moto,
device = settings.device,
process = settings.process,
mp = [device.bedWidth, device.bedDepth],
ms = [mp[0] / 2, mp[1] / 2],
mi = mp[0] > 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('<?xml version="1.0" standalone="no"?>');
lines.push('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">');
lines.push('<svg width="'+width+'mm" height="'+height+'mm" viewBox="0 0 '+width+' '+height+'" xmlns="http://www.w3.org/2000/svg" version="1.1">');
},
function(poly) {
lines.push('<polyline points="'+poly.join(' ')+'" fill="none" stroke="blue" stroke-width="0.01mm" />');
},
function() {
lines.push("</svg>");
},
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');
};
})();

165
js/kiri-layer.js Normal file
View file

@ -0,0 +1,165 @@
"use strict";
var gs_kiri_layer = {
copyright:"stewart allen <stewart@neuron.com> -- 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;
}
})();

64
js/kiri-pack.js Normal file
View file

@ -0,0 +1,64 @@
"use strict";
var gs_kiri_pack = {
copyright:"stewart allen <stewart@neuron.com> -- 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;
})();

765
js/kiri-print.js Normal file
View file

@ -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<lines.length; i++) {
point = lines[i];
if (point.del) continue;
dist = SQRT(startPoint.distToSq2D(point));
if (dist < mindist) {
find = {i:i, p:point};
mindist = dist;
}
}
if (find) {
if (find.i % 2 === 0) {
p1 = find.p;
p2 = lines[find.i + 1];
} else {
p1 = find.p;
p2 = lines[find.i - 1];
}
p1.del = true;
p2.del = true;
preout.push(newOut(p1, SQRT(startPoint.distToSq2D(p1)) < minSeek ? seekMult : 0));
preout.push(newOut(p2, printMult));
startPoint = p2;
} else {
break;
}
}
// clear delete marks so we can re-print later
if (lines) lines.forEach(function(p) { p.del = false });
}
/**
* given array of polygons, emit them in next closest order
* @param {Array} array of Polygon or Polygon wrappers
* @param {Function} fn
* @param {Function} fnp convert 'next' object into a Polygon
*/
function outputOrderClosest(array, fn, fnp) {
array = array.slice();
var closest, find, next, poly;
for (;;) {
closest = null;
for (i=0; i<array.length; i++) {
next = array[i];
if (!next) continue;
poly = fnp ? fnp(next) : next;
find = poly.findClosestPointTo(startPoint);
if (!closest || find.distance < closest.distance) {
closest = find;
closest.i = i;
closest.next = next;
}
}
if (!closest) return;
array[closest.i] = null;
fn(closest.next);
}
}
var all = [].appendAll(slice.supports || []).appendAll(slice.tops || []);
outputOrderClosest(all || [], function(next) {
if (next instanceof Polygon) {
// support polygon
next.setZ(z);
outputTraces(next, extrude);
outputTraces(next.inner, extrude);
if (next.fills) {
next.fills.forEach(function(p) { p.z = z });
outputFills(next.fills, extrude);
}
} else {
// top object
outputTraces(next.traces, extrude);
outputTraces(next.innerTraces(), extrude);
outputFills(next.fill_lines);
outputSparse(next.fill_sparse);
}
}, function(obj) {
return obj instanceof Polygon ? obj : obj.poly;
});
// offset print points
for (i=0; i<preout.length; i++) {
preout[i].point = preout[i].point.add(offset);
}
// add offset points to total print
addPrintPoints(preout, output, origin);
return startPoint.add(offset);
};
/**
*
* @param {Output[]} input
* @param {Point[]} output
* @param {Point} [startPoint]
*/
function addPrintPoints(input, output, startPoint) {
if (startPoint && input.length > 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<poolsAbove.length; i++) {
const above = poolsAbove[i];
// can only add open polys to open polys
if (above.isOpen() && pool.isClosed()) {
// console.log({skip_open_above:above});
continue;
}
// if pool fits into smallest above pool, add it and break
if (polygonFitsIn(pool, above, 0.1)) {
above.poolsDown.push(pool);
return;
}
}
});
});
const emitPool = function(poolPoly) {
if (poolPoly.mark) return;
poolPoly.mark = true;
const polys = poolPoly.pool.slice().append(poolPoly);
startPoint = poly2polyEmit(polys, startPoint, emitter);
poolPoly.poolsDown.forEach(function(downPool) {
emitPool(downPool);
});
};
// from the top layer, iterate and descend through all connected pools
// pools are sorted smallest to largest. pools are polygons with an
// attached 'pool' array of polygons
layers.forEach(function(pools) {
pools.forEach(function(poolPoly) {
emitPool(poolPoly);
});
})
return startPoint;
}
/**
* flatten deeply nested polygons preserving inner arrays
*
* @param {Polygon | Polygon[]} poly or array to flatten
* @param {Polygon[]} to
* @returns {Polygon[]}
*/
function flattenPolygons(poly, to) {
if (!poly) return;
if (!to) to = [];
if (Array.isArray(poly)) {
poly.forEach(function(p) {
flattenPolygons(p, to);
})
} else {
to.push(poly);
flattenPolygons(poly.inner, to);
}
return to;
}
function polygonFitsIn(inside, outside, tolerance) {
return inside.isInside(outside, tolerance);
// return inside.area() <= outside.area() + tolerance &&
// (polygonWithinOffset(inside, outside, tolerance) || inside.isInside(outside, tolerance));
}
function polygonWithinOffset(poly1, poly2, offset) {
return polygonMinOffset(poly1, poly2, offset) <= offset;
}
function polygonMinOffset(poly1, poly2, offset) {
var mindist = Infinity;
poly1.forEachPoint(function(p) {
const nextdist = p.distToPolySegments(poly2, offset);
mindist = Math.min(mindist, nextdist);
// returning true terminates forEachPoint()
if (mindist <= offset) return true;
});
return mindist;
}
/**
* @param noz nozzle diameter
* @param fil filament diameter
* @param slice height in mm
* @returns filament extruded per mm
*/
function extrudePerMM(noz, fil, slice) {
return ((PI * UTIL.sqr(noz/2)) /
(PI * UTIL.sqr(fil/2))) *
(slice / noz);
}
function constOp(tok, consts, opch, op) {
var pos, v1, v2;
if ((pos = tok.indexOf(opch)) > 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;
}
}
})();

608
js/kiri-serial.js Normal file
View file

@ -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=<ms> (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("&raquo; "+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("&laquo; "+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 = '<option>select port</option>',
portModes = [];
ports = portList;
ports.forEach(function(port, index) {
portsHTML += '<option value='+index+'>' + port.Friendly + '</option>';
});
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('<','&lt').replace('>','&gt');
logBuffer.push(msg);
if (logBuffer.length > 100) logBuffer = logBuffer.slice(1);
}
logDiv.innerHTML = logBuffer.join('<br>');
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();
}
})();

1202
js/kiri-slice.js Normal file

File diff suppressed because it is too large Load diff

781
js/kiri-slicer.js Normal file
View file

@ -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<points.length; index++) {
p = points[index];
cached = pref[p.key];
// skip points already altered
if (cached) {
points[index] = cached;
continue;
}
cached = p.clone();
if (options.swapX) cached.swapXZ();
else if (options.swapY) cached.swapYZ();
cached.rekey();
pref[p.key] = cached;
points[index] = cached;
}
// update temp bounds from new points
btmp.setFromPoints(points);
for (var p, index=0; index<points.length; index++) {
p = points[index];
if (p.mod === 1) continue;
p.mod = 1;
p.z -= btmp.min.z;
}
// update temp bounds from points with altered Z
btmp.setFromPoints(points);
bounds = btmp;
}
var zMin = options.zmin || Math.floor(bounds.min.z),
zMax = options.zmax || Math.ceil(bounds.max.z),
zInc = options.height,
zOff = true ? zInc / 2 : 0,
zIndexes = [],
zList = {},
zFlat = {},
zScale,
zPos,
timeStart = time(),
slices = [],
zSum = 0.0,
buckets = [],
i, j = 0, k, p1, p2, p3, px,
CPRO = KIRI.driver.CAM.process;
// gather z-index stats
// these are used for auto-slicing in laser
// and to flats detection in CAM mode
for (i = 0; i < points.length;) {
p1 = points[i++];
p2 = points[i++];
p3 = points[i++];
zSum += (Math.abs(p1.z - p2.z) + Math.abs(p2.z - p3.z) + Math.abs(p3.z - p1.z));
// laser auto-detect z slice points
if (zInc === 0) {
zList[UTIL.round(p1.z,5)] = 1;
zList[UTIL.round(p2.z,5)] = 1;
zList[UTIL.round(p3.z,5)] = 1;
}
// cam auto-detect flats
if (options.cam) {
if (p1.z === p2.z && p2.z === p3.z && p1.z > 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<slices.length; i++) {
slices[i-1].up = slices[i];
slices[i].down = slices[i-1];
}
slices.slice_time = time() - timeStart;
// pass Slices array back to ondone function
ondone(slices);
/** ***** SLICING FUNCTIONS ***** */
/**
* given a point, append to the correct
* 'where' objec tarray (on, over or under)
*
* @param {Point} p
* @param {number} z offset
* @param {Obejct} where
*/
function checkUnderOverOn(p, z, where) {
var delta = p.z - z;
if (Math.abs(delta) < CONF.precision_slice_z) { // on
where.on.push(p);
} else if (delta < 0) { // under
where.under.push(p);
} else { // over
where.over.push(p);
}
}
/**
* Given a point over and under a z offset, calculate
* and return the intersection point on that z plane
*
* @param {Point} over
* @param {Point} under
* @param {number} z offset
* @returns {Point} intersection point
*/
function intersectPoints(over, under, z) {
var ip = [];
for (var i = 0; i < over.length; i++) {
for (var j = 0; j < under.length; j++) {
ip.push(over[i].intersectZ(under[j], z));
}
}
return ip;
}
/**
* Ensure points are unique with a cache/key algorithm
*/
function getCachedPoint(phash, p) {
var cached = phash[p.key];
if (!cached) {
phash[p.key] = p;
return p;
}
return cached;
}
/**
* Given two points and hints about their edges,
* return a new Line object with points sorted
* lexicographically by key. This allows for future
* line de-duplication and joins.
*
* @param {Object} phash
* @param {Point} p1
* @param {Point} p2
* @param {boolean} [coplanar]
* @param {boolean} [edge]
* @returns {Line}
*/
function makeZLine(phash, p1, p2, coplanar, edge) {
p1 = getCachedPoint(phash, p1);
p2 = getCachedPoint(phash, p2);
var line = newOrderedLine(p1,p2);
line.coplanar = coplanar || false;
line.edge = edge || false;
return line;
}
/**
* process a single z-slice on a single mesh and
* add to slices array
*
* @param {number} z
*/
function sliceZ(z) {
var phash = {},
lines = [],
slice = newSlice(z, options.view ? options.view.newGroup() : null),
bucket = bucketCount == 1 ? points : buckets[Math.floor(z * zScale)];
if (!bucket) return;
// iterate over matching buckets for this z offset
for (var i = 0; i < bucket.length;) {
p1 = bucket[i++];
p2 = bucket[i++];
p3 = bucket[i++];
var where = {under: [], over: [], on: []};
checkUnderOverOn(p1, z, where);
checkUnderOverOn(p2, z, where);
checkUnderOverOn(p3, z, where);
if (where.under.length === 3 || where.over.length === 3) {
// does not intersect
} else if (where.on.length === 2) {
// one side of triangle is on the Z plane
lines.push(makeZLine(phash, where.on[0], where.on[1], false, true));
} else if (where.on.length === 3) {
// triangle is coplanar with Z
//lines.push(makeZLine(phash, where.on[0], where.on[1], true));
//lines.push(makeZLine(phash, where.on[1], where.on[2], true));
//lines.push(makeZLine(phash, where.on[2], where.on[0], true));
} else if (where.under.length === 0 || where.over.length === 0) {
// does not intersect (but one point is on the plane)
} else {
// compute two point intersections and construct line
var line = intersectPoints(where.over, where.under, z);
if (line.length < 2 && where.on.length === 1) {
line.push(where.on[0]);
}
if (line.length === 2) {
lines.push(makeZLine(phash, line[0], line[1]));
} else {
console.log({msg: "invalid ips", line: line, where: where});
}
}
}
// allow empty slices in CAM swap mode (for topos w/ gaps)
if (lines.length == 0 && !(options.swapX || options.swapY)) return;
slice.index = slices.length;
slice.lines = removeDuplicateLines(lines);
// annotate slices with cam flats for finishing waterlines
if (options.cam) slice.hasFlats = zFlat[z] > 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<llen; idx++) { line = lines[idx];
line.p1 = line.p1.clone();
line.p2 = line.p2.clone();
}
for (idx=0; idx<llen; idx++) { line = lines[idx];
if (options.swapX) {
line.p1.swapXZ();
line.p2.swapXZ();
} else {
line.p1.swapYZ();
line.p2.swapYZ();
}
line.p1.move(move);
line.p2.move(move);
}
} else {
slice.tops.forEach(function(top) {
top.poly.swap(options.swapX, options.swapY);
top.poly.move(move);
top.poly.inner = null;
});
drape(slice, options.swapX, options.swapY);
}
}
slices.push(slice);
}
}
/**
* Given an array of input lines (line soup), find the path through
* joining line ends that encompasses the greatest area without self
* interesection. Eliminate used points and repeat. Unjoined lines
* are permitted and handled after all other cases are handled.
*
* @param {Line[]} input
* @param {number} [index]
* @returns {Array}
*/
function connectLines(input, index) {
// map points to all other points they're connected to
var DBUG = BASE.debug,
CONF = BASE.config,
pmap = {},
points = [],
output = [],
connect = [],
search = 1,
nextMod = 1,
debug = (BASE.debug.get('z-index') === index) && BASE.debug.get('connect'),
bridge = CONF.bridgeLineGapDistance,
p1, p2;
function cachedPoint(p) {
var cp = pmap[p.key];
if (cp) return cp;
points.push(p);
pmap[p.key] = p;
p.mod = nextMod++; // unique seq ID for points
p.toString = function() { return this.mod }; // point array concat
return p;
}
function addConnected(p1, p2) {
if (!p1.group) p1.group = [ p2 ];
else p1.group.push(p2);
}
function sliceAtTerm(path, term) {
var idx, len = path.length;
for (idx = 0; idx < len-1; idx++) {
if (path[idx] === term) {
return path.slice(idx);
}
}
return path;
}
/**
* using minimal recursion, follow points through connected lines
* to form candidate output paths.
*/
function findPathsMinRecurse(point, path, paths, from) {
var stack = [ ];
if (paths.length > 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<stack.length; i++) stack[i].del = false;
// stack.forEach(function(p) { p.del = false });
}
// emit a polygon if it can be cleaned and still have 2 or more points
function emit(poly) {
poly = poly.clean();
if (poly.length > 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<connect.length; i++) {
var array = connect[i],
last = array[array.length-1],
tmp, dist, j;
if (!bridge) {
emit(BASE.newPolygon().addPoints(array).setOpen());
continue;
}
if (array.delete) continue;
loop: for (var merged=0;;) {
var closest = { dist:Infinity };
for (j=i+1; j<connect.length; j++) {
tmp = connect[j];
if (tmp.delete) continue;
dist = last.distToSq2D(tmp[0]);
if (dist < closest.dist && dist <= bridge) {
closest = {
dist: dist,
array: tmp
}
}
dist = last.distToSq2D(tmp[tmp.length-1]);
if (dist < closest.dist && dist <= bridge) {
closest = {
dist: dist,
array: tmp,
reverse: true
}
}
}
if (tmp = closest.array) {
if (closest.reverse) tmp.reverse();
tmp.delete = true;
array.appendAll(tmp);
last = array[array.length-1];
merged++;
// tail meets head (closed)
if (close(array[0], last)) {
emit(BASE.newPolygon().addPoints(array));
break loop;
}
} else {
// no more closest polys (open set)
emit(BASE.newPolygon().addPoints(array));
break loop;
}
}
}
return output;
}
/**
* eliminate duplicate lines and interior-only lines (coplanar)
*
* lines are sorted using lexicographic point keys such that
* they are comparable even if their points are reversed. hinting
* for deletion, co-planar and suspect shared edge is detectable at
* this time.
*
* @param {Line[]} lines
* @returns {Line[]}
*/
function removeDuplicateLines(lines) {
var output = [],
tmplines = [],
points = [],
pmap = {};
function cachePoint(p) {
var cp = pmap[p.key];
if (cp) return cp;
points.push(p);
pmap[p.key] = p;
return p;
}
function addLinesToPoint(point, line) {
cachePoint(point);
if (!point.group) point.group = [ line ];
else point.group.push(line);
}
// mark duplicates for deletion preserving edges
lines.sort(function (l1, l2) {
if (l1.key === l2.key) {
l1.del = !l1.edge;
l2.del = !l2.edge;
return 0;
}
return l1.key < l2.key ? -1 : 1;
});
// associate points with their lines, cull deleted
lines.forEach(function(line) {
if (!line.del) {
tmplines.push(line);
addLinesToPoint(line.p1, line);
addLinesToPoint(line.p2, line);
}
});
// merge collinear lines
points.forEach(function(point) {
if (point.group.length != 2) return;
var l1 = point.group[0],
l2 = point.group[1];
if (l1.isCollinear(l2)) {
l1.del = true;
l2.del = true;
// find new endpoints that are not shared point
var p1 = l1.p1 != point ? l1.p1 : l1.p2,
p2 = l2.p1 != point ? l2.p1 : l2.p2,
newline = base.newOrderedLine(p1,p2);
// remove deleted lines from associated points
p1.group.remove(l1);
p1.group.remove(l2);
p2.group.remove(l1);
p2.group.remove(l2);
// associate new line with points
p1.group.push(newline);
p2.group.push(newline);
// add new line to lines array
newline.edge = l1.edge || l2.edge;
tmplines.push(newline);
}
});
// mark duplicates for deletion
// but preserve one if it's an edge
tmplines.sort(function (l1, l2) {
if (l1.key === l2.key) {
l1.del = true;
l2.del = !l2.edge;
return 0;
}
return l1.key < l2.key ? -1 : 1;
});
// create new line array culling deleted
tmplines.forEach(function(line) {
if (!line.del) {
output.push(line);
line.p1.group = null;
line.p2.group = null;
}
});
return output;
}
})();

647
js/kiri-widget.js Normal file
View file

@ -0,0 +1,647 @@
"use strict";
var gs_kiri_widget = {
copyright:"stewart allen <stewart@neuron.com> -- 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<oldpoints; ) {
var p1 = parr[i++],
p2 = parr[i++],
p3 = parr[i++];
lines.push( {p1:p1, p2:p2, d:SQRT(p1.distToSq3D(p2))} );
lines.push( {p1:p1, p2:p3, d:SQRT(p1.distToSq3D(p3))} );
lines.push( {p1:p2, p2:p3, d:SQRT(p2.distToSq3D(p3))} );
}
// sort by ascending line length
lines.sort(function(a,b) {
return a.d - b.d
});
// create offset mid-points
for (i=0; i<lines.length; i++) {
line = lines[i];
if (line.d >= 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<oldpoints; ) {
var p1 = parr[i++],
p2 = parr[i++],
p3 = parr[i++];
// drop facets with two offset points
if (p1.op && p1.op === p2.op) continue;
if (p1.op && p1.op === p3.op) continue;
if (p2.op && p2.op === p3.op) continue;
// otherwise emit altered facet
points[newpoints++] = p1.op || p1;
points[newpoints++] = p2.op || p2;
points[newpoints++] = p3.op || p3;
}
parr = points.slice(0,newpoints);
oldpoints = newpoints;
}
if (passes) DBUG.log({
before: array.length / 3,
after: parr.length,
unique: unique,
decimations: passes,
time: (time() - t)
});
return parr;
};
Widget.pointsToVertices = function(points) {
var vertices = new Float32Array(points.length * 3),
i = 0, vi = 0;
while (i < points.length) {
vertices[vi++] = points[i].x;
vertices[vi++] = points[i].y;
vertices[vi++] = points[i++].z;
}
return vertices;
};
/** ******************************************************************
* Widget Prototype Functions
******************************************************************* */
WP.saveToCatalog = function(filename) {
var widget = this;
var time = UTIL.time();
KIRI.catalog.putFile(filename, this.getGeoVertices(), function(vertices) {
if (vertices && vertices.length) {
console.log("saving decimated mesh ["+vertices.length+"] time ["+(UTIL.time()-time)+"]");
widget.loadVertices(vertices);
}
});
return this;
};
WP.saveState = function(ondone) {
var widget = this;
KIRI.odb.put('ws-save-'+this.id, {geo:widget.getGeoVertices(), orient:widget.orient}, function(result) {
widget.saved = time();
if (ondone) ondone();
});
};
WP.encodeSlices = function() {
var encoded = [];
if (this.slices) this.slices.forEach(function(slice) {
encoded.push(slice.encode());
});
return encoded;
};
WP.decodeSlices = function(encoded) {
this.slices = KIRI.codec.decode(encoded, { mesh:this.mesh });
};
/**
*
* @param {Float32Array} vertices
* @returns {Widget}
*/
WP.loadVertices = function(vertices) {
if (this.mesh) {
this.mesh.geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
this.mesh.geometry.computeFaceNormals();
this.mesh.geometry.computeVertexNormals();
this.points = null;
return this;
} else {
var geometry = new THREE.BufferGeometry();
geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
return this.loadGeometry(geometry);
}
};
/**
* @param {THREE.Geometry} geometry
* @returns {Widget}
*/
WP.loadGeometry = function(geometry) {
var mesh = new THREE.Mesh(
geometry,
new THREE.MeshPhongMaterial({
color: 0xffff00,
specular: 0x181818,
shininess: 100,
transparent: true,
opacity: solid_opacity
})
);
// fix invalid normals
geometry.computeFaceNormals();
geometry.computeVertexNormals();
// to fix mirroring of normals not working as expected
mesh.material.side = THREE.DoubleSide;
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.widget = this;
this.mesh = mesh;
// invalidates points cache (like any scale/rotation)
this.center();
return this;
};
/**
* @param {Point[]} points
* @returns {Widget}
*/
WP.setPoints = function(points) {
this.points = points || null;
return this;
};
/**
* remove slice data and their views
*/
WP.clearSlices = function() {
var slices = this.slices,
mesh = this.mesh;
if (slices) {
slices.forEach(function(slice) {
mesh.remove(slice.view);
});
this.slices = null;
}
};
/**
* @param {number} color
*/
WP.setColor = function(color) {
var material = this.mesh.material;
material.color.set(color);
};
/**
* @param {number} value
*/
WP.setOpacity = function(value) {
var mesh = this.mesh;
if (value <= 0.0) {
mesh.material.transparent = solid_opacity < 1.0;
mesh.material.opacity = solid_opacity;
mesh.material.visible = false;
} else if (UTIL.inRange(value, 0.0, solid_opacity)) {
mesh.material.transparent = value < 1.0;
mesh.material.opacity = value;
mesh.material.visible = true;
}
};
/**
* center geometry bottom (on platform) at 0,0,0
*/
WP.center = function() {
var i = 0,
mesh = this.mesh,
geo = mesh.geometry,
bb = mesh.getBoundingBox(true),
bm = bb.min.clone(),
bM = bb.max.clone(),
bd = bM.sub(bm).multiplyScalar(0.5),
gap = geo.attributes.position,
pa = gap.array;
// center point array on 0,0,0
for ( ; i < pa.length; i += 3) {
pa[i ] -= bm.x + bd.x;
pa[i + 1] -= bm.y + bd.y;
pa[i + 2] -= bm.z;
}
gap.needsUpdate = true;
bb = mesh.getBoundingBox(true);
// for use with the packer
mesh.w = (bb.max.x - bb.min.x);
mesh.h = (bb.max.y - bb.min.y);
mesh.d = (bb.max.z - bb.min.z);
// invalidate cached points
this.points = null;
this.modified = true;
};
/**
* moves top of widget to given Z
* used in CAM mode
*
* @param {number} z position
*/
WP.setTopZ = function(z) {
var mesh = this.mesh,
pos = this.orient.pos;
if (z) {
pos.z = mesh.getBoundingBox().max.z - z;
mesh.position.z = -pos.z - 0.01;
} else {
pos.z = 0;
mesh.position.z = 0;
}
this.modified = true;
}
/**
*
* @param {number} x
* @param {number} y
* @param {number} z
* @param {boolean} abs
*/
WP.move = function(x, y, z, abs) {
var mesh = this.mesh,
pos = this.orient.pos;
// do not allow moves in pure slice view
if (!mesh.material.visible) return;
if (abs) {
mesh.position.set(x,y,z);
pos.x = (x || 0);
pos.y = (y || 0);
pos.z = (z || 0);
} else {
mesh.position.x += ( x || 0);
mesh.position.y += ( y || 0);
mesh.position.z += (-z || 0);
pos.x += (x || 0);
pos.y += (y || 0);
pos.z += (z || 0);
}
if (x || y || z) this.modified = true;
};
/**
*
* @param {number} x
* @param {number} y
* @param {number} z
*/
WP.scale = function(x, y, z) {
var mesh = this.mesh,
scale = this.orient.scale;
this.setWireframe(false);
this.clearSlices();
mesh.geometry.applyMatrix(new THREE.Matrix4().makeScale(x, y, z));
this.center();
scale.x *= (x || 1.0);
scale.y *= (y || 1.0);
scale.z *= (z || 1.0);
};
/**
*
* @param {number} x
* @param {number} y
* @param {number} z
*/
WP.rotate = function(x, y, z) {
this.setWireframe(false);
this.clearSlices();
this.mesh.geometry.applyMatrix(new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(x || 0, y || 0, z || 0)));
this.center();
var rot = this.orient.rot;
rot.x += (x || 0);
rot.y += (y || 0);
rot.z += (z || 0);
};
WP.mirror = function() {
this.setWireframe(false);
this.clearSlices();
var i,
o = this.orient,
geo = this.mesh.geometry,
at = geo.attributes,
pa = at.position.array,
nm = at.normal.array;
for (i = 0 ; i < pa.length; i += 3) {
pa[i] = -pa[i];
nm[i] = -nm[i];
}
geo.computeFaceNormals();
geo.computeVertexNormals();
this.center();
o.mirror = !o.mirror;
};
WP.getGeoVertices = function() {
return this.mesh.geometry.getAttribute('position').array;
};
WP.getPoints = function() {
if (!this.points) {
// convert and cache points from geometry vertices
this.points = Widget.verticesToPoints(this.getGeoVertices());
}
return this.points;
};
WP.getBoundingBox = function(refresh) {
// if (this.mesh) return this.mesh.getBoundingBox();
if (!this.bounds || refresh) {
this.bounds = new THREE.Box3();
this.bounds.setFromPoints(this.getPoints());
}
return this.bounds;
};
WP.isModified = function() {
return this.modified;
};
/**
* processes points into facets, then into slices
*
* once upon a time there were multiple slicers. this was the fastest in most cases.
* lines are added to all the buckets they cross. then buckets are processed in order.
* buckets are contiguous ranges of z slicers. the advantage of this method is that
* as long as a large percentage of lines do not cross large z distances, this reduces
* the number of lines each slice has to consider thus improving speed.
*
* @params {Object} settings
* @params {Function} [ondone]
* @params {Function} [onupdate]
* @params {boolean} [remote]
*/
WP.slice = function(settings, ondone, onupdate, remote) {
var widget = this,
startTime = UTIL.time();
widget.settings = settings;
widget.cancel = false;
onupdate(0.0001, "slicing");
if (remote) {
KIRI.work.slice(settings, this, function (reply) {
if (reply.update) {
onupdate(reply.update, reply.updateStatus);
}
if (reply.send_start) widget.xfer = {start: reply.send_start};
if (reply.topo) widget.topo = reply.topo;
if (reply.stats) widget.stats = reply.stats;
if (reply.send_end) widget.stats.load_time = widget.xfer.start - reply.send_end;
if (reply.slices) { widget.clearSlices(); widget.slices = [] };
if (reply.slice) widget.slices.push(KIRI.codec.decode(reply.slice, {mesh:widget.mesh}));
if (reply.done) {
ondone(true);
widget.modified = false;
}
});
} else {
widget.clearSlices();
var catchdone = function() {
onupdate(1.0, "transferring");
widget.stats.slice_time = UTIL.time() - startTime;
widget.modified = false;
ondone(true);
};
var catchupdate = function(progress, message) {
onupdate(progress, message);
return widget.cancel ? 42 : 0;
};
var driver = null;
switch (settings.mode) {
case 'LASER': driver = LASER; break;
case 'FDM': driver = FDM; break;
case 'CAM': driver = CAM; break;
}
if (driver) {
driver.slice(settings, widget, catchupdate, catchdone);
} else {
DBUG.log('invalid mode: '+settings.mode);
ondone(false);
}
}
};
WP.getCamBounds = function(settings) {
var bounds = this.getBoundingBox().clone();
bounds.max.z += settings.process.camZTopOffset;
return bounds;
};
/**
* render all slice and processed data
* @param {number} renderMode
* @param {boolean} cam mode
*/
WP.render = function(renderMode, cam) {
var slices = this.slices;
if (!slices) return;
// render outline
slices.forEach(function(s) { s.renderOutline(renderMode) });
// render shells
slices.forEach(function(s) { s.renderShells(renderMode) });
// render diff
if (!cam) slices.forEach(function(s) { s.renderDiff() });
// render solid fill (include solid flats/bridges)
slices.forEach(function(s) { s.renderSolidFill() });
// render solid fill outlines
if (!cam) slices.forEach(function(s) { s.renderSolidOutlines() });
// render sparse fill
if (!cam) slices.forEach(function(s) { s.renderSparseFill() });
// render supports
if (!cam) slices.forEach(function(s) { s.renderSupport() });
};
WP.hideSlices = function() {
var showing = false;
if (this.slices) this.slices.forEach(function(slice) {
showing = showing || slice.view.visible;
slice.view.visible = false;
});
return showing;
};
WP.toggleWireframe = function (color, opacity) {
this.setWireframe(!this.wire, color, opacity);
};
WP.setWireframe = function(set, color, opacity) {
var mesh = this.mesh,
widget = this;
if (this.wire) {
mesh.remove(this.wire);
this.wire = null;
this.setOpacity(solid_opacity);
this.hideSlices();
}
if (set) {
widget.wire = base.render.wireframe(mesh, this.getPoints(), color);
widget.setOpacity(opacity);
}
};
})();

317
js/kiri-work.js Normal file
View file

@ -0,0 +1,317 @@
if (self.window) {
if (!self.kiri) self.kiri = {};
var loc = self.location,
host = loc.hostname,
port = loc.port,
proto = loc.protocol,
pre = host.indexOf(".space") > 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);
};
}

3291
js/kiri.js Normal file

File diff suppressed because it is too large Load diff

7
js/license.js Normal file
View file

@ -0,0 +1,7 @@
var exports = {
COPYRIGHT:"Copyright (C) 2014-2017 Stewart Allen <stewart@neuron.com> - 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;

2136
js/meta.js Normal file

File diff suppressed because it is too large Load diff

77
js/moto-ajax.js Normal file
View file

@ -0,0 +1,77 @@
"use strict";
var gs_moto_ajax = {
copyright:"stewart allen <stewart@neuron.com> -- 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();
}
};
})();

566
js/moto-ctrl.js Normal file
View file

@ -0,0 +1,566 @@
"use strict";
var gs_moto_ctrl = {
copyright:"stewart allen <stewart@neuron.com> -- 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<choices.length; i++) {
var v = choices[i];
if (isValue(v)) return v;
}
return null;
}
this.setZoom = function(reverse, speed) {
scope.reverseZoom = reverse;
scope.zoomSpeed = speed || 1.0;
};
this.setPosition = function(set) {
thetaSet = firstValue([set.left, set.theta, thetaSet]);
phiSet = firstValue([set.up, set.phi, phiSet]);
if (set.panX) this.target.x = set.panX;
if (set.panY) this.target.y = set.panY;
if (set.panZ) this.target.z = set.panZ;
if (set.scale) scale = set.scale;
};
this.getPosition = function(scaled) {
var t = this.target,
pos = { left:theta, up:phi, panX:t.x, panY:t.y, panZ:t.z, scale:scaled ? scaleSave : 1 };
return pos;
};
this.dollyIn = function (dollyScale) {
if (dollyScale === undefined) {
dollyScale = getZoomScale();
}
scale *= dollyScale;
};
this.dollyOut = function (dollyScale) {
if (dollyScale === undefined) {
dollyScale = getZoomScale();
}
scale /= dollyScale;
};
this.update = function () {
var position = this.object.position;
offset.copy(position).sub(this.target);
// rotate offset to "y-axis-is-up" space
offset.applyQuaternion(quat);
// angle from z-axis around y-axis
theta = isValue(thetaSet) ? thetaSet : Math.atan2(offset.x, offset.z);
// angle from y-axis
phi = isValue(phiSet) ? phiSet : Math.atan2(Math.sqrt(offset.x * offset.x + offset.z * offset.z), offset.y);
theta += thetaDelta;
phi += phiDelta;
// restrict theta to be between desired limits
theta = Math.max(this.minAzimuthAngle, Math.min(this.maxAzimuthAngle, theta));
// restrict phi to be between desired limits
phi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, phi));
// restrict phi to be betwee EPS and PI-EPS
phi = Math.max(EPS, Math.min(Math.PI - EPS, phi));
var radius = offset.length() * (mode === MODE.PERSPECTIVE ? scale : 1);
// restrict radius to be between desired limits
radius = Math.max(this.minDistance, Math.min(this.maxDistance, radius));
// move target to panned location
this.target.add(pan);
offset.x = radius * Math.sin(phi) * Math.sin(theta);
offset.y = radius * Math.cos(phi);
offset.z = radius * Math.sin(phi) * Math.cos(theta);
// rotate offset back to "camera-up-vector-is-up" space
offset.applyQuaternion(quatInverse);
position.copy(this.target).add(offset);
this.object.lookAt(this.target);
thetaDelta = 0;
thetaSet = null;
phiDelta = 0;
phiSet = null;
pan.set(0, 0, 0);
scaleSave *= scale;
if (mode === MODE.ORTHOGRAPHIC) {
scope.object.zoom = 1/scale;
scope.object.updateProjectionMatrix();
} else {
scale = 1;
}
// update condition is:
// min(camera displacement, camera rotation in radians)^2 > 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);

211
js/moto-db.js Normal file
View file

@ -0,0 +1,211 @@
"use strict";
var gs_moto_db = {
copyright:"stewart allen <stewart@neuron.com> -- 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();
};
})();

56
js/moto-kv.js Normal file
View file

@ -0,0 +1,56 @@
"use strict";
var gs_moto_kv = {
copyright:"stewart allen <stewart@neuron.com> -- 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];
}
}
};
})();

239
js/moto-load-obj.js Normal file
View file

@ -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;
}
})();

250
js/moto-load-stl.js Normal file
View file

@ -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<vertices.length; i++) vToFloat32[i] = vertices[i];
for (i=0; i<normals.length; i++) nToFloat32[i] = normals[i];
this.vertices = vToFloat32;
this.normals = nToFloat32;
return vToFloat32;
};
SP.convertToString = function (buf) {
if (typeof buf !== "string") {
var array_buffer = new Uint8Array(buf);
var str = '';
for (var i = 0; i < buf.byteLength; i++) {
str += String.fromCharCode(array_buffer[i]);
}
return str;
} else {
return buf;
}
};
SP.convertToBinary = function (buf) {
if (typeof buf === "string") {
var array_buffer = new Uint8Array(buf.length);
for (var i = 0; i < buf.length; i++) {
array_buffer[i] = buf.charCodeAt(i) & 0xff;
}
return array_buffer.buffer || array_buffer;
} else {
return buf;
}
};
})();

622
js/moto-space.js Normal file
View file

@ -0,0 +1,622 @@
"use strict";
var gs_moto_space = {
copyright:"stewart allen <stewart@neuron.com> -- 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<pairs.length; i += 2) {
addEventListener(el, pairs[i], pairs[i+1]);
}
}
function onEnterKey(el, fn) {
if (Array.isArray(el)) {
for (var i=0; i<el.length; i += 2) onEnterKey(el[i], el[i+1]);
return;
}
addEventListener(el, 'keyup', function(event) {
if (event.keyCode === 13) fn(event);
});
}
function addLight(x,y,z,i) {
var l = new THREE.PointLight(0xffffff, i, 0);
l.position.set(x,y,z);
SCENE.add(l);
return l;
}
function updatePlatformPosition() {
platform.position.y = -platform.scale.z/2 - platformZOff;
// platform.position.y = -(platform.scale.z / 2 + platformZOff);
requestRefresh();
}
function setPlatformSize(width, depth, height) {
platform.scale.set(width || 300, depth || 175, height || 5);
viewControl.maxDistance = Math.max(width,depth) * 2;
updatePlatformPosition();
var y = Math.max(width, height) * 1;
light1.position.set( width, y, depth);
light2.position.set(-width, y, -depth);
light4.position.set( width, light4.position.y, -depth);
light5.position.set(-width, light5.position.y, depth);
}
function setPlatformSizeUpdateGrid(width, depth, height) {
setPlatformSize(width, depth, height);
setGrid(gridUnitMajor, gridUnitMinor);
}
function setPlatformColor(color) {
platform.material.color.set(color);
requestRefresh();
}
function setGrid(unitMajor, unitMinor, colorMajor, colorMinor) {
if (gridView) Space.scene.remove(gridView);
if (!unitMajor) return;
gridView = new THREE.Group();
gridUnitMajor = unitMajor;
gridUnitMinor = unitMinor;
var x = platform.scale.x,
y = platform.scale.y,
z = platform.scale.z,
xr = ROUND(x / unitMajor) * unitMajor,
yr = ROUND(y / unitMajor) * unitMajor,
xo = Math.ceil(xr / 2),
yo = Math.ceil(yr / 2),
w = x / 2,
h = y / 2,
d = z / 2,
zp = -d - platformZOff + gridZOff,
majors = [], minors = unitMinor ? [] : null, i;
for (i = -xo; i <= xo; i++) {
if (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<int.length; i++) {
if (!trackInt && int[i].object === trackTo) {
trackInt = int[i];
} else if (!selectInt && selection.contains(int[i].object)) {
selectInt = int[i];
}
}
if (trackInt && selectInt) {
mouseDragPoint = trackInt.point.clone();
mouseDragStart = mouseDragPoint;
viewControl.enabled = false;
mouseDownSelect(selectInt, event);
}
}
}
if (platformClick) {
var vis = platform.visible;
platform.visible = true;
int = intersect([platform], false);
platform.visible = vis;
platformClickAt = int && int.length > 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;
})();

323
js/moto-ui.js Normal file
View file

@ -0,0 +1,323 @@
"use strict";
var gs_moto_ui = {
copyright:"stewart allen <stewart@neuron.com> -- 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<arrayOfArrays.length; i++) {
array.push(newRowTable(arrayOfArrays[i]));
}
return array;
}
function newRowTable(array) {
var div = DOC.createElement('div');
div.setAttribute("class", "tablerow");
array.forEach(function(c) {
div.appendChild(c);
});
return div;
}
function newRow(children, options) {
var row = addCollapsableElement((options && options.noadd) ? null : lastDiv);
if (children) children.forEach(function (c) { row.appendChild(c) });
addModeControls(row, options);
return row;
}
function addCollapsableElement(parent) {
var row = DOC.createElement('div');
if (parent) parent.appendChild(row);
if (lastGroup) row.setAttribute("class", lastGroup);
return row;
}
})();

1006
js/web-server.js Normal file

File diff suppressed because it is too large Load diff

8
license.md Normal file
View file

@ -0,0 +1,8 @@
Copyright (C) Stewart Allen <stewart@neuron.com> 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.

112
notes.md Normal file
View file

@ -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

31
package.json Normal file
View file

@ -0,0 +1,31 @@
{
"name": "gridspace",
"version": "1.0.0",
"description": "gridspace 3d slice & modeling tools",
"author": "Stewart Allen <stewart.allen@gmail.com>",
"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"
}
}

21
readme.md Normal file
View file

@ -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

BIN
web/kiri/favicon-mobile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

BIN
web/kiri/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -0,0 +1 @@
Any.Generic.Grbl

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -0,0 +1 @@
Any.Generic.Marlin

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

27
web/kiri/help-kiri.html Normal file
View file

@ -0,0 +1,27 @@
<button id="help-close">X</button>
<center><b>Quick Start</b></center>
<p>
<a href="#" title="Kiri-e is the Japanese art of papercutting"><b>Kiri:Moto</b></a>
is an extensible, multi-purpose slicing<br>and visualization engine that produces output for:
<li><label>CAM</label> : 3 axis CNC toolpaths
<li><label>FDM</label> : GCode for 3D printers
<li><label>LASER</label> : DXG / SVG cut paths
<li><label><a target="_onshape" href="https://appstore.onshape.com/apps/CAM/EAAEWYIOMQKBENEMYW2N7MF253CT4WYL6SUJGEY=/description">Onshape</a></label> : Directly Integrated
<li><label><a target="_thingiverse" href="http://www.thingiverse.com/apps/kirimoto/">Thingiverse</a></label> : Thing App
</p>
<p>
<b><a>Files</a></b> are loaded via drag/drop or 'load' menu
<li>Loaded files are cached in the lower-left menu
<li>Check device mode (FDM, CAM, Laser)
<li>Select a GCode profile for your device
<li>Check slicing parameters in right menu
<li>Slice can be found under 'function'
</p>
<p>Refer to the <a target="_help" href="https://wiki.grid.space/wiki/Kiri:Moto">Grid.Space Wiki</a> for more information</p>
<p id="kiri-version"></p>

251
web/kiri/index.html Normal file
View file

@ -0,0 +1,251 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<meta name="copyright" content="stewart allen [stewart@neuron.com]">
<meta name="description" content="3d slice modeler, gcode and CNC toolpath generator">
<meta name="keywords" content="3d slicer,slicer,3d slicing,cnc,cam,toolpaths,toolpath generation,kiri,kirimoto,kiri:moto,gcode" />
<meta name="author" content="Stewart Allen">
<meta name="robots" content="noindex, nofollow">
<meta property="og:description" content="Kiri:Moto is a unique multi-modal 3D slicer that runs entirely in browser and creates output for your favorite maker tools: 3D Printers, CNC Mills and Laser Cutters. Advanted layer view helps debug prints ahead of time.">
<meta property="og:title" content="Cloud-based Slicer for Makers">
<meta property="og:type" content="website">
<meta property="og:url" content="https://grid.space/kiri">
<meta property="og:image" content="https://grid.space/img/logo_km_og.png">
<title>Kiri:Moto</title>
<link rel="icon" href="/kiri/favicon.ico">
<link rel="apple-touch-icon" href="/kiri/favicon-mobile.png">
<link rel="stylesheet" type="text/css" href="/moto/style.css">
<link rel="stylesheet" type="text/css" href="/kiri/style.css">
</head>
<body>
<script>
/* because TWEEN complains */
module = {};
</script>
<!--{kiri}-->
<!-- app title home page links -->
<div id="appid">
<span><a href="#" id="kiri">Kiri:Moto</a> by <a href="/" target="home">Grid.Space</a></span>
</div>
<!-- left control menu -->
<div id="control-left" class="control"><div id="assets"></div></div>
<!-- right control menu -->
<div id="control-right" class="control"><div id="control"></div></div>
<!-- SPJS gcode sender -->
<div id="sender">
<div id="sender-bar">
<button id="sender-spjs">?</button>
<button id="sender-close">X</button>
</div>
<span>direct device control</span>
<div class="sender-sel">
<table>
<tr>
<th colspan="2" class="flow-row">
<input class="flow-grow" id="sender-host" size="30" spellcheck="false" placeholder="host:port of SPJS server" />
</th>
<td> <button id="sender-connect">connect</button> </td>
</tr>
<tr>
<th class="flow-row">
<select class="flow-grow" id="sender-port" disabled="true"><option>select port</option></select> &nbsp;
<select id="sender-mode" disabled="true">
<option id="mode-option">set mode</option>
<option value="grbl">grbl</option>
<option value="tinyg">tinyg</option>
</select>
</th>
<td> <button id="sender-port-close">close</button> </td>
</tr>
</table>
</div>
<div class="sender-sel"> <div id="sender-log"></div> </div>
<div class="sender-sel" id="sender-cc">
<table>
<tr>
<th>
<label>status</label>
<input id="sender-status" size="5" disabled="true" />
<label>send</label>
<input id="sender-command" size="30" />
</th>
</tr>
<tr>
<th class="flow-row" id="sender-quick">
<button id="sender-ctrlx">soft reset</button> &nbsp;
<button id="sender-hold">feed hold</button> &nbsp;
<button id="sender-resume">feed resume</button>
</td>
</tr>
</table>
</div>
<div class="sender-sel flow-row flow-space-between" id="sender-pad">
<div>
<table>
<tr><th>
<button id="sjx-">X-</button>
<button id="sjx+">X+</button>
</th></tr>
<tr><th>
<button id="sjy-">Y-</button>
<button id="sjy+">Y+</button>
</th></tr>
<tr><th>
<button id="sjz-">Z-</button>
<button id="sjz+">Z+</button>
</th></tr>
</table>
</div><div>
<table>
<tr><th> <label>jog</label><input id="sender-jog" size="4" value="1" title="jog distance in mm" /><label>mm</label> </th></tr>
<tr><th> <button id="sender-set-zero">zero out axes</button> </th></tr>
<tr><th> <button id="sender-goto-zero">goto zero axis</button> </th></tr>
</table>
</div><div>
<table>
<tr><th><label>X</label> <input id="saxi" disabled="true" size="9" /> <input id="srxi" disabled="true" size="9" /></th></tr>
<tr><th><label>Y</label> <input id="sayi" disabled="true" size="9" /> <input id="sryi" disabled="true" size="9" /></th></tr>
<tr><th><label>Z</label> <input id="sazi" disabled="true" size="9" /> <input id="srzi" disabled="true" size="9" /></th></tr>
</table>
</div>
</div>
<div class="sender-sel" id="sender-control">
<table>
<tr><td>
<label>gcode control</label>
<div id="sender-control-buttons">
<button id="sender-gc-send">send</button>
<button id="sender-gc-pause">pause</button>
<button id="sender-gc-abort">abort</button>
: <button id="sender-gc-runbox">runbox</button>
</div>
</td></tr>
<tr><td class="flow-row flow-space-between">
<div><label>queue</label><input id="sender-gc-queue" disabled="true" size="9" /> </div>
<div><label>sent</label><input id="sender-gc-sent" disabled="true" size="9" /> </div>
<div><label>total</label><input id="sender-gc-lines" disabled="true" size="9" /> </div>
</td></tr>
</table>
</div>
</div>
<!-- all modal dialogs -->
<div id="modal">
<div id="help"></div>
<!-- changeable print dialog -->
<div id="print"></div>
<!-- hidden file input loader -->
<input id="load-file" type="file" name="loadme" style="display:none" accept=".stl,.gcode,.nc"/>
</div>
<!-- 3js -->
<div id="container"></div>
<!-- progress bar -->
<div id="loading"><div id="progress"><span id="prostatus">status</span></div></div>
<!-- file load catalog -->
<div id="catalog" class="dialog">
<div id="catalogBody" class="flow-col">
<span id="localCache">local cache</span>
<div id="catalogList"></div>
</div>
</div>
<!-- settings load dialog -->
<div id="settings" class="dialog">
<div id="settingsBody" class="flow-col">
<span id="settingsCache">saved settings</span>
<div id="settingsList"></div>
</div>
</div>
<!-- GCode filter editor -->
<div id="devices" class="dialog">
<div id="devices-body" class="flow-col">
<div id="device-labels" class="flow-row">
<span>device</span>
<span>settings</span>
</div>
<div id="device-cols" class="flow-row">
<div id="device-list" class="flow-col">
<select id="device-select" size="15"></select>
</div>
<div id="device-info">
<div id="device"></div>
</div>
</div>
<div id="device-action" class="flow-row">
<button id="device-add">+</button>
<button id="device-del">-</button>
<span id="device-spacer"></span>
<button id="device-save">save</button>
<button id="device-close">close</button>
</div>
</div>
</div>
<!-- CAM tools dialog -->
<div id="tools" class="dialog">
<div id="tools-body" class="flow-col">
<div id="tool-labels" class="flow-row">
<span>tools</span>
<span>details</span>
</div>
<div id="tool-cols" class="flow-row">
<div id="tool-list" class="flow-col">
<select id="tool-select" size="15"></select>
</div>
<div id="tool-info" class="flow-col">
<div class="flow-row"><label>type</label>
<select id="tool-type">
<option value="endmill" selected>endmill</option>
<option value="ballmill">ballmill</option>
</select>
</div>
<div class="flow-row"><label>name</label><input id="tool-name" size=8></input></div>
<div class="flow-row"><label>tool #</label><input id="tool-num" size=5></input></div>
<div class="flow-row"><label>metric</label><input id="tool-metric" type="checkbox"></input></div>
<div class="grouphead">flute</div>
<div class="flow-row" title="flute diameter"><label>diameter</label><input id="tool-fdiam" size=5></input></div>
<div class="flow-row" title="flute length"><label>length</label><input id="tool-flen" size=5></input></div>
<div class="grouphead">shaft</div>
<div class="flow-row" title="shaft diameter"><label>diameter</label><input id="tool-sdiam" size=5></input></div>
<div class="flow-row" title="shaft length"><label>length</label><input id="tool-slen" size=5></input></div>
</div>
</div>
<div id="tool-action" class="flow-row">
<button id="tool-add">+</button>
<button id="tool-del">-</button>
<span id="tool-spacer"></span>
<button id="tools-save">save</button>
<button id="tools-close">done</button>
</div>
</div>
</div>
<!-- selection info -->
<div id="selection">
<span>
<label class="label">width:</label><label id="sel_width" class="value">mm</label>
<label class="label">depth:</label><label id="sel_depth" class="value">mm</label>
<label class="label">height:</label><label id="sel_height" class="value">mm</label>
&nbsp;&nbsp;
<label class="label">scale</label>
<label class="label">x</label><input id="scale_x" size="3" value="1"/>
<label class="label">y</label><input id="scale_y" size="3" value="1"/>
<label class="label">z</label><input id="scale_z" size="3" value="1"/>
<label class="label">uniform</label><input type="checkbox" id="scale_uni" checked>
&nbsp;&nbsp;
<label class="label">rotate</label>
<button id="x+" title="hold SHIFT to rotate 5 degrees">x+</button><button id="x-" title="hold SHIFT to rotate 5 degrees">x-</button>
<button id="y+" title="hold SHIFT to rotate 5 degrees">y+</button><button id="y-" title="hold SHIFT to rotate 5 degrees">y-</button>
<button id="z+" title="hold SHIFT to rotate 5 degrees">z+</button><button id="z-" title="hold SHIFT to rotate 5 degrees">z-</button>
</span>
</div>
<!-- layer slider -->
<div id="layer-view" class="flow-row">
<span>
<button id="layer-toggle">layers</button>
<div id="layers"></div>
<input id="layer-id" size="4">
<input id="layer-slider" type="range">
<button id="layer-range"> section</button>
<input id="layer-span" size="4" value="1">
</span>
</div>
</body>
</html>

View file

@ -0,0 +1,47 @@
<button id="print-close">X</button>
<span id="print-title">output options</span>
<div>
<table id="print-info">
<tr><th>file name</th><td><input id="print-filename" size="30" spellcheck="false" /></td></tr>
<tr><th>file size (bytes)</th><td><input id="print-filesize" size="10" disabled="true" /></td></tr>
<tr id="mill-info"><th>estimated mill time (h:m:s)</th><td><input id="mill-time" size="10" disabled="true" /></td></tr>
</table>
</div>
<div id="print-filament-row">
<table>
<tr><th>filament used (mm)</th><td><input id="print-filament" size="10" disabled="true" /></td></tr>
<tr><th>filament density (g/cm^3)</th><td><input id="print-density" size="10" value="1.25" /></td></tr>
<tr><th>printed weight (g)</th><td><input id="print-weight" size="10" disabled="true" /></td></tr>
<tr><th>estimated print time (h:m:s)</th><td><input id="print-time" size="10" disabled="true" /></td></tr>
</table>
</div>
<div class="print-sel">
<table><tr><th>
<button id="print-download">download gcode file</button>
<button id="print-serial">use gcode sender</button>
</th></tr></table>
</div>
<div class="print-sel" id="send-to-octoprint">
<table>
<tr><th colspan="2"><button id="print-octoprint">send to octoprint</button></th></tr>
<tr id="ophost"><th>host:port</th><td><input id="octo-host" size="35" placeholder="http(s)://host:port" /></td></tr>
<tr id="opapik"><th>api key</th><td><input id="octo-apik" size="35" /></td></tr>
<!--<tr><th>options</th><td><input type="checkbox" id="octo-print">start print</td></tr>-->
<tr id="ophint"><td colspan="2"><span id="octo-hint"><a href="http://docs.octoprint.org/en/master/api/general.html?highlight=cors#cross-origin-requests" target="_help">CORS support</a> must be enabled in API settings</span></td></tr>
</table>
</div>
<div class="print-sel" id="send-to-gridprint">
<table>
<tr><th colspan="2"><button id="print-gridprint">send to grid:print</button></th></tr>
<tr><th>host:port</th><td><input id="grid-host" size="35" placeholder="http(s)://host:port" /></td></tr>
<tr><th>api key</th><td><input id="grid-apik" size="35" /></td></tr>
<tr><th>target</th><td><input id="grid-target" size="35" /></td></tr>
<tr><td colspan="2"><span id="grid-hint">setup <a href="https://github.com/stewartoallen/grid-print" target="_help">grid:print</a> for your network</span></td></tr>
</table>
</div>

View file

@ -0,0 +1,18 @@
<button id="print-close">X</button>
<span>output options</span>
<div>
<table id="print-info">
<tr><th>file name</th><td><input id="print-filename" size="30" spellcheck="false" /></td></tr>
<tr><th>segments</th><td><input id="print-lines" size="10" disabled="true" /></td></tr>
</table>
</div>
<div class="print-sel">
<table>
<tr><th><button id="print-svg">download as svg</button></th></tr>
<tr><th><button id="print-dxf">download as dxf</button></th></tr>
<tr><th><button id="print-lg">download as gcode</button></th></tr>
</table>
</div>

View file

@ -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;
}

578
web/kiri/style.css Normal file
View file

@ -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);
}

BIN
web/meta/favicon-mobile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
web/meta/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

26
web/meta/index.html Normal file
View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<meta name="copyright" content="stewart allen [stewart@neuron.com]">
<meta name="description" content="3d block modeler">
<meta name="keywords" content="gridspace,meta,metamoto,3d,block,builder,modeler,modeling">
<meta name="author" content="Stewart Allen">
<meta name="robots" content="noindex, nofollow">
<title>Meta:Moto</title>
<link rel="icon" href="/meta/favicon.ico">
<link rel="apple-touch-icon" href="/meta/favicon-mobile.png">
<link rel="stylesheet" type="text/css" href="/moto/style.css">
<link rel="stylesheet" type="text/css" href="/meta/style.css">
</head>
<body>
<script>
/* because TWEEN complains */
module = {};
</script>
<!--{meta}-->
<div id="control-left"><div id="assets"><div class="title"><a href="/"><span>grid</span>:<span>space</span></a></div></div></div>
<div id="control-right"><div id="control"><div class="title"><a href="/meta/"><span>meta</span>:<span>moto</span></a></div></div></div>
<div id="container"></div>
</body>
</html>

56
web/meta/style.css Normal file
View file

@ -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;
}

277
web/moto/style.css Normal file
View file

@ -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
}

BIN
web/obj/cube.stl Normal file

Binary file not shown.

BIN
web/obj/meta-corner-1.stl Normal file

Binary file not shown.

BIN
web/obj/meta-corner-2.stl Normal file

Binary file not shown.

BIN
web/obj/meta-corner-3.stl Normal file

Binary file not shown.

BIN
web/obj/meta-corner-4.stl Normal file

Binary file not shown.