merge development branch
This commit is contained in:
commit
9b0fa9dedf
128 changed files with 3943 additions and 4261 deletions
139
src/add/array.js
Normal file
139
src/add/array.js
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
var AP = Array.prototype;
|
||||
|
||||
/** ******************************************************************
|
||||
* array prototype helpers
|
||||
******************************************************************* */
|
||||
|
||||
if (!AP.flat) {
|
||||
AP.flat = function() {
|
||||
return [].concat.apply([], this);
|
||||
};
|
||||
}
|
||||
|
||||
AP.equals = function(arr) {
|
||||
if (!arr) return false;
|
||||
if (arr.length !== this.length) return false;
|
||||
for (let i=0; i<this.length; i++) {
|
||||
if (arr[i] !== this[i]) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
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('');
|
||||
};
|
||||
|
||||
/** ******************************************************************
|
||||
* Object static helpers
|
||||
******************************************************************* */
|
||||
|
||||
Object.clone = function(o) {
|
||||
return o ? JSON.parse(JSON.stringify(o)) : o;
|
||||
};
|
||||
|
||||
Math.bound = function(val,min,max) {
|
||||
return Math.max(min,Math.min(max,val));
|
||||
};
|
||||
|
||||
})();
|
||||
116
src/add/class.js
Normal file
116
src/add/class.js
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
class DataWriter {
|
||||
constructor(view, pos) {
|
||||
this.view = view;
|
||||
this.pos = pos || 0;
|
||||
}
|
||||
|
||||
skip(v) {
|
||||
let p = this.pos;
|
||||
this.pos += v;
|
||||
return p;
|
||||
}
|
||||
|
||||
writeU8(v) {
|
||||
try {
|
||||
this.view.setUint8(this.pos, v);
|
||||
return this.skip(1);
|
||||
} catch (err) {
|
||||
console.log({pos:this.pos, err})
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
writeU16(v,le) {
|
||||
this.view.setUint16(this.pos, v, le);
|
||||
return this.skip(2);
|
||||
}
|
||||
|
||||
writeU32(v,le) {
|
||||
this.view.setUint32(this.pos, v, le);
|
||||
return this.skip(4);
|
||||
}
|
||||
|
||||
writeI32(v,le) {
|
||||
this.view.setInt32(this.pos, v, le);
|
||||
return this.skip(4);
|
||||
}
|
||||
|
||||
writeUB64(v,le) {
|
||||
this.view.setBigUint64(this.pos, v, le);
|
||||
return this.skip(8);
|
||||
}
|
||||
|
||||
writeB64(v,le) {
|
||||
this.view.setBigInt64(this.pos, v, le);
|
||||
return this.skip(8);
|
||||
}
|
||||
|
||||
writeF32(v,le) {
|
||||
this.view.setFloat32(this.pos, v, le);
|
||||
return this.skip(4);
|
||||
}
|
||||
|
||||
writeF64(v,le) {
|
||||
this.view.setFloat64(this.pos, v, le);
|
||||
return this.skip(8);
|
||||
}
|
||||
}
|
||||
|
||||
class DataReader {
|
||||
constructor(view, pos) {
|
||||
this.view = view;
|
||||
this.pos = pos || 0;
|
||||
}
|
||||
|
||||
skip(v) {
|
||||
let p = this.pos;
|
||||
this.pos += v;
|
||||
return p;
|
||||
}
|
||||
|
||||
readU8() {
|
||||
return this.view.getUint8(this.pos++);
|
||||
}
|
||||
|
||||
readU16(le) {
|
||||
let v = this.view.getUint16(this.pos, le);
|
||||
this.pos += 2;
|
||||
return v;
|
||||
}
|
||||
|
||||
readU32(le) {
|
||||
let v = this.view.getUint32(this.pos, le);
|
||||
this.pos += 4;
|
||||
return v;
|
||||
}
|
||||
|
||||
readI32(le) {
|
||||
let v = this.view.getInt32(this.pos, le);
|
||||
this.pos += 4;
|
||||
return v;
|
||||
}
|
||||
|
||||
readB64(le) {
|
||||
let v = this.view.getBigInt64(this.pos, le);
|
||||
this.pos += 8;
|
||||
return v;
|
||||
}
|
||||
|
||||
readUB64(le) {
|
||||
let v = this.view.getBigUint64(this.pos, le);
|
||||
this.pos += 8;
|
||||
return v;
|
||||
}
|
||||
|
||||
readF32(le) {
|
||||
let v = this.view.getFloat32(this.pos, le);
|
||||
this.pos += 4;
|
||||
return v;
|
||||
}
|
||||
|
||||
readF64(le) {
|
||||
let v = this.view.getFloat64(this.pos, le);
|
||||
this.pos += 8;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
121
src/add/three.js
Normal file
121
src/add/three.js
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */
|
||||
|
||||
"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);
|
||||
};
|
||||
|
||||
// fast mirror of vertices given axis array offset
|
||||
// array elements are repeating [x,y,z,x,y,z,...]
|
||||
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;
|
||||
};
|
||||
|
||||
// center geometry on x,y,z coordinates (defaults to 0,0,0)
|
||||
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;
|
||||
};
|
||||
|
||||
// return cached or refreshed (when update = true) bounding box
|
||||
GP.getBoundingBox = function(update) {
|
||||
if (update || !this.boundingBox) this.computeBoundingBox();
|
||||
this.boundingBox.dim = this.boundingBox.max.clone().sub(this.boundingBox.min);
|
||||
return this.boundingBox;
|
||||
};
|
||||
|
||||
// uniformly scale any mesh to a max x/y/z dim of 'unit' (defaults to 1)
|
||||
GP.unitScale = function(unit) {
|
||||
var bbox = this.getBoundingBox().clone(),
|
||||
scale = (unit || 1) / Math.max(
|
||||
bbox.max.x - bbox.min.x,
|
||||
bbox.max.y - bbox.min.y,
|
||||
bbox.max.z - bbox.min.z
|
||||
);
|
||||
this.applyMatrix4(new THREE.Matrix4().makeScale(scale,scale,scale));
|
||||
// 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.setAttribute('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 = [];
|
||||
};
|
||||
|
||||
})();
|
||||
22
src/dev/cam/Any.Generic.Grbl
Normal file
22
src/dev/cam/Any.Generic.Grbl
Normal 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
|
||||
}
|
||||
}
|
||||
24
src/dev/cam/Any.Generic.LinuxCNC
Normal file
24
src/dev/cam/Any.Generic.LinuxCNC
Normal 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
|
||||
}
|
||||
}
|
||||
1
src/dev/cam/Any.Generic.TinyG
Symbolic link
1
src/dev/cam/Any.Generic.TinyG
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
Any.Generic.Grbl
|
||||
22
src/dev/cam/Carbide3D.Nomad.883Pro
Normal file
22
src/dev/cam/Carbide3D.Nomad.883Pro
Normal 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
|
||||
}
|
||||
}
|
||||
22
src/dev/cam/Carbide3D.Shapeoko.3
Normal file
22
src/dev/cam/Carbide3D.Shapeoko.3
Normal 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
|
||||
}
|
||||
}
|
||||
22
src/dev/cam/Carbide3D.Shapeoko.XL
Normal file
22
src/dev/cam/Carbide3D.Shapeoko.XL
Normal 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
|
||||
}
|
||||
}
|
||||
22
src/dev/cam/Carbide3D.Shapeoko.XXL
Normal file
22
src/dev/cam/Carbide3D.Shapeoko.XXL
Normal 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
|
||||
}
|
||||
}
|
||||
24
src/dev/cam/Inventables.Carvey.Any
Normal file
24
src/dev/cam/Inventables.Carvey.Any
Normal 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
|
||||
}
|
||||
}
|
||||
22
src/dev/cam/Inventables.XCarve.1000mm
Normal file
22
src/dev/cam/Inventables.XCarve.1000mm
Normal 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
|
||||
}
|
||||
}
|
||||
22
src/dev/cam/Inventables.XCarve.500mm
Normal file
22
src/dev/cam/Inventables.XCarve.500mm
Normal 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
|
||||
}
|
||||
}
|
||||
24
src/dev/cam/Sienci.LongMill
Normal file
24
src/dev/cam/Sienci.LongMill
Normal 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)",
|
||||
"G0 F6000 ; default rapid move speed",
|
||||
"G1 F1500 ; default cutting speed"
|
||||
],
|
||||
"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": 760,
|
||||
"bed_depth": 760
|
||||
}
|
||||
}
|
||||
22
src/dev/cam/Sienci.Mill.One
Normal file
22
src/dev/cam/Sienci.Mill.One
Normal 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
|
||||
}
|
||||
}
|
||||
22
src/dev/cam/Techno.Davinci.CNC
Normal file
22
src/dev/cam/Techno.Davinci.CNC
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"file-ext": "ncd",
|
||||
"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": 250,
|
||||
"bed_depth": 300
|
||||
}
|
||||
}
|
||||
48
src/dev/fdm/Any.Generic.Marlin
Normal file
48
src/dev/fdm/Any.Generic.Marlin
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"pre":[
|
||||
"M104 S{temp} T{tool} ; set extruder temperature",
|
||||
"M140 S{bed_temp} T{tool} ; set bed temperature",
|
||||
"G90 ; set absolute positioning mode",
|
||||
"M83 ; set relative positioning for extruder",
|
||||
"M107 ; turn off filament cooling fan",
|
||||
"G28 ; home axes",
|
||||
"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} T{tool} ; wait for bed to reach target temp",
|
||||
"M109 S{temp} T{tool} ; 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 T{tool} ; turn off right extruder",
|
||||
"M140 S0 T{tool} ; turn off bed",
|
||||
"G1 Z{z_max} F1200 ; drop bed",
|
||||
"G28 X0 Y0 ; home XY axes",
|
||||
"M84 ; disable stepper motors"
|
||||
],
|
||||
"extruders":[
|
||||
{
|
||||
"nozzle": 0.4,
|
||||
"filament": 1.75,
|
||||
"offset_x": 0,
|
||||
"offset_y": 0,
|
||||
"select": [ "T0" ]
|
||||
},{
|
||||
"nozzle": 0.4,
|
||||
"filament": 1.75,
|
||||
"offset_x": 30,
|
||||
"offset_y": 0,
|
||||
"select": [ "T1" ]
|
||||
}
|
||||
],
|
||||
"cmd":{
|
||||
"fan_power": "M106 S{fan_speed}"
|
||||
},
|
||||
"settings":{
|
||||
"origin_center": false,
|
||||
"extrude_abs": false
|
||||
}
|
||||
}
|
||||
1
src/dev/fdm/Any.Generic.Sailfish
Symbolic link
1
src/dev/fdm/Any.Generic.Sailfish
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
Any.Generic.Marlin
|
||||
51
src/dev/fdm/Anycubic.i3.Mega
Normal file
51
src/dev/fdm/Anycubic.i3.Mega
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"pre":[
|
||||
"M105 ; update temperature",
|
||||
"M104 S{temp} T{tool}",
|
||||
"M140 S{bed_temp} T{tool}",
|
||||
"M109 S{temp} T{tool}",
|
||||
"M190 S{bed_temp} T{tool}",
|
||||
"M105 ; update temperature",
|
||||
"G21 ; set metric values",
|
||||
"G90 ; set absolute positioning",
|
||||
"M82 ; set extruder to absolute mode",
|
||||
"M107 ; turn off fan",
|
||||
"G28 X0 Y0 ; move X/Y to min",
|
||||
"G28 Z0 ; move Z to min",
|
||||
"G1 Z15.0 F100 ; move the platform down 15mm",
|
||||
"G92 E0 ; zero extruder",
|
||||
"G1 F200 E3 ; extrude 3mm of feed stock",
|
||||
"G92 E0 ; zero extruder",
|
||||
"G1 F100 ; set default feed rate",
|
||||
"G0 Y20 ; wipe move to Y20",
|
||||
"G92 E0 ; zero extruder",
|
||||
"M117 Printing..."
|
||||
],
|
||||
"post":[
|
||||
"M107 ; turn off fan",
|
||||
"M104 S0 T{tool} ; turn off extruder",
|
||||
"M140 S0 T{tool} ; turn off bed",
|
||||
"G1 Z{z_max} F1200 ; drop bed",
|
||||
"G28 X0 Y0 ; home XY axes",
|
||||
"M84 ; disable stepper motors"
|
||||
],
|
||||
"extruders":[
|
||||
{
|
||||
"nozzle": 0.4,
|
||||
"filament": 1.75,
|
||||
"offset_x": 0,
|
||||
"offset_y": 0,
|
||||
"select": [ "T0" ]
|
||||
}
|
||||
],
|
||||
"cmd":{
|
||||
"fan_power": "M106 S{fan_speed}"
|
||||
},
|
||||
"settings":{
|
||||
"origin_center": false,
|
||||
"extrude_abs": true,
|
||||
"bed_width": 210,
|
||||
"bed_depth": 210,
|
||||
"build_height": 205
|
||||
}
|
||||
}
|
||||
27
src/dev/fdm/Atom.v2
Normal file
27
src/dev/fdm/Atom.v2
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"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,
|
||||
"bed_circle": true,
|
||||
"build_height": 320
|
||||
}
|
||||
}
|
||||
43
src/dev/fdm/Creality.Ender.3
Normal file
43
src/dev/fdm/Creality.Ender.3
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"pre":[
|
||||
"M107 ; turn off filament cooling fan",
|
||||
"G90 ; set absolute positioning mode",
|
||||
"M82 ; set absolute positioning for extruder",
|
||||
"M104 S{temp} T{tool} ; set extruder temperature",
|
||||
"M140 S{bed_temp} T{tool} ; set bed temperature",
|
||||
"G28 ; home axes",
|
||||
"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} T{tool} ; wait for bed to reach target temp",
|
||||
"M109 S{temp} T{tool} ; wait for extruder to reach target temp",
|
||||
"G92 E0 ; zero the extruded",
|
||||
"G1 F225 ; set feed speed"
|
||||
],
|
||||
"post":[
|
||||
"M107 ; turn off filament cooling fan",
|
||||
"M104 S0 T{tool} ; turn off right extruder",
|
||||
"M140 S0 T{tool} ; turn off bed",
|
||||
"G1 X0 Y300 F1200 ; end move",
|
||||
"M84 ; disable stepper motors"
|
||||
],
|
||||
"extruders":[
|
||||
{
|
||||
"nozzle": 0.4,
|
||||
"filament": 1.75,
|
||||
"offset_x": 0,
|
||||
"offset_y": 0,
|
||||
"select": [ "T0" ]
|
||||
}
|
||||
],
|
||||
"cmd":{
|
||||
"fan_power": "M106 S{fan_speed}"
|
||||
},
|
||||
"settings":{
|
||||
"origin_center": false,
|
||||
"extrude_abs": true,
|
||||
"bed_width": 220,
|
||||
"bed_depth": 220,
|
||||
"build_height": 300
|
||||
}
|
||||
}
|
||||
40
src/dev/fdm/Creality.Ender.5
Normal file
40
src/dev/fdm/Creality.Ender.5
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"pre":[
|
||||
"G28 X ; home X",
|
||||
"M107 ; turn off filament cooling fan",
|
||||
"M104 S{temp} T{tool} ; set extruder temperature",
|
||||
"M140 S{bed_temp} T{tool} ; set bed temperature",
|
||||
"M109 S{temp} T{tool} ; wait for extruder to reach target temp",
|
||||
"M190 S{bed_temp} T{tool} ; wait for bed to reach target temp",
|
||||
"G90 ; set absolute positioning mode",
|
||||
"M82 ; set absolute positioning for extruder",
|
||||
"G28 ; home axes",
|
||||
"G1 X220 Y220 Z0.25 F180 ; move XY to 0,0 and Z 0.25mm over bed"
|
||||
],
|
||||
"post":[
|
||||
"M107 ; turn off filament cooling fan",
|
||||
"M104 S0 T{tool} ; turn off right extruder",
|
||||
"M140 S0 T{tool} ; turn off bed",
|
||||
"G1 X0 Y300 F1200 ; end move",
|
||||
"M84 ; disable stepper motors"
|
||||
],
|
||||
"extruders":[
|
||||
{
|
||||
"nozzle": 0.4,
|
||||
"filament": 1.75,
|
||||
"offset_x": 0,
|
||||
"offset_y": 0,
|
||||
"select": [ "T0" ]
|
||||
}
|
||||
],
|
||||
"cmd":{
|
||||
"fan_power": "M106 S{fan_speed}"
|
||||
},
|
||||
"settings":{
|
||||
"origin_center": false,
|
||||
"extrude_abs": true,
|
||||
"bed_width": 220,
|
||||
"bed_depth": 220,
|
||||
"build_height": 300
|
||||
}
|
||||
}
|
||||
37
src/dev/fdm/Cubicon.Single+
Normal file
37
src/dev/fdm/Cubicon.Single+
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"pre":[
|
||||
"M104 S{temp} T{tool} ; set extruder temperature",
|
||||
"M140 S{bed_temp} T{tool} ; set bed temperature",
|
||||
"M190 S{bed_temp} T{tool} ; set bed temperature",
|
||||
"M109 S{temp} T{tool} ; set extruder temperature",
|
||||
"M901 S35 ; set chamber temperature to 35 (standard)",
|
||||
"G90 ; set absolute positioning mode",
|
||||
"M82 ; set absolute positioning for extruder",
|
||||
"M107 ; turn off filament cooling fan",
|
||||
"G28 ; home axes",
|
||||
"G29 ; level bed",
|
||||
"G92 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 extruder",
|
||||
"G1 E15 F200 ; purge 15mm from extruder",
|
||||
"G92 E0 ; zero the extruded"
|
||||
],
|
||||
"post":[
|
||||
"M107 ; turn off filament cooling fan",
|
||||
"M104 S0 T{tool} ; turn off right extruder",
|
||||
"M140 S0 ; turn off bed",
|
||||
"M904 ; park extruder",
|
||||
"M84 ; disable stepper motors"
|
||||
],
|
||||
"cmd":{
|
||||
"fan_power": "M106 S{fan_speed}"
|
||||
},
|
||||
"file-ext": "hvs",
|
||||
"settings":{
|
||||
"origin_center": false,
|
||||
"extrude_abs": true,
|
||||
"bed_width": 240,
|
||||
"bed_depth": 190,
|
||||
"build_height": 200
|
||||
}
|
||||
}
|
||||
32
src/dev/fdm/Cubicon.Style
Normal file
32
src/dev/fdm/Cubicon.Style
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"pre":[
|
||||
"T{tool} ; select extruder #0",
|
||||
"M104 S{temp} ; set nozzle temp",
|
||||
"M140 S{bed_temp} T{tool} ; set bed temperature",
|
||||
"M109 S{temp} ; wait for nozzle to heat",
|
||||
"S210 T{tool} ; wait for heating?",
|
||||
"M82 ; absolute extrusion mode",
|
||||
"G28 ; home",
|
||||
"G1 Z15 F6000 ; lower platform 15mm",
|
||||
"G92 E0 ; zero out extruder position",
|
||||
"G1 F200 E3 ; prime the extruder 3mm"
|
||||
],
|
||||
"post":[
|
||||
"M107 ; turn off filament cooling fan",
|
||||
"M104 S0 T{tool} ; turn off right extruder",
|
||||
"M140 S0 ; turn off bed",
|
||||
"M904 ; park extruder",
|
||||
"M84 ; disable stepper motors"
|
||||
],
|
||||
"cmd":{
|
||||
"fan_power": "M106 S{fan_speed}"
|
||||
},
|
||||
"file-ext": "hvs",
|
||||
"settings":{
|
||||
"origin_center": false,
|
||||
"extrude_abs": true,
|
||||
"bed_width": 150,
|
||||
"bed_depth": 150,
|
||||
"build_height": 150
|
||||
}
|
||||
}
|
||||
31
src/dev/fdm/DeltaMaker.2T
Normal file
31
src/dev/fdm/DeltaMaker.2T
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"pre":[
|
||||
"M104 S{temp} T{tool} ; 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} T{tool} ; 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 T{tool} ; turn off right extruder",
|
||||
"M140 S0 T{tool} ; 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,
|
||||
"bed_circle": true,
|
||||
"build_height": 480
|
||||
}
|
||||
}
|
||||
31
src/dev/fdm/DeltaMaker.Original
Normal file
31
src/dev/fdm/DeltaMaker.Original
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"pre":[
|
||||
"M104 S{temp} T{tool} ; 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} T{tool} ; 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 T{tool} ; turn off right extruder",
|
||||
"M140 S0 T{tool} ; 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,
|
||||
"bed_circle": true,
|
||||
"build_height": 260
|
||||
}
|
||||
}
|
||||
29
src/dev/fdm/Flashforge.Finder
Normal file
29
src/dev/fdm/Flashforge.Finder
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"pre":[
|
||||
"M118 X15.00 Y15.00 Z10.00 T{tool}",
|
||||
"M104 S{temp} T{tool}",
|
||||
"M107",
|
||||
"G90",
|
||||
"M132 X Y Z A B",
|
||||
"M907 X100 Y100 Z40 A80 B20",
|
||||
"; pre-extrude",
|
||||
"G1 X-75 Y-75 Z0.35 F400",
|
||||
"G1 Y75 E30 F1800"
|
||||
],
|
||||
"post":[
|
||||
"M107",
|
||||
"M104 S0 T{tool}",
|
||||
"G162 Z",
|
||||
"M18"
|
||||
],
|
||||
"cmd":{
|
||||
"fan_power": "M106 S{fan_speed}"
|
||||
},
|
||||
"settings":{
|
||||
"origin_center": true,
|
||||
"extrude_abs": true,
|
||||
"bed_width": 140,
|
||||
"bed_depth": 140,
|
||||
"build_height": 140
|
||||
}
|
||||
}
|
||||
43
src/dev/fdm/Flashforge.Guider.II
Normal file
43
src/dev/fdm/Flashforge.Guider.II
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"pre":[
|
||||
"; set bed and nozzle temps",
|
||||
"M104 S{temp} T{tool}",
|
||||
"M140 S{bed_temp}",
|
||||
"; absolute positioning and home",
|
||||
"G90",
|
||||
"G28",
|
||||
"; wait for nozzle and bed to reach target temps",
|
||||
"M190 S{bed_temp}",
|
||||
"M109 S{temp} T{tool}",
|
||||
"M7 T{tool}",
|
||||
"M6 T{tool}",
|
||||
"; select nozzle 0",
|
||||
"M108 T{tool}",
|
||||
"G92 E0",
|
||||
"G1 E0 F300",
|
||||
"; purge nozzle",
|
||||
"G1 X150 Y75 Z15 E-2 F500",
|
||||
"G1 Y125 Z0.5 F600",
|
||||
"G1 Y50 E40 F600"
|
||||
],
|
||||
"post":[
|
||||
"; stop fan, cool nozzle and bed",
|
||||
"M107",
|
||||
"M104 S0 T{tool}",
|
||||
"M140 S0",
|
||||
"; slow descent of bed",
|
||||
"G0 F300",
|
||||
"; end print",
|
||||
"M18"
|
||||
],
|
||||
"cmd":{
|
||||
"fan_power": "M106 S{fan_speed}"
|
||||
},
|
||||
"settings":{
|
||||
"origin_center": true,
|
||||
"extrude_abs": true,
|
||||
"bed_width": 280,
|
||||
"bed_depth": 250,
|
||||
"build_height": 300
|
||||
}
|
||||
}
|
||||
44
src/dev/fdm/FolgerTech.FT5
Normal file
44
src/dev/fdm/FolgerTech.FT5
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"pre":[
|
||||
"M117 Heating",
|
||||
"M104 S{temp} T{tool} ; set extruder temperature",
|
||||
"M140 S{bed_temp} T{tool} ; set bed temperature",
|
||||
"G90 ; set absolute positioning mode",
|
||||
"M83 ; set relative positioning for extruder",
|
||||
"M107 ; turn off filament cooling fan",
|
||||
"G28 ; home axes",
|
||||
"G92 X0 Y0 Z0 E0 ; reset all axes positions",
|
||||
"G1 X0 Y5 Z3 ; move XY to 0,5 and Z 3mm over bed",
|
||||
"M190 S{bed_temp} T{tool} ; wait for bed to reach target temp",
|
||||
"M109 S{temp} T{tool} ; wait for extruder to reach target temp",
|
||||
"G0 Z0.25 ; position 0.25mm over bed",
|
||||
"M117 Purge Nozzle",
|
||||
"G1 X100 E30 F600 ; purge 30mm from extruder",
|
||||
"M201 X1000 Y1000 Z100 E2000 ; max accel",
|
||||
"M204 P1000 T2000 ; default accel",
|
||||
"M301 P17.14 I1.12 D65.29 ; pid params",
|
||||
"M900 K{kfactor} ; k factor",
|
||||
"M205 X5 ; max xy jerk"
|
||||
],
|
||||
"post":[
|
||||
"M117 Done",
|
||||
"M107 ; turn off filament cooling fan",
|
||||
"M104 S0 T{tool} ; turn off extruder",
|
||||
"M140 S0 T{tool} ; turn off bed",
|
||||
"G91 ; relative movement",
|
||||
"G1 Z10 F1200 ; drop bed 10mm",
|
||||
"G90 ; absolute positioning",
|
||||
"G28 X0 Y0 ; home XY axes",
|
||||
"M84 ; disable stepper motors"
|
||||
],
|
||||
"cmd":{
|
||||
"fan_power": "M106 S{fan_speed}",
|
||||
"progress": "M117 {progress}% layer {layer} / {layers}"
|
||||
},
|
||||
"settings":{
|
||||
"origin_center": false,
|
||||
"bed_width": 300,
|
||||
"bed_depth": 300,
|
||||
"build_height": 300
|
||||
}
|
||||
}
|
||||
40
src/dev/fdm/GridBot.One
Normal file
40
src/dev/fdm/GridBot.One
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"pre":[
|
||||
"M117 Heating ; announce heating",
|
||||
"M107 ; turn off filament cooling fan",
|
||||
"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",
|
||||
"G28 ; home axes",
|
||||
"G1 F3000 ; set default move speed",
|
||||
"M190 S{bed_temp} T0 ; wait for bed to reach target temp",
|
||||
"M109 S{temp} T0 ; wait for extruder to reach target temp",
|
||||
"M117 Purge ; announce purge",
|
||||
"G92 E0 ; set extruder position to 0",
|
||||
"G1 E20 F250 ; purge 20mm from extruder",
|
||||
"G1 X0 Y0 Z0 E1 F3000 ; move to origin / wipe",
|
||||
"M117 Start Print ; announce start of print"
|
||||
],
|
||||
"post":[
|
||||
"M117 Done ; announce done",
|
||||
"M107 ; turn off filament cooling fan",
|
||||
"M104 S0 T0 ; turn off extruder",
|
||||
"M140 S0 T0 ; turn off bed",
|
||||
"G91 ; relative movement",
|
||||
"G1 Z10 F1200 ; drop bed 10mm",
|
||||
"G90 ; absolute positioning",
|
||||
"G28 X0 Y0 ; home XY axes",
|
||||
"M84 ; disable stepper motors"
|
||||
],
|
||||
"cmd":{
|
||||
"fan_power": "M106 S{fan_speed}",
|
||||
"progress": "M117 {progress}% layer {layer} / {layers}"
|
||||
},
|
||||
"settings":{
|
||||
"origin_center": false,
|
||||
"bed_width": 300,
|
||||
"bed_depth": 300,
|
||||
"build_height": 300
|
||||
}
|
||||
}
|
||||
1
src/dev/fdm/GridBot.Two
Symbolic link
1
src/dev/fdm/GridBot.Two
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
GridBot.One
|
||||
75
src/dev/fdm/IMADE3D.JellyBOX.Original
Normal file
75
src/dev/fdm/IMADE3D.JellyBOX.Original
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"pre":[
|
||||
";---------------------------------------",
|
||||
"; ; ; Jellybox Start Script Begin ; ; ;",
|
||||
";_______________________________________",
|
||||
"; for slicer: Kiri",
|
||||
"; start gcode last modified Mar 28, 2019",
|
||||
"G21 ;metric values",
|
||||
"G90 ;absolute positioning",
|
||||
"M107 ;start with the fan off",
|
||||
"M117 Preparing ;write Preparing",
|
||||
"M190 S45 ;wait for the bed to reach 45C",
|
||||
"M109 S180 ;wait for the extruder to reach 180C",
|
||||
"G28 ;home all axes",
|
||||
"G29 O ;auto bed leveling procedure only if leveling NOT active",
|
||||
";M500 ;optionally save the mesh",
|
||||
"G28 X ;home x to get as far from the plate as possible",
|
||||
"M420 S1 ;(re) enable bed leveling if turned off by the G28",
|
||||
"G0 Y0 F5000 ;position Y in front",
|
||||
"G0 Z15 F3000 ;position Z",
|
||||
"M190 S{bed_temp} T{tool} ;wait for the bed to reach desired temperature",
|
||||
"M109 S{temp} T{tool} ;wait for the extruder to reach desired temperature",
|
||||
"; M300 S440 P300 ;play a tone",
|
||||
"G4 P200 ;pause",
|
||||
"; M0 Click to start print ; optional, recommended only with a rotary encoder",
|
||||
"M117 Print starting ;write Print starting",
|
||||
";================ ;PRINT:LINE start",
|
||||
"G90 ;absolute positioning",
|
||||
"M83 ;relative extrusion mode",
|
||||
"G0 Z0 ;get Z down",
|
||||
"G1 E18 F500 ;extrude __mm of feed stock",
|
||||
"G1 E16 F400 ;extrude __mm of feed stock",
|
||||
"G1 E8 F400 ;extrude __mm of feed stock",
|
||||
"G4 S2 ;pause for ooze",
|
||||
"M400 ;make sure all is finished",
|
||||
"G0 F500 X3 Y0 Z0.6;get to the start of the LINE",
|
||||
"G1 E2 F300 ;extrude __mm of feed stock",
|
||||
"G1 F1000 X104 E10 ;print a thick LINE extruding __mm along the way",
|
||||
"G92 E0 ;reset the extruder position",
|
||||
";---------------------------------------------",
|
||||
"; ; ; Jellybox Printer Start Script End ; ; ;",
|
||||
";_____________________________________________"
|
||||
],
|
||||
"post":[
|
||||
";---------------------------------",
|
||||
";;; Jellybox End Script Begin ;;;",
|
||||
";_________________________________",
|
||||
"; end gcode last modified Nov 30, 2018",
|
||||
"M117 Finishing Up ;write Finishing Up",
|
||||
"M107 ;turn the fan off",
|
||||
"M104 S0 ;extruder heater off",
|
||||
"M140 S0 ;bed heater off (if you have it)",
|
||||
"G91 ;relative positioning (includes extruder)",
|
||||
"G1 E-1 F2500 ;retract the filament a bit before lifting the nozzle to release some of the pressure",
|
||||
"G1 Z0.5 E-4 X-10 F9000 ;get out and retract filament even more",
|
||||
"G1 E-25 F2500 ;retract even more",
|
||||
"G90 ;absolute positioning (includes extruder)",
|
||||
"G28 X ;home X so the head is out of the way",
|
||||
"G1 Y140 ;move Y forward, so the print is more accessible",
|
||||
"M84 ;steppers off",
|
||||
"M117 Print finished ;write Print finished",
|
||||
";---------------------------------------",
|
||||
";;; Jellybox End Script End ;;;",
|
||||
";_______________________________________"
|
||||
],
|
||||
"cmd":{
|
||||
"fan_power": "M106 S{fan_speed}"
|
||||
},
|
||||
"settings":{
|
||||
"origin_center": false,
|
||||
"bed_width": 170,
|
||||
"bed_depth": 145,
|
||||
"build_height": 140
|
||||
}
|
||||
}
|
||||
75
src/dev/fdm/IMADE3D.JellyBOX2
Normal file
75
src/dev/fdm/IMADE3D.JellyBOX2
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"pre":[
|
||||
";---------------------------------------",
|
||||
"; ; ; Jellybox Start Script Begin ; ; ;",
|
||||
";_______________________________________",
|
||||
"; for slicer: Kiri",
|
||||
"; start gcode last modified Mar 28, 2019",
|
||||
"G21 ;metric values",
|
||||
"G90 ;absolute positioning",
|
||||
"M107 ;start with the fan off",
|
||||
"M117 Preparing ;write Preparing",
|
||||
"M190 S45 ;wait for the bed to reach 45C",
|
||||
"M109 S180 ;wait for the extruder to reach 180C",
|
||||
"G28 ;home all axes",
|
||||
"G29 O ;auto bed leveling procedure only if leveling NOT active",
|
||||
";M500 ;optionally save the mesh",
|
||||
"G28 X ;home x to get as far from the plate as possible",
|
||||
"M420 S1 ;(re) enable bed leveling if turned off by the G28",
|
||||
"G0 Y0 F5000 ;position Y in front",
|
||||
"G0 Z15 F3000 ;position Z",
|
||||
"M190 S{bed_temp} T{tool} ;wait for the bed to reach desired temperature",
|
||||
"M109 S{temp} T{tool} ;wait for the extruder to reach desired temperature",
|
||||
"; M300 S440 P300 ;play a tone",
|
||||
"G4 P200 ;pause",
|
||||
"; M0 Click to start print ; optional, recommended only with a rotary encoder",
|
||||
"M117 Print starting ;write Print starting",
|
||||
";================ ;PRINT:LINE start",
|
||||
"G90 ;absolute positioning",
|
||||
"M83 ;relative extrusion mode",
|
||||
"G0 Z0 ;get Z down",
|
||||
"G1 E18 F500 ;extrude __mm of feed stock",
|
||||
"G1 E16 F400 ;extrude __mm of feed stock",
|
||||
"G1 E8 F400 ;extrude __mm of feed stock",
|
||||
"G4 S2 ;pause for ooze",
|
||||
"M400 ;make sure all is finished",
|
||||
"G0 F500 X3 Y0 Z0.6;get to the start of the LINE",
|
||||
"G1 E2 F300 ;extrude __mm of feed stock",
|
||||
"G1 F1000 X104 E10 ;print a thick LINE extruding __mm along the way",
|
||||
"G92 E0 ;reset the extruder position",
|
||||
";---------------------------------------------",
|
||||
"; ; ; Jellybox Printer Start Script End ; ; ;",
|
||||
";_____________________________________________"
|
||||
],
|
||||
"post":[
|
||||
";---------------------------------",
|
||||
";;; Jellybox End Script Begin ;;;",
|
||||
";_________________________________",
|
||||
"; end gcode last modified Nov 30, 2018",
|
||||
"M117 Finishing Up ;write Finishing Up",
|
||||
"M107 ;turn the fan off",
|
||||
"M104 S0 ;extruder heater off",
|
||||
"M140 S0 ;bed heater off (if you have it)",
|
||||
"G91 ;relative positioning (includes extruder)",
|
||||
"G1 E-1 F2500 ;retract the filament a bit before lifting the nozzle to release some of the pressure",
|
||||
"G1 Z0.5 E-4 X-10 F9000 ;get out and retract filament even more",
|
||||
"G1 E-25 F2500 ;retract even more",
|
||||
"G90 ;absolute positioning (includes extruder)",
|
||||
"G28 X ;home X so the head is out of the way",
|
||||
"G1 Y140 ;move Y forward, so the print is more accessible",
|
||||
"M84 ;steppers off",
|
||||
"M117 Print finished ;write Print finished",
|
||||
";---------------------------------------",
|
||||
";;; Jellybox End Script End ;;;",
|
||||
";_______________________________________"
|
||||
],
|
||||
"cmd":{
|
||||
"fan_power": "M106 S{fan_speed}"
|
||||
},
|
||||
"settings":{
|
||||
"origin_center": false,
|
||||
"bed_width": 177,
|
||||
"bed_depth": 155,
|
||||
"build_height": 140
|
||||
}
|
||||
}
|
||||
35
src/dev/fdm/Leapfrog.CreatrHS
Normal file
35
src/dev/fdm/Leapfrog.CreatrHS
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"pre":[
|
||||
"M104 S{temp} T{tool} ; set extruder temperature",
|
||||
"M140 S{bed_temp} T{tool} ; 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} T{tool} ; 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 T{tool} ; turn off right extruder",
|
||||
"M140 S0 T{tool} ; 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
|
||||
}
|
||||
}
|
||||
37
src/dev/fdm/MakerGear.M2
Normal file
37
src/dev/fdm/MakerGear.M2
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"pre":[
|
||||
"M108 S255 ; set extruder speed",
|
||||
"M104 S{temp} T{tool} ; set extruder temperature",
|
||||
"M140 S{bed_temp} T{tool} ; 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} T{tool} ; 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 T{tool} ; turn off right extruder",
|
||||
"M140 S0 T{tool} ; 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
|
||||
}
|
||||
}
|
||||
44
src/dev/fdm/Makerbot.Replicator2
Normal file
44
src/dev/fdm/Makerbot.Replicator2
Normal 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} T{tool} ; set extruder temperature for T{tool} (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 T{tool} ; wait for extruder T{tool} 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 T{tool} ; 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
|
||||
}
|
||||
}
|
||||
37
src/dev/fdm/Prusa.i3.MK2S
Normal file
37
src/dev/fdm/Prusa.i3.MK2S
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"pre":[
|
||||
"M83 ; extruder relative mode",
|
||||
"M104 S160 ; set extruder to a temporary high enough temp",
|
||||
"M140 S{bed_temp} ; set bed temp",
|
||||
"M190 S{bed_temp} ; wait for bed temp",
|
||||
"M109 S160 ; wait for extruder temporary high enough temp",
|
||||
"G28 W ; home all without mesh bed level",
|
||||
"G80 ; mesh bed leveling",
|
||||
"G90 ; set absolute positioning mode",
|
||||
"M83 ; set relative positioning for extruder",
|
||||
"G1 Y-3.0 F1000.0 ; go outside print area",
|
||||
"M104 S{temp} ; set extruder actual required temp",
|
||||
"M109 S{temp} ; wait for extruder actual required temp",
|
||||
"G1 X60.0 E9.0 F1000.0 ; intro line",
|
||||
"G1 X100.0 E12.5 F1000.0 ; intro line",
|
||||
"M900 K{kfactor} ; set Linear Advance factor, see https://www.prusaprinters.org/fw-3-1-0-linear-advance-drivers-1-9-1/"
|
||||
],
|
||||
"post":[
|
||||
"G4 ; wait",
|
||||
"M104 S0 ; turn off temperature",
|
||||
"M140 S0 ; turn off heatbed",
|
||||
"M107 ; turn off fan",
|
||||
"G1 X0 Y200; home X axis",
|
||||
"G1 E-1 F900",
|
||||
"M84 ; disable motors"
|
||||
],
|
||||
"cmd":{
|
||||
"fan_power": "M106 S{fan_speed}"
|
||||
},
|
||||
"settings":{
|
||||
"origin_center": false,
|
||||
"bed_width": 250,
|
||||
"bed_depth": 210,
|
||||
"build_height": 210
|
||||
}
|
||||
}
|
||||
65
src/dev/fdm/Raise3D.N2
Normal file
65
src/dev/fdm/Raise3D.N2
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
{
|
||||
"pre":[
|
||||
"M140 S{bed_temp}",
|
||||
"M221 T{tool} S100.00",
|
||||
"M104 T{tool} S{temp}",
|
||||
"M109 T{tool} S{temp}",
|
||||
"M190 S{bed_temp}",
|
||||
"G21",
|
||||
"G90",
|
||||
"M82",
|
||||
"M107",
|
||||
"G28 X0 Y0",
|
||||
"G28 Z0",
|
||||
"T{tool}",
|
||||
"G1 Z0.05 F6000.00",
|
||||
"G92 E0",
|
||||
"G1 F140 E29",
|
||||
"G4 P500",
|
||||
"G1 X15 Y15 Z0.02 F2000",
|
||||
"G1 F6000.00",
|
||||
"M117 Printing...",
|
||||
"M1001"
|
||||
],
|
||||
"post":[
|
||||
"G91",
|
||||
"G1 Z10",
|
||||
"G90",
|
||||
"M1002",
|
||||
"M104 T{tool} S0",
|
||||
"M104 S0",
|
||||
"M140 S0",
|
||||
"G1 X20 Y20 F6000",
|
||||
"G1 X0 Y0",
|
||||
"M84",
|
||||
";Print Time: {time}",
|
||||
";Material#1 Used: {material}",
|
||||
";Material#2 Used: 0.0"
|
||||
],
|
||||
"pause":[
|
||||
"; --- pause begin ---",
|
||||
"G91 ; Relative Positioning",
|
||||
"G0 Z20 ; Move Bed down 20mm",
|
||||
"G90 ; Absolute positioning",
|
||||
"G0 X10 Y10 ; Move to 10,10",
|
||||
"M2000 ; Pause command",
|
||||
"G91 ; Relative Positioning",
|
||||
"G0 Z-20 ; Move Bed up 20mm",
|
||||
"G90 ; Absolute positioning",
|
||||
"; --- pause end ---"
|
||||
],
|
||||
"cmd":{
|
||||
"fan_power": "M106 S{fan_speed}",
|
||||
"layer": [
|
||||
";LAYER:{layer}",
|
||||
";HEIGHT:{height}"
|
||||
]
|
||||
},
|
||||
"settings":{
|
||||
"origin_center": false,
|
||||
"extrude_abs": true,
|
||||
"bed_width": 310,
|
||||
"bed_depth": 310,
|
||||
"build_height": 305
|
||||
}
|
||||
}
|
||||
37
src/dev/fdm/TypeA.Series1
Normal file
37
src/dev/fdm/TypeA.Series1
Normal 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
|
||||
}
|
||||
}
|
||||
40
src/dev/fdm/TypeA.Series1Pro
Normal file
40
src/dev/fdm/TypeA.Series1Pro
Normal 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
|
||||
}
|
||||
}
|
||||
45
src/dev/fdm/Ulio3D
Normal file
45
src/dev/fdm/Ulio3D
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"pre":[
|
||||
"M104 S{temp} T{tool} ; set extruder temperature",
|
||||
"G90 ; set absolute positioning mode",
|
||||
"M83 ; set relative positioning for extruder",
|
||||
"M107 ; turn off filament cooling fan",
|
||||
"G28 ; home axes",
|
||||
"G92 X0 Y0 Z0 E0 ; reset all axes positions",
|
||||
"G1 X0 Y0 Z20 F180 ; move xy to 0,0 and z 20mm over bed",
|
||||
"G92 E0 ; zero the extruded",
|
||||
"M109 S{temp} T{tool} ; wait for extruder to reach target temp",
|
||||
"G1 E15 F200 ; purge 15mm from extruder",
|
||||
"G92 E0 ; zero the extruded",
|
||||
"G1 F225 ; set feed speed",
|
||||
"M117 ULIO Printing ; text for screen"
|
||||
],
|
||||
"post":[
|
||||
"M107 ; turn off filament cooling fan",
|
||||
"M104 S0 T{tool} ; turn off right extruder",
|
||||
"G1 Z{z_max} F180 ; drop bed",
|
||||
"G28 X0 Y0 ; home XY axes",
|
||||
"M84 ; disable stepper motors"
|
||||
],
|
||||
"pause":[
|
||||
"; --- pause begin ---",
|
||||
"G91 ; Relative Positioning",
|
||||
"G0 Z20 ; Move Bed down 20mm",
|
||||
"G90 ; Absolute positioning",
|
||||
"G0 X10 Y10 ; Move to 10,10",
|
||||
"M2000 ; Pause command",
|
||||
"G91 ; Relative Positioning",
|
||||
"G0 Z-20 ; Move Bed up 20mm",
|
||||
"G90 ; Absolute positioning",
|
||||
"; --- pause end ---"
|
||||
],
|
||||
"cmd":{
|
||||
"fan_power": "M106 S200"
|
||||
},
|
||||
"settings":{
|
||||
"origin_center": false,
|
||||
"bed_width": 103,
|
||||
"bed_depth": 103,
|
||||
"build_height": 78
|
||||
}
|
||||
}
|
||||
35
src/dev/fdm/Ultimaker.Ultimaker2
Normal file
35
src/dev/fdm/Ultimaker.Ultimaker2
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"pre":[
|
||||
"M104 S{temp} T{tool} ; set extruder temperature",
|
||||
"M140 S{bed_temp} T{tool} ; 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} T{tool} ; 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 T{tool} ; turn off right extruder",
|
||||
"M140 S0 T{tool} ; 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
|
||||
}
|
||||
}
|
||||
35
src/dev/fdm/Wanhao.Duplicator_4s
Normal file
35
src/dev/fdm/Wanhao.Duplicator_4s
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"pre":[
|
||||
"M104 S{temp} T{tool} ; set extruder temperature",
|
||||
"M140 S{bed_temp} T{tool} ; 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} T{tool} ; 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 T{tool} ; turn off right extruder",
|
||||
"M140 S0 T{tool} ; 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
|
||||
}
|
||||
}
|
||||
35
src/dev/fdm/Wanhao.Duplicator_5s
Normal file
35
src/dev/fdm/Wanhao.Duplicator_5s
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"pre":[
|
||||
"M104 S{temp} T{tool} ; set extruder temperature",
|
||||
"M140 S{bed_temp} T{tool} ; 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} T{tool} ; 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 T{tool} ; turn off right extruder",
|
||||
"M140 S0 T{tool} ; 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
|
||||
}
|
||||
}
|
||||
35
src/dev/fdm/Wanhao.Duplicator_6
Normal file
35
src/dev/fdm/Wanhao.Duplicator_6
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"pre":[
|
||||
"M104 S{temp} T{tool} ; set extruder temperature",
|
||||
"M140 S{bed_temp} T{tool} ; 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} T{tool} ; 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 T{tool} ; turn off right extruder",
|
||||
"M140 S0 T{tool} ; 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
|
||||
}
|
||||
}
|
||||
35
src/dev/fdm/Wanhao.Duplicator_i3
Normal file
35
src/dev/fdm/Wanhao.Duplicator_i3
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"pre":[
|
||||
"M104 S{temp} T{tool} ; set extruder temperature",
|
||||
"M140 S{bed_temp} T{tool} ; 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} T{tool} ; 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 T{tool} ; turn off right extruder",
|
||||
"M140 S0 T{tool} ; 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
|
||||
}
|
||||
}
|
||||
31
src/dev/fdm/XYZ.daVinci.Jr.Pro
Normal file
31
src/dev/fdm/XYZ.daVinci.Jr.Pro
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"pre":[
|
||||
"T{tool} ; select extruder #0",
|
||||
"M104 S{temp} ; set nozzle temp",
|
||||
"M109 S{temp} ; wait for nozzle to heat",
|
||||
"M82 ; absolute extrusion mode",
|
||||
"G28 ; home",
|
||||
"G1 Z15 F6000 ; lower platform 15mm",
|
||||
"G92 E0 ; zero out extruder position",
|
||||
"G1 F200 E3 ; prime the extruder 3mm"
|
||||
],
|
||||
"post":[
|
||||
"M107 ; fan off",
|
||||
"M104 S0 ; set nozzle temp to 0",
|
||||
"M140 S0 ; set bed temp to 0",
|
||||
"G92 E0 ; zero out extruder position",
|
||||
"G1 E-1 F300 ; retract filament",
|
||||
"G28 X0 Y0 ; home X Y",
|
||||
"M84 ; disable steppers"
|
||||
],
|
||||
"cmd":{
|
||||
"fan_power": "M106 S{fan_speed}"
|
||||
},
|
||||
"settings":{
|
||||
"origin_center": false,
|
||||
"extrude_abs": true,
|
||||
"bed_width": 150,
|
||||
"bed_depth": 150,
|
||||
"build_height": 150
|
||||
}
|
||||
}
|
||||
34
src/dev/fdm/XYZ.daVinci.Mini.w
Normal file
34
src/dev/fdm/XYZ.daVinci.Mini.w
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"pre":[
|
||||
"T{tool} ; select extruder #0",
|
||||
"M104 S{temp} ; set nozzle temp",
|
||||
"M109 S{temp} ; wait for nozzle to heat",
|
||||
"G21 ; set units to millimeters",
|
||||
"M82 ; absolute extrusion mode",
|
||||
"G28 ; home",
|
||||
"G1 Z15 F6000 ; lower platform 15mm",
|
||||
"G92 E0 ; zero out extruder position",
|
||||
"G1 F200 E3 ; prime the extruder 3mm"
|
||||
],
|
||||
"post":[
|
||||
"M107 ; fan off",
|
||||
"M104 S0 ; set nozzle temp to 0",
|
||||
"M140 S0 ; set bed temp to 0",
|
||||
"G92 E0 ; zero out extruder position",
|
||||
"G1 E-1 F300 ; retract filament",
|
||||
"G28 X0 Y0 ; home X Y",
|
||||
"M84 ; disable steppers"
|
||||
],
|
||||
"cmd":{
|
||||
"fan_power": "M106 S{fan_speed}"
|
||||
},
|
||||
"proc": "kiri_fdm_xyz_mini_w",
|
||||
"file-ext": "3w",
|
||||
"settings":{
|
||||
"origin_center": false,
|
||||
"extrude_abs": true,
|
||||
"bed_width": 150,
|
||||
"bed_depth": 150,
|
||||
"build_height": 150
|
||||
}
|
||||
}
|
||||
21
src/dev/laser/Any.Generic.Laser
Normal file
21
src/dev/laser/Any.Generic.Laser
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"file-ext": "gcode",
|
||||
"token-space": " ",
|
||||
"pre":[
|
||||
"G21 ; set units to MM (required)",
|
||||
"G90 ; absolute position mode (required)"
|
||||
],
|
||||
"post":[
|
||||
"M30 ; program end"
|
||||
],
|
||||
"laser-on":[
|
||||
"M106 S{power}"
|
||||
],
|
||||
"laser-off":[
|
||||
"M107"
|
||||
],
|
||||
"settings": {
|
||||
"bed_width": 300,
|
||||
"bed_depth": 300
|
||||
}
|
||||
}
|
||||
12
src/dev/laser/Glowforge
Normal file
12
src/dev/laser/Glowforge
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"file-ext": "gcode",
|
||||
"token-space": " ",
|
||||
"pre":[],
|
||||
"post":[],
|
||||
"laser-on":[],
|
||||
"laser-off":[],
|
||||
"settings": {
|
||||
"bed_width": 500,
|
||||
"bed_depth": 300
|
||||
}
|
||||
}
|
||||
10
src/dev/sla/Anycubic.Photon
Normal file
10
src/dev/sla/Anycubic.Photon
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"no_clone": true,
|
||||
"cmd":{},
|
||||
"settings":{
|
||||
"bed_width": 120.96,
|
||||
"bed_depth": 68.04,
|
||||
"bed_height": 1,
|
||||
"max_height": 150
|
||||
}
|
||||
}
|
||||
10
src/dev/sla/Anycubic.Photon.S
Normal file
10
src/dev/sla/Anycubic.Photon.S
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"no_clone": true,
|
||||
"cmd":{},
|
||||
"settings":{
|
||||
"bed_width": 120.96,
|
||||
"bed_depth": 68.04,
|
||||
"bed_height": 1,
|
||||
"max_height": 150
|
||||
}
|
||||
}
|
||||
6986
src/ext/clip.js
Executable file
6986
src/ext/clip.js
Executable file
File diff suppressed because it is too large
Load diff
7629
src/ext/clip2.js
Normal file
7629
src/ext/clip2.js
Normal file
File diff suppressed because it is too large
Load diff
1
src/ext/earcut.js
Symbolic link
1
src/ext/earcut.js
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../node_modules/earcut/src/earcut.js
|
||||
270
src/ext/fsave.js
Normal file
270
src/ext/fsave.js
Normal 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;
|
||||
});
|
||||
}
|
||||
16433
src/ext/pngjs.js
Normal file
16433
src/ext/pngjs.js
Normal file
File diff suppressed because it is too large
Load diff
1
src/ext/three.js
Symbolic link
1
src/ext/three.js
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../node_modules/three/build/three.min.js
|
||||
1
src/ext/tween.js
Symbolic link
1
src/ext/tween.js
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../node_modules/tween.js/index.js
|
||||
397
src/geo/base.js
Normal file
397
src/geo/base.js
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.base) return;
|
||||
|
||||
const BASE = self.base = {},
|
||||
round_decimal_precision = 8;
|
||||
|
||||
/** ******************************************************************
|
||||
* Utility Functions
|
||||
******************************************************************* */
|
||||
|
||||
function time() { return Date.now() }
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
let 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 Math.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 Math.abs(offset) - precision;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value
|
||||
* @param min
|
||||
* @param max
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function inRange(value, min, max) {
|
||||
let val = parseFloat(value);
|
||||
return val >= min && val <= max;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param v
|
||||
* @param zeros
|
||||
* @returns {number}
|
||||
*/
|
||||
function round(v, zeros) {
|
||||
let 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) {
|
||||
let 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 (Math.abs(d) < 0.0000000001) {
|
||||
if (Math.abs(d) < 0.0001) {
|
||||
// lines are parallel or collinear
|
||||
return test && !parallelok ? null : keys.PARALLEL;
|
||||
}
|
||||
|
||||
let 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
|
||||
|
||||
let 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;
|
||||
|
||||
let 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) {
|
||||
let 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);
|
||||
|
||||
let a = p1y - p3y,
|
||||
b = p1x - p3x,
|
||||
n1 = (s2x * a) - (s2y * b),
|
||||
n2 = (s1x * a) - (s1y * b);
|
||||
|
||||
if (Math.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)) {
|
||||
let 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) {
|
||||
let 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 : 250
|
||||
};
|
||||
|
||||
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,
|
||||
comma: (v) => {
|
||||
let [lt,rt] = v.toString().split('.');
|
||||
lt = lt.toString().split('').reverse().map((v,i,a) => {
|
||||
return (i < a.length - 1 && i % 3 === 2) ? `,${v}` : v
|
||||
}).reverse().join('');
|
||||
return rt ? `${lt}.${rt}` : lt;
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
135
src/geo/bounds.js
Normal file
135
src/geo/bounds.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.base.Bounds) return;
|
||||
|
||||
/**
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function Bounds() {
|
||||
this.minx = 10e7;
|
||||
this.miny = 10e7;
|
||||
this.maxx = -10e7;
|
||||
this.maxy = -10e7;
|
||||
this.leftMost = null;
|
||||
}
|
||||
|
||||
const BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
CONF = BASE.config,
|
||||
PRO = Bounds.prototype;
|
||||
|
||||
BASE.Bounds = Bounds;
|
||||
BASE.newBounds = newBounds;
|
||||
|
||||
/** ******************************************************************
|
||||
* Bounds Prototype Functions
|
||||
******************************************************************* */
|
||||
|
||||
/**
|
||||
* @returns {Bounds}
|
||||
*/
|
||||
PRO.clone = function() {
|
||||
let b = new Bounds();
|
||||
b.minx = this.minx;
|
||||
b.miny = this.miny;
|
||||
b.maxx = this.maxx;
|
||||
b.maxy = this.maxy;
|
||||
return b;
|
||||
};
|
||||
|
||||
PRO.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
|
||||
*/
|
||||
PRO.merge = function(b) {
|
||||
this.minx = Math.min(this.minx, b.minx);
|
||||
this.maxx = Math.max(this.maxx, b.maxx);
|
||||
this.miny = Math.min(this.miny, b.miny);
|
||||
this.maxy = Math.max(this.maxy, b.maxy);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Point} p
|
||||
*/
|
||||
PRO.update = function(p) {
|
||||
this.minx = Math.min(this.minx, p.x);
|
||||
this.maxx = Math.max(this.maxx, p.x);
|
||||
this.miny = Math.min(this.miny, p.y);
|
||||
this.maxy = Math.max(this.maxy, p.y);
|
||||
if (this.minx === p.x) this.leftMost = p;
|
||||
};
|
||||
|
||||
PRO.contains = function(bounds) {
|
||||
return bounds.isNested(this);
|
||||
};
|
||||
|
||||
PRO.containsXY = function(x,y) {
|
||||
return x >= this.minx && x <= this.maxx && y >= this.miny && y <= this.maxy;
|
||||
};
|
||||
|
||||
PRO.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
|
||||
*/
|
||||
PRO.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}
|
||||
*/
|
||||
PRO.overlaps = function(b, precision) {
|
||||
return (
|
||||
Math.abs(this.centerx() - b.centerx()) * 2 - precision < this.width() + b.width() &&
|
||||
Math.abs(this.centery() - b.centery()) * 2 - precision < this.height() + b.height()
|
||||
);
|
||||
};
|
||||
|
||||
PRO.width = function() {
|
||||
return this.maxx - this.minx;
|
||||
};
|
||||
|
||||
PRO.height = function() {
|
||||
return this.maxy - this.miny;
|
||||
};
|
||||
|
||||
PRO.centerx = function() {
|
||||
return this.minx + this.width() / 2;
|
||||
};
|
||||
|
||||
PRO.centery = function() {
|
||||
return this.miny + this.height() / 2;
|
||||
};
|
||||
|
||||
/** ******************************************************************
|
||||
* Connect to base and Helpers
|
||||
******************************************************************* */
|
||||
|
||||
function newBounds() {
|
||||
return new Bounds();
|
||||
}
|
||||
|
||||
})();
|
||||
219
src/geo/debug.js
Normal file
219
src/geo/debug.js
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.base.debug) return;
|
||||
|
||||
const base = self.base,
|
||||
flags = {},
|
||||
stash = [],
|
||||
debug_colors = [0xff0000, 0x00ff00, 0x0000ff, 0x00ffff, 0xff00ff, 0xffff00];
|
||||
|
||||
let size = 20,
|
||||
enabled = false,
|
||||
next_debug_color = 0;
|
||||
|
||||
/** ******************************************************************
|
||||
* 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 (let i=0; i<f.length; i++) flags[f[i]] = (value || 1);
|
||||
} else {
|
||||
flags[f] = (value || 1);
|
||||
}
|
||||
}
|
||||
|
||||
function clear(f) {
|
||||
if (Array.isArray(f)) {
|
||||
for (let i = 0; i < f.length; i++) delete flags[f[i]];
|
||||
} else {
|
||||
delete flags[f];
|
||||
}
|
||||
}
|
||||
|
||||
function test(isSet, isNotSet) {
|
||||
let 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) {
|
||||
let 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
|
||||
};
|
||||
|
||||
})();
|
||||
198
src/geo/gyroid.js
Normal file
198
src/geo/gyroid.js
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
/** Copyright Stewart Allen -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.base.gyroid) return;
|
||||
|
||||
const base = self.base,
|
||||
PI2 = Math.PI * 2;
|
||||
|
||||
/**
|
||||
* @param off {number} z offset value from 0-1
|
||||
* @param res {number} resolution (pixels/slices per side)
|
||||
*/
|
||||
function slice(off, res, val) {
|
||||
let rez = parseInt(res || 200);
|
||||
let inc = PI2 / rez;
|
||||
let z = PI2 * off;
|
||||
let tip = val || 0;
|
||||
let edge = [];
|
||||
let vals = [];
|
||||
let points = 0;
|
||||
let points_lr = 0;
|
||||
let points_td = 0;
|
||||
for (let x=0; x<PI2; x += inc) {
|
||||
let vrow = []; // raw values row
|
||||
let erow = []; // edge values row
|
||||
edge.push(erow);
|
||||
vals.push(vrow);
|
||||
for (let y=0; y<PI2; y += inc) {
|
||||
erow.push(0);
|
||||
vrow.push(
|
||||
Math.sin(x) * Math.cos(y) +
|
||||
Math.sin(y) * Math.cos(z) +
|
||||
Math.sin(z) * Math.cos(x)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// left-right threshold search (red)
|
||||
vals.forEach((vrow, y) => {
|
||||
let erow = edge[y];
|
||||
let lval = vrow[vrow.length - 1];
|
||||
vrow.forEach((val, x) => {
|
||||
if (
|
||||
(lval <= tip && val >= tip) || (lval >= tip && val <= tip) ||
|
||||
(lval <= -tip && val >= -tip) || (lval >= -tip && val <= -tip)
|
||||
) {
|
||||
erow[x] = 1;
|
||||
points++;
|
||||
points_lr++;
|
||||
}
|
||||
lval = val;
|
||||
})
|
||||
});
|
||||
|
||||
// top-down threshold search (green)
|
||||
for (let x=0; x<rez; x++) {
|
||||
let lval = vals[vals.length-1][x];
|
||||
for (let y=0; y<rez; y++) {
|
||||
let val = vals[y][x];
|
||||
if (
|
||||
(lval <= tip && val >= tip) || (lval >= tip && val <= tip) ||
|
||||
(lval <= -tip && val >= -tip) || (lval >= -tip && val <= -tip)
|
||||
) {
|
||||
if (edge[y][x]) {
|
||||
edge[y][x] = 3;
|
||||
} else {
|
||||
edge[y][x] = 2;
|
||||
points++
|
||||
}
|
||||
points_td++;
|
||||
}
|
||||
lval = val;
|
||||
}
|
||||
}
|
||||
|
||||
// deterime prevailing direction for chaining
|
||||
let dir = points_td > points_lr ? 'lr' : 'td';
|
||||
|
||||
// create sparse representation
|
||||
let sparse = [];
|
||||
let center = rez / 2;
|
||||
edge.forEach((row,y) => {
|
||||
row.forEach((val,x) => {
|
||||
if (val) {
|
||||
let dx = Math.abs(x - center);
|
||||
let dy = Math.abs(y - center);
|
||||
sparse.push({x: x/rez, y: y/rez, val, dist: Math.max(dx,dy)});
|
||||
}
|
||||
});
|
||||
});
|
||||
sparse.sort((a,b) => {
|
||||
return b.dist - a.dist;
|
||||
});
|
||||
|
||||
// join sparse points array by closest distance
|
||||
let polys = [];
|
||||
let chain;
|
||||
let added;
|
||||
let cleared = 0;
|
||||
let maxdist = 0.05;
|
||||
|
||||
do {
|
||||
chain = null;
|
||||
for (let i=0; i<sparse.length; i++) {
|
||||
if (sparse[i]) {
|
||||
chain = [ sparse[i] ];
|
||||
polys.push(chain);
|
||||
sparse[i] = null;
|
||||
cleared++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
do {
|
||||
added = false;
|
||||
let target = chain[chain.length - 1];
|
||||
let cl_elm = null;
|
||||
let cl_idx = null;
|
||||
let cl_dst = Infinity;
|
||||
for (let i=0; i<sparse.length; i++) {
|
||||
let test_el = sparse[i];
|
||||
if (test_el) {
|
||||
let dst = distTo(target, test_el, dir);
|
||||
if (cl_idx === null || dst < cl_dst) {
|
||||
cl_idx = i;
|
||||
cl_elm = test_el;
|
||||
cl_dst = dst;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cl_elm) {
|
||||
if (cl_dst > maxdist) {
|
||||
break;
|
||||
}
|
||||
sparse[cl_idx] = null;
|
||||
cleared++;
|
||||
chain.push(cl_elm);
|
||||
added = true;
|
||||
}
|
||||
} while (added);
|
||||
} while (cleared < sparse.length);
|
||||
|
||||
let psimple = polys
|
||||
.map(poly => filter(poly, 0))
|
||||
.map(poly => filter(poly, inc));
|
||||
|
||||
return {edge, points, dir, polys: psimple};
|
||||
}
|
||||
|
||||
// merge co-linear and distance threshold
|
||||
function filter(poly, inc) {
|
||||
if (poly.length <= 2) {
|
||||
return poly;
|
||||
}
|
||||
let nupoly = [ poly[0] ];
|
||||
let e1 = poly[1];
|
||||
let e2 = null;
|
||||
let last = poly.length - 2;
|
||||
for (let i=1; i<poly.length; i++) {
|
||||
let el = poly[i];
|
||||
let drop = inc ?
|
||||
(distTo(e1, el) <= inc) :
|
||||
(e1.x === el.x || e1.y === el.y);
|
||||
if (drop) {
|
||||
e2 = el;
|
||||
if (i < last) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (e2) {
|
||||
nupoly.push({x:(e1.x + e2.x)/2, y:(e1.y + e2.y)/2});
|
||||
e2 = null;
|
||||
} else {
|
||||
nupoly.push(e1);
|
||||
if (i === last) {
|
||||
nupoly.push(el);
|
||||
}
|
||||
}
|
||||
e1 = el;
|
||||
}
|
||||
nupoly.push(poly[poly.length-1]);
|
||||
return nupoly;
|
||||
}
|
||||
|
||||
function distTo(a, b, dir) {
|
||||
let dx = a.x - b.x;
|
||||
let dy = a.y - b.y;
|
||||
// bias distance by prevailing direction of discovery to join stragglers
|
||||
if (dir === 'lr') dx = dx / 2;
|
||||
if (dir === 'td') dy = dy / 2;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
base.gyroid = { slice };
|
||||
|
||||
})();
|
||||
110
src/geo/line.js
Normal file
110
src/geo/line.js
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.base.Line) return;
|
||||
|
||||
const BASE = self.base, PRO = Line.prototype;
|
||||
|
||||
BASE.Line = Line;
|
||||
BASE.newLine = newLine;
|
||||
BASE.newOrderedLine = newOrderedLine;
|
||||
|
||||
/**
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
/** ******************************************************************
|
||||
* Line Prototype Functions
|
||||
******************************************************************* */
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
PRO.length = function() {
|
||||
return Math.sqrt(this.length2());
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {number} square of length
|
||||
*/
|
||||
PRO.length2 = function() {
|
||||
return this.p1.distToSq2D(this.p2);
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {Slope}
|
||||
*/
|
||||
PRO.slope = function() {
|
||||
return BASE.newSlope(this.p1.slopeTo(this.p2));
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {Line}
|
||||
*/
|
||||
PRO.reverse = function() {
|
||||
let t = this.p1;
|
||||
this.p1 = this.p2;
|
||||
this.p2 = t;
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {Point}
|
||||
*/
|
||||
PRO.midpoint = function() {
|
||||
return this.p1.midPointTo(this.p2);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Line} line
|
||||
* @returns {boolean}
|
||||
*/
|
||||
PRO.isCollinear = function(line) {
|
||||
let 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);
|
||||
}
|
||||
|
||||
})();
|
||||
708
src/geo/point.js
Normal file
708
src/geo/point.js
Normal file
|
|
@ -0,0 +1,708 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.base.Point) return;
|
||||
|
||||
const BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
CONF = BASE.config,
|
||||
KEYS = BASE.key,
|
||||
ROUND = UTIL.round,
|
||||
PRO = Point.prototype;
|
||||
|
||||
BASE.Point = Point;
|
||||
BASE.newPoint = newPoint;
|
||||
|
||||
/**
|
||||
*
|
||||
* @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
|
||||
}
|
||||
|
||||
/** ******************************************************************
|
||||
* Point Prototype Functions
|
||||
******************************************************************* */
|
||||
|
||||
PRO.setZ = function(z) {
|
||||
this.z = z;
|
||||
return this;
|
||||
}
|
||||
|
||||
PRO.swapXZ = function() {
|
||||
let p = this,
|
||||
t = p.x;
|
||||
p.x = p.z;
|
||||
p.z = t;
|
||||
};
|
||||
|
||||
PRO.swapYZ = function() {
|
||||
let p = this,
|
||||
t = p.y;
|
||||
p.y = p.z;
|
||||
p.z = t;
|
||||
};
|
||||
|
||||
PRO.round = function(precision) {
|
||||
return newPoint(ROUND(this.x,precision), ROUND(this.y,precision), ROUND(this.z,precision));
|
||||
};
|
||||
|
||||
PRO.addFacet = function(facet) {
|
||||
if (!this.group) this.group = [];
|
||||
this.group.push(facet);
|
||||
return this;
|
||||
};
|
||||
|
||||
PRO.rekey = function() {
|
||||
this.key = [this.x,this.y,this.z].join(',');
|
||||
};
|
||||
|
||||
PRO.toString = function() {
|
||||
return this.key;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {Point}
|
||||
*/
|
||||
PRO.clone = function() {
|
||||
return newPoint(this.x, this.y, this.z, this.key);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Point} p
|
||||
* @returns {Slope}
|
||||
*/
|
||||
PRO.slopeTo = function(p) {
|
||||
return BASE.newSlope(this, p);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Point} p
|
||||
* @param {String} [k]
|
||||
* @returns {Line}
|
||||
*/
|
||||
PRO.lineTo = function(p, k) {
|
||||
return BASE.newLine(this, p, k);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Point} p
|
||||
* @param {number} [dist]
|
||||
* @returns {boolean}
|
||||
*/
|
||||
PRO.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}
|
||||
*/
|
||||
PRO.distToLine = function(p1, p2) {
|
||||
// return p2l(this, p1, p2);
|
||||
return Math.sqrt(this.distToLineSq(p1, p2));
|
||||
};
|
||||
|
||||
/**
|
||||
* used exclusively in new fill code. output does not agree with
|
||||
* old distToLine, but is the only method that seems to work for
|
||||
* fill. using new distToLine as a global replacement breaks support
|
||||
* offset clipping. both need to be investigated and a single line
|
||||
* normal distance needs to be formulated to replace both functions.
|
||||
*/
|
||||
PRO.distToLineNew = function(p1, p2) {
|
||||
return p2l(this, 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}
|
||||
*/
|
||||
PRO.distToLineSq = function(p1, p2) {
|
||||
let p = this,
|
||||
d = UTIL.distSq(p1, p2);
|
||||
|
||||
let 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));
|
||||
};
|
||||
|
||||
// ---( begin fix distToLine )---
|
||||
|
||||
function dot(u, v) {
|
||||
return u.x * v.x + u.y * v.y;
|
||||
}
|
||||
|
||||
function norm(v) {
|
||||
return Math.sqrt(dot(v,v));
|
||||
}
|
||||
|
||||
function d(u, v) {
|
||||
return norm({x: u.x - v.x, y: u.y - v.y});
|
||||
}
|
||||
|
||||
function p2l(p, l1, l2) {
|
||||
let v = {x: l2.x - l1.x, y: l2.y - l1.y};
|
||||
let w = {x: p.x - l1.x, y: p.y - l1.y};
|
||||
let c1 = dot(w, v);
|
||||
let c2 = dot(v, v);
|
||||
let b = c1 / c2;
|
||||
if (isNaN(b)) {
|
||||
// console.log('nan', {p, l1, l2, v, w, c1, c2});
|
||||
b = 0;
|
||||
}
|
||||
let pb = {x: l1.x + b * v.x, y: l1.y + b * v.y};
|
||||
return d(p, pb);
|
||||
}
|
||||
|
||||
// ---( end fix distToLine )---
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Point} p1
|
||||
* @param {Point} p2
|
||||
* @param {number} dist2
|
||||
* @returns {boolean}
|
||||
*/
|
||||
PRO.withinDist2 = function(p1, p2, dist2) {
|
||||
let 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}
|
||||
*/
|
||||
PRO.midPointTo = function(p2) {
|
||||
return newPoint((this.x + p2.x)/2, (this.y + p2.y)/2, this.z);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Point} p2
|
||||
* @returns {Point}
|
||||
*/
|
||||
PRO.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}
|
||||
*/
|
||||
PRO.projectOnSlope = function(slope, mult) {
|
||||
return newPoint(
|
||||
this.x + slope.dx * mult,
|
||||
this.y + slope.dy * mult,
|
||||
this.z);
|
||||
};
|
||||
|
||||
PRO.followTo = function(point, mult) {
|
||||
return this.follow(this.slopeTo(point), mult);
|
||||
};
|
||||
|
||||
/**
|
||||
* return a point along the line this from point to p2 offset
|
||||
* by a distance. positive distances are closer to this point.
|
||||
*
|
||||
* @param p2
|
||||
* @param dist
|
||||
*/
|
||||
PRO.offsetPointFrom = function(p2, dist) {
|
||||
let 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);
|
||||
};
|
||||
|
||||
/**
|
||||
* return a point along the line this from point to p2 offset
|
||||
* by a distance. positive distances are farther from this point.
|
||||
*
|
||||
* @param p2
|
||||
* @param dist
|
||||
*/
|
||||
PRO.offsetPointTo = function(p2, dist) {
|
||||
let p1 = this,
|
||||
dx = p2.x - p1.x,
|
||||
dy = p2.y - p1.y;
|
||||
|
||||
if (dx === 0 && dy === 0) return this;
|
||||
|
||||
let ls = dist / Math.sqrt(dx * dx + dy * dy),
|
||||
ox = dx * ls,
|
||||
oy = dy * ls;
|
||||
|
||||
return newPoint(p1.x + ox, p1.y + oy, p2.z, KEYS.NONE);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Point} p2
|
||||
* @param {number} offset
|
||||
* @returns {Line}
|
||||
*/
|
||||
PRO.offsetLineTo = function(p2, offset) {
|
||||
let 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);
|
||||
};
|
||||
|
||||
PRO.offset = function(x, y, z) {
|
||||
return newPoint(this.x + x, this.y + y, this.z + z);
|
||||
};
|
||||
|
||||
/**
|
||||
* checks if a point is inside of a polygon
|
||||
* does not check children/holes
|
||||
*
|
||||
* @param {Polygon} poly
|
||||
* @returns {boolean}
|
||||
*/
|
||||
PRO.inPolygon = function(poly) {
|
||||
if (!poly.bounds.containsXY(this.x, this.y)) return false;
|
||||
|
||||
let 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
|
||||
*/
|
||||
PRO.isInPolygon = function(poly) {
|
||||
let point = this, i;
|
||||
if (Array.isArray(poly)) {
|
||||
for (i=0; i<poly.length; i++) {
|
||||
if (point.isInPolygon(poly[i])) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
let 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
|
||||
*/
|
||||
PRO.isInPolygonOnly = function(poly) {
|
||||
let point = this, i;
|
||||
if (Array.isArray(poly)) {
|
||||
for (i=0; i<poly.length; i++) {
|
||||
if (point.isInPolygonOnly(poly[i])) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
let 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}
|
||||
*/
|
||||
PRO.nearPolygon = function(poly, dist2, inner) {
|
||||
// throw new Error("nearPolygon");
|
||||
for (let 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 (let 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}
|
||||
*/
|
||||
PRO.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}
|
||||
*/
|
||||
PRO.follow = function(slope, distance) {
|
||||
let 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}
|
||||
*/
|
||||
PRO.intersectZ = function(p, z) {
|
||||
let 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}
|
||||
*/
|
||||
PRO.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}
|
||||
*/
|
||||
PRO.isMergable2D = function(p) {
|
||||
return this.isEqual2D(p) || (this.distToSq2D(p) < CONF.precision_merge_sq);
|
||||
};
|
||||
|
||||
/**
|
||||
* compares 3D point
|
||||
*
|
||||
* @param {Point} p
|
||||
* @returns {boolean}
|
||||
*/
|
||||
PRO.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}
|
||||
*/
|
||||
PRO.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}
|
||||
*/
|
||||
PRO.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
|
||||
*/
|
||||
PRO.distToPolySegments = function(poly, threshold) {
|
||||
let 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
|
||||
*/
|
||||
PRO.distToPolyPoints = function(poly, threshold) {
|
||||
let 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
|
||||
*/
|
||||
PRO.nearestTo = function(points, max) {
|
||||
if (!max) throw "missing max";
|
||||
let 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
|
||||
*/
|
||||
PRO.averageDistTo = function(points) {
|
||||
let 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}
|
||||
*/
|
||||
PRO.distTo2D = function(p) {
|
||||
let 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}
|
||||
*/
|
||||
PRO.distToSq2D = function(p) {
|
||||
let dx = this.x - p.x,
|
||||
dy = this.y - p.y;
|
||||
return dx * dx + dy * dy;
|
||||
};
|
||||
|
||||
PRO.distTo3D = function(p) {
|
||||
let 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}
|
||||
*/
|
||||
PRO.distToSq3D = function(p) {
|
||||
let 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}
|
||||
*/
|
||||
PRO.inTriangle = function(a, b, c) {
|
||||
let 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}
|
||||
*/
|
||||
PRO.onLine = function(p1, p2) {
|
||||
return this.distToLine(p1, p2) < CONF.precision_point_on_line;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {THREE.Vector3} delta
|
||||
* @return {Point} new offset point
|
||||
*/
|
||||
PRO.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
|
||||
*/
|
||||
PRO.sub = function(delta) {
|
||||
return newPoint(this.x - delta.x, this.y - delta.y, this.z - delta.z);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {THREE.Vector3} delta
|
||||
*/
|
||||
PRO.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);
|
||||
}
|
||||
|
||||
})();
|
||||
1356
src/geo/polygon.js
Normal file
1356
src/geo/polygon.js
Normal file
File diff suppressed because it is too large
Load diff
890
src/geo/polygons.js
Normal file
890
src/geo/polygons.js
Normal file
|
|
@ -0,0 +1,890 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (!self.base) self.base = {};
|
||||
if (self.base.polygons) return;
|
||||
|
||||
const BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
CONF = BASE.config,
|
||||
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,
|
||||
expand2 : expand2,
|
||||
expand : expand,
|
||||
expand_lines: expand_lines,
|
||||
union : union,
|
||||
nest : nest,
|
||||
diff : doDiff,
|
||||
filter : filter,
|
||||
toClipper : toClipper,
|
||||
fromClipperNode : fromClipperNode,
|
||||
fromClipperTree : fromClipperTree,
|
||||
fromClipperTreeUnion: fromClipperTreeUnion,
|
||||
cleanClipperTree : cleanClipperTree,
|
||||
fingerprintCompare: fingerprintCompare,
|
||||
fingerprint: fingerprint
|
||||
};
|
||||
|
||||
/** ******************************************************************
|
||||
* Polygon array utility functions
|
||||
******************************************************************* */
|
||||
|
||||
function toClipper(polys,debug) {
|
||||
let out = [];
|
||||
polys.forEach(function(poly) { poly.toClipper(out,debug) });
|
||||
return out;
|
||||
}
|
||||
|
||||
function fromClipperNode(tnode, z) {
|
||||
let 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, minarea) {
|
||||
let polys = tops || [],
|
||||
poly,
|
||||
min = minarea || 0.1;
|
||||
|
||||
tnode.m_Childs.forEach(function(child) {
|
||||
poly = fromClipperNode(child, z);
|
||||
// throw out all tiny polygons
|
||||
if (poly.area() < min) {
|
||||
return;
|
||||
}
|
||||
if (parent) {
|
||||
parent.addInner(poly);
|
||||
} else {
|
||||
polys.push(poly);
|
||||
}
|
||||
if (child.m_Childs) {
|
||||
fromClipperTree(child, z, polys, parent ? null : poly, minarea);
|
||||
}
|
||||
});
|
||||
|
||||
return polys;
|
||||
};
|
||||
|
||||
function fromClipperTreeUnion(tnode, z, minarea, tops, parent) {
|
||||
let polys = tops || [], poly;
|
||||
|
||||
tnode.m_Childs.forEach(function(child) {
|
||||
poly = fromClipperNode(child, z);
|
||||
if (minarea && poly.area() < minarea) {
|
||||
return;
|
||||
}
|
||||
if (parent) {
|
||||
parent.addInner(poly);
|
||||
} else {
|
||||
polys.push(poly);
|
||||
}
|
||||
if (child.m_Childs) {
|
||||
fromClipperTreeUnion(child, z, minarea, polys, parent ? null : poly);
|
||||
}
|
||||
});
|
||||
|
||||
return polys;
|
||||
};
|
||||
|
||||
function cleanClipperTree(tree) {
|
||||
let 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
let 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 (let j = i + 1; j < polygons.length; j++) {
|
||||
let 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
|
||||
let 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;
|
||||
let 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) {
|
||||
let 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 (let 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) {
|
||||
let 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) {
|
||||
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, null, null, min), 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, null, null, min), outB);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slice.doProjectedFills()
|
||||
* Print.init w/ brims
|
||||
*
|
||||
* @param {Polygon[]} polys
|
||||
* @returns {Polygon[]}
|
||||
*/
|
||||
function union(polys, minarea) {
|
||||
if (polys.length < 2) return polys;
|
||||
|
||||
let 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], minarea);
|
||||
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) {
|
||||
let 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;
|
||||
|
||||
let out = [], tmp;
|
||||
UTIL.doCombinations(setA, setB, {}, function(a, b) {
|
||||
if (tmp = a.mask(b)) {
|
||||
out.appendAll(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function sumCirc(polys) {
|
||||
let sum = 0.0;
|
||||
polys.forEach(function(poly) {
|
||||
sum += poly.circularityDeep();
|
||||
});
|
||||
return sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Polygon[]} polys
|
||||
* @param {number} distance offset
|
||||
* @param {number} [z] defaults to 0
|
||||
*/
|
||||
function expand_lines(poly, distance, z) {
|
||||
let fact = CONF.clipper,
|
||||
clib = self.ClipperLib,
|
||||
cjnt = clib.JoinType,
|
||||
cety = clib.EndType,
|
||||
coff = new clib.ClipperOffset(),
|
||||
ctre = new clib.PolyTree();
|
||||
|
||||
coff.AddPaths(poly.toClipper(), cjnt.jtMiter, cety.etOpenSquare);
|
||||
coff.Execute(ctre, distance * fact);
|
||||
|
||||
return fromClipperTree(ctre, z, null, null, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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, min) {
|
||||
// prepare alignments for clipper lib
|
||||
alignWindings(polys);
|
||||
polys.forEach(function(poly) {
|
||||
if (poly.inner) setWinding(poly.inner, !poly.isClockwise());
|
||||
});
|
||||
|
||||
let 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(),
|
||||
circ = sumCirc(polys);
|
||||
|
||||
polys.forEach(function(poly) {
|
||||
let clean = clip.CleanPolygons(poly.toClipper(), CONF.clipperClean);
|
||||
let simple = clip.SimplifyPolygons(clean, cpft.pftNonZero);
|
||||
coff.AddPaths(simple, cjnt.jtMiter, cety.etClosedPolygon);
|
||||
});
|
||||
|
||||
coff.Execute(ctre, distance * fact);
|
||||
polys = fromClipperTree(ctre, z, null, null, min);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* by "over" expanding then contracting, this causes shells that are too
|
||||
* close together to merge and cancel out. it's a more expensive operation
|
||||
* but prevents shells that are too close together to extrude properly.
|
||||
*
|
||||
* @param {Polygon[]} polys
|
||||
* @param {number} dist1 first offset distance
|
||||
* @param {number} dist2 2nd thru last offset distance
|
||||
* @param {Polygon[]} out optional collector
|
||||
* @param {number} count offset passes (0 == until no space left)
|
||||
* @param {Function} collector receives output of each pass
|
||||
* @param {Function} thins receives output of each pass
|
||||
* @param {number} [z] defaults to 0
|
||||
* @returns {Polygon[]} last offset
|
||||
*/
|
||||
function expand2(polys, dist1, dist2, out, count, collector, thins, z) {
|
||||
// prepare alignments for clipper lib
|
||||
alignWindings(polys);
|
||||
polys.forEach(function(poly) {
|
||||
if (poly.inner) setWinding(poly.inner, !poly.isClockwise());
|
||||
});
|
||||
|
||||
let 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(),
|
||||
orig = polys,
|
||||
over = dist1 * 0.45;
|
||||
|
||||
// inset
|
||||
polys.forEach(function(poly) {
|
||||
let clean = clip.CleanPolygons(poly.toClipper(), CONF.clipperClean);
|
||||
let simple = clip.SimplifyPolygons(clean, cpft.pftNonZero);
|
||||
coff.AddPaths(simple, cjnt.jtMiter, cety.etClosedPolygon);
|
||||
});
|
||||
|
||||
coff.Execute(ctre, (dist1 + over) * fact);
|
||||
polys = fromClipperTree(ctre, z);
|
||||
|
||||
// outset
|
||||
coff = new clib.ClipperOffset();
|
||||
ctre = new clib.PolyTree();
|
||||
polys.forEach(function(poly) {
|
||||
let clean = clip.CleanPolygons(poly.toClipper(), CONF.clipperClean);
|
||||
let simple = clip.SimplifyPolygons(clean, cpft.pftNonZero);
|
||||
coff.AddPaths(simple, cjnt.jtMiter, cety.etClosedPolygon);
|
||||
});
|
||||
coff.Execute(ctre, -over * fact);
|
||||
polys = fromClipperTree(ctre, z);
|
||||
|
||||
// detect possible thin walls
|
||||
if (thins) {
|
||||
let circ1 = sumCirc(orig),
|
||||
circ2 = sumCirc(polys),
|
||||
diff = Math.abs(1 - (circ1 / circ2));
|
||||
|
||||
if (diff > 0.2) {
|
||||
thins(orig, out.length ? polys : null, diff, -over);
|
||||
}
|
||||
}
|
||||
|
||||
// process
|
||||
if (out) out.appendAll(polys);
|
||||
if (collector) collector(polys, count);
|
||||
if ((count === 0 || count > 1) && polys.length > 0) {
|
||||
expand2(polys, dist2 || dist1, dist2, out, count > 0 ? count-1 : 0, collector, thins, z);
|
||||
}
|
||||
|
||||
return polys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Polygon} poly input
|
||||
* @param {Polygon[]} traces output
|
||||
* @param {number} offset distance to offset
|
||||
* @param {number} count number of offsets
|
||||
* @param {number} depth current depth (count) into offsets
|
||||
* @param {Polygon[]} [last]
|
||||
* @param {Polygon[]} [first]
|
||||
*/
|
||||
function trace2count(poly, traces, offset, count, depth, last, first) {
|
||||
if (count === 0) {
|
||||
if (last) last.append(poly);
|
||||
return;
|
||||
}
|
||||
|
||||
// offset polygon to outer traces array
|
||||
let 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);
|
||||
});
|
||||
}
|
||||
|
||||
let 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 {number} [minLen]
|
||||
* @param {number} [maxLen]
|
||||
* @returns {Point[]} supplied output or new array
|
||||
*/
|
||||
function fillArea(polys, angle, spacing, output, minLen, maxLen) {
|
||||
if (polys.length === 0) return;
|
||||
|
||||
let 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,
|
||||
Math.cos(angle * DEG2RAD) * spacing,
|
||||
Math.sin(angle * DEG2RAD) * spacing
|
||||
);
|
||||
|
||||
// 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
|
||||
let 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),
|
||||
maxlen = BASE.config.clipper * (maxLen || 0),
|
||||
lines = [];
|
||||
|
||||
// store origin as start/affinity point for fill
|
||||
rayint.origin = newPoint(start.x, start.y, start.z);
|
||||
|
||||
for (i = 0; i < steps; i++) {
|
||||
let 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);
|
||||
|
||||
lines = [];
|
||||
|
||||
if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftNonZero, cfil.pftEvenOdd)) {
|
||||
ctre.m_AllPolys.forEach(function(poly) {
|
||||
if (minlen || maxlen) {
|
||||
let plen = clib.JS.PerimeterOfPath(poly.m_polygon, false, 1);
|
||||
if (minlen && plen < minlen) return;
|
||||
if (maxlen && plen > maxlen) return;
|
||||
}
|
||||
let p1 = newPoint(null,null,zpos,null,poly.m_polygon[0]);
|
||||
let p2 = newPoint(null,null,zpos,null,poly.m_polygon[1]);
|
||||
let od = rayint.origin.distToLineNew(p1,p2) / spacing;
|
||||
lines.push([p1, p2, od]);
|
||||
});
|
||||
}
|
||||
|
||||
lines.sort(function(a,b) {
|
||||
return a[2] - b[2];
|
||||
})
|
||||
|
||||
lines.forEach(function(line) {
|
||||
let dist = Math.round(line[2]);
|
||||
line[0].index = dist;
|
||||
line[1].index = dist;
|
||||
rayint.push(line[0]);
|
||||
rayint.push(line[1]);
|
||||
})
|
||||
|
||||
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) {
|
||||
let 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) {
|
||||
let polygon = polygons[i++],
|
||||
pp = polygon.points,
|
||||
pl = pp.length,
|
||||
dbug = BASE.debug,
|
||||
debug = false;
|
||||
for (let j = 0; j < pl; j++) {
|
||||
let 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) {
|
||||
let 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)) {
|
||||
let 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) {
|
||||
let 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) {
|
||||
let 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) {
|
||||
let 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) {
|
||||
let np = [];
|
||||
for (i = 0; i < points.length; i++) {
|
||||
let p = points[i];
|
||||
if (!p.del) {
|
||||
np.push(p);
|
||||
}
|
||||
}
|
||||
points = np;
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
function fingerprint(polys) {
|
||||
let finger = [];
|
||||
flatten(polys).sort((a,b) => {
|
||||
return a.area() > b.area();
|
||||
}).forEach(p => {
|
||||
finger.push(p.area());
|
||||
finger.push(p.perimeter());
|
||||
finger.push(p.bounds);
|
||||
});
|
||||
return finger;
|
||||
}
|
||||
|
||||
function fingerprintCompare(a, b) {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
if (!a || !b) {
|
||||
return false;
|
||||
}
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i=0; i<a.length; i += 3) {
|
||||
if (Math.abs(a[i] - b[i]) > 0.001) {
|
||||
return false;
|
||||
}
|
||||
if (Math.abs(a[i+1] - b[i+1]) > 0.0001) {
|
||||
return false;
|
||||
}
|
||||
if (Math.abs(a[i+2].centerx() - b[i+2].centerx()) > 0.0001) {
|
||||
return false;
|
||||
}
|
||||
if (Math.abs(a[i+2].centery() - b[i+2].centery()) > 0.0001) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
})();
|
||||
97
src/geo/render.js
Normal file
97
src/geo/render.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (!self.base) self.base = {};
|
||||
if (self.base.render) return;
|
||||
|
||||
const BASE = self.base,
|
||||
materialCache = {},
|
||||
line_width = 1;
|
||||
|
||||
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;
|
||||
let lines = new THREE.Geometry(),
|
||||
hash = {},
|
||||
added = 0;
|
||||
for (let i = 0; i < points.length; i += 3) {
|
||||
let 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;
|
||||
let 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) {
|
||||
let 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);
|
||||
}
|
||||
|
||||
})();
|
||||
130
src/geo/slope.js
Normal file
130
src/geo/slope.js
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.base.Slope) return;
|
||||
|
||||
const BASE = self.base,
|
||||
CONF = BASE.config,
|
||||
ABS = Math.abs,
|
||||
PRO = 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)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
/** ******************************************************************
|
||||
* Slope Prototype Functions
|
||||
******************************************************************* */
|
||||
|
||||
PRO.toString = function() {
|
||||
return [this.dx, this.dy, this.angle].join(',');
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Slope} s
|
||||
* @returns {boolean}
|
||||
*/
|
||||
PRO.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
|
||||
let 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}
|
||||
*/
|
||||
PRO.normal = function() {
|
||||
let t = this.dx;
|
||||
this.dx = -this.dy;
|
||||
this.dy = t;
|
||||
this.angle = Math.atan2(this.dy, this.dx) * RAD2DEG;
|
||||
return this;
|
||||
};
|
||||
|
||||
PRO.toUnit = function() {
|
||||
let max = Math.max(ABS(this.dx), ABS(this.dy));
|
||||
this.dx = this.dx / max;
|
||||
this.dy = this.dy / max;
|
||||
return this;
|
||||
};
|
||||
|
||||
PRO.factor = function(f) {
|
||||
this.dx *= f;
|
||||
this.dy *= f;
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* reverse (180 degree) slope
|
||||
*
|
||||
* @returns {Slope}
|
||||
*/
|
||||
PRO.invert = function() {
|
||||
this.dx = -this.dx;
|
||||
this.dy = -this.dy;
|
||||
this.angle = 360 - this.angle;
|
||||
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);
|
||||
}
|
||||
|
||||
})();
|
||||
133
src/geo/wasm.js
Normal file
133
src/geo/wasm.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
// only active in workers
|
||||
if (!self.window) (function() {
|
||||
|
||||
if (!self.geo) self.geo = {};
|
||||
|
||||
const factor = self.base.config.clipper;
|
||||
const geo = self.geo;
|
||||
|
||||
geo.poly = { offset : polyOffset };
|
||||
|
||||
function log() {
|
||||
console.log(...arguments);
|
||||
}
|
||||
|
||||
function writePoly(view, poly, inner) {
|
||||
if (inner) {
|
||||
poly.setCounterClockwise();
|
||||
} else {
|
||||
poly.setClockwise();
|
||||
}
|
||||
let count = 1;
|
||||
let points = poly.points;
|
||||
let inners = poly.inner;
|
||||
view.writeU16(points.length, true);
|
||||
for (let i=0, il=points.length; i<il; i++) {
|
||||
let point = points[i];
|
||||
view.writeI32((point.x * factor)|0, true);
|
||||
view.writeI32((point.y * factor)|0, true);
|
||||
}
|
||||
if (inners) {
|
||||
for (let i=0, il=inners.length; i<il; i++) {
|
||||
count += writePoly(view, inners[i], true);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function readPoly(view, z) {
|
||||
let points = view.readU16(true);
|
||||
if (points === 0) return;
|
||||
let poly = self.base.newPolygon();
|
||||
while (points-- > 0) {
|
||||
poly.add(view.readI32(true)/factor, view.readI32(true)/factor, z || 0);
|
||||
}
|
||||
return poly;
|
||||
}
|
||||
|
||||
function polyOffset(polys, offset, z) {
|
||||
let wasm = geo.wasm,
|
||||
memat = wasm.malloc(1024 * 128),
|
||||
writer = new DataWriter(wasm.heap, memat),
|
||||
pcount = 0;
|
||||
polys.forEach(poly => pcount += writePoly(writer, poly));
|
||||
let resat = wasm.offset(memat, pcount, offset * factor),
|
||||
reader = new DataReader(wasm.heap, resat),
|
||||
out = [];
|
||||
for (;;) {
|
||||
let poly = readPoly(reader, z);
|
||||
if (poly) {
|
||||
out.push(poly);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
wasm.free(memat);
|
||||
return polyNest(out);
|
||||
}
|
||||
|
||||
// nest closed polygons without existing parent / child relationships
|
||||
function polyNest(polys) {
|
||||
polys.sort((a,b) => {
|
||||
return b.bounds.minx - a.bounds.minx;
|
||||
});
|
||||
// from smallest to largest, check for enclosing bounds and nest
|
||||
for (let i=0, il=polys.length; i<il; i++) {
|
||||
let smaller = polys[i];
|
||||
// prevent parent poly from being consumed
|
||||
if (smaller.inner) continue;
|
||||
for (let j=i+1; j<il; j++) {
|
||||
let larger = polys[j];
|
||||
if (larger.bounds.contains(smaller.bounds)) {
|
||||
larger.addInner(smaller);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let tops = [];
|
||||
for (let i=0, il=polys.length; i<il; i++) {
|
||||
let poly = polys[i];
|
||||
if (!poly.parent) {
|
||||
tops.push(poly);
|
||||
}
|
||||
}
|
||||
return tops;
|
||||
}
|
||||
|
||||
fetch('/wasm/kiri-geo.wasm')
|
||||
.then(response => response.arrayBuffer())
|
||||
.then(bytes => WebAssembly.instantiate(bytes, {
|
||||
env: {
|
||||
polygon: (a,b) => { console.log('polygon',a,b) },
|
||||
point: (a,b) => { console.log('point',a,b) },
|
||||
abs: (a) => { console.log('abs',a); return Math.abs(a) }
|
||||
},
|
||||
wasi_snapshot_preview1: {
|
||||
args_get: (count,bufsize) => { return 0 },
|
||||
args_sizes_get: (count,bufsize) => { },
|
||||
environ_get: (count,bufsize) => { return 0 },
|
||||
environ_sizes_get: (count,bufsize) => { },
|
||||
proc_exit: (code) => { return code }
|
||||
}
|
||||
}))
|
||||
.then(results => {
|
||||
let {module, instance} = results;
|
||||
let {exports} = instance;
|
||||
let heap = new DataView(exports.memory.buffer);
|
||||
let wasm = geo.wasm = {
|
||||
heap,
|
||||
memory: exports.memory,
|
||||
start: exports._start,
|
||||
offset: exports.poly_offset,
|
||||
malloc: exports.mem_get,
|
||||
free: exports.mem_clr
|
||||
};
|
||||
|
||||
wasm.start();
|
||||
});
|
||||
|
||||
})();
|
||||
3
src/ico/cnc.svg
Normal file
3
src/ico/cnc.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="0 0 170 170" width="75px" height="75px"><defs><path d="M15 38.33C15 38.33 15 38.33 15 38.33C15 25.45 25.45 15 38.33 15C44.56 15 75.67 15 131.67 15C131.67 15 131.67 15 131.67 15C137.85 15 143.79 17.46 148.17 21.83C152.54 26.21 155 32.15 155 38.33C155 47.67 155 122.33 155 131.67C155 144.55 144.55 155 131.67 155C122.33 155 47.67 155 38.33 155C25.45 155 15 144.55 15 131.67C15 119.22 15 88.11 15 38.33Z" id="e3irHaXr4"></path><path d="M65 15L105 15L105 125L65 125L65 15Z" id="c1YkLol10d"></path><path d="M105 25L65 55" id="b4hxjCK1zy"></path><path d="M105 55L65 85" id="apUaV8mZu"></path><path d="M105 85L65 115" id="bk21dIqOq"></path></defs><g><g><g><g><use xlink:href="#e3irHaXr4" opacity="1" fill="#000000" fill-opacity="0"></use><g><use xlink:href="#e3irHaXr4" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8" stroke-opacity="1"></use></g></g><g><use xlink:href="#c1YkLol10d" opacity="1" fill="#000000" fill-opacity="0"></use><g><use xlink:href="#c1YkLol10d" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#b4hxjCK1zy" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8.01" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#apUaV8mZu" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8.01" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#bk21dIqOq" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8.01" stroke-opacity="1"></use></g></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
3
src/ico/fdm.svg
Normal file
3
src/ico/fdm.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="0 0 170 170" width="75px" height="75px"><defs><path d="M15 38.33C15 38.33 15 38.33 15 38.33C15 25.45 25.45 15 38.33 15C44.56 15 75.67 15 131.67 15C131.67 15 131.67 15 131.67 15C137.85 15 143.79 17.46 148.17 21.83C152.54 26.21 155 32.15 155 38.33C155 47.67 155 122.33 155 131.67C155 144.55 144.55 155 131.67 155C122.33 155 47.67 155 38.33 155C25.45 155 15 144.55 15 131.67C15 119.22 15 88.11 15 38.33Z" id="a14Galv9UF"></path><path d="M45 15L125 15L125 75L45 75L45 15Z" id="c3q839LU3"></path><path d="M35 141.48C53.11 141.48 64.43 141.48 68.96 141.48C77.82 141.48 85 134.3 85 125.45C85 125.45 85 125.45 85 125.45" id="chE0t1xfL"></path><path d="M105 95L85 115L65 95L105 95Z" id="a6K998F3GT"></path><path d="M75 75L95 75L95 95L75 95L75 75Z" id="e1iiMERqAh"></path></defs><g><g><g><g><use xlink:href="#a14Galv9UF" opacity="1" fill="#000000" fill-opacity="0"></use><g><use xlink:href="#a14Galv9UF" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8" stroke-opacity="1"></use></g></g><g><use xlink:href="#c3q839LU3" opacity="1" fill="#000000" fill-opacity="0"></use><g><use xlink:href="#c3q839LU3" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#chE0t1xfL" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8.01" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#a6K998F3GT" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8.01" stroke-opacity="1"></use></g></g><g><use xlink:href="#e1iiMERqAh" opacity="1" fill="#000000" fill-opacity="0"></use><g><use xlink:href="#e1iiMERqAh" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8" stroke-opacity="1"></use></g></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2 KiB |
3
src/ico/laser.svg
Normal file
3
src/ico/laser.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="0 0 170 170" width="75px" height="75px"><defs><path d="M15 38.33C15 38.33 15 38.33 15 38.33C15 25.45 25.45 15 38.33 15C44.56 15 75.67 15 131.67 15C131.67 15 131.67 15 131.67 15C137.85 15 143.79 17.46 148.17 21.83C152.54 26.21 155 32.15 155 38.33C155 47.67 155 122.33 155 131.67C155 144.55 144.55 155 131.67 155C122.33 155 47.67 155 38.33 155C25.45 155 15 144.55 15 131.67C15 119.22 15 88.11 15 38.33Z" id="a4l00mnEny"></path><path d="M65 15L105 15L105 55L65 55L65 15Z" id="bq8oBWrfG"></path><path d="M85 125L85 65" id="aXzHtGbsD"></path><path d="M55 105L75 135" id="akSzh0QKx"></path><path d="M115 105L95 135" id="bloAs9giZ"></path><path d="M35 125L75 145" id="gk6ohj0JL"></path><path d="M135 123.96L95 145" id="e404eoj7L"></path></defs><g><g><g><g><use xlink:href="#a4l00mnEny" opacity="1" fill="#000000" fill-opacity="0"></use><g><use xlink:href="#a4l00mnEny" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8" stroke-opacity="1"></use></g></g><g><use xlink:href="#bq8oBWrfG" opacity="1" fill="#000000" fill-opacity="0"></use><g><use xlink:href="#bq8oBWrfG" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#aXzHtGbsD" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8.01" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#akSzh0QKx" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8.01" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#bloAs9giZ" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8.01" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#gk6ohj0JL" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8.01" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#e404eoj7L" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8.01" stroke-opacity="1"></use></g></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
3
src/ico/sla.svg
Normal file
3
src/ico/sla.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="0 0 170 170" width="75px" height="75px"><defs><path d="M15 38.33C15 38.33 15 38.33 15 38.33C15 25.45 25.45 15 38.33 15C44.56 15 75.67 15 131.67 15C131.67 15 131.67 15 131.67 15C137.85 15 143.79 17.46 148.17 21.83C152.54 26.21 155 32.15 155 38.33C155 47.67 155 122.33 155 131.67C155 144.55 144.55 155 131.67 155C122.33 155 47.67 155 38.33 155C25.45 155 15 144.55 15 131.67C15 119.22 15 88.11 15 38.33Z" id="b3TRNHybxX"></path><path d="M31.88 42.5L138.12 42.5L138.12 47.81L31.88 47.81L31.88 42.5Z" id="aNMkzLwb"></path><path d="M75 21.87L95 21.87L95 41.88L75 41.88L75 21.87Z" id="d1fKKg2Jru"></path><path d="M115.93 107.19C115.93 124.27 102.07 138.13 85 138.13C67.93 138.13 54.07 124.27 54.07 107.19C54.07 90.12 67.93 76.26 85 76.26C102.07 76.26 115.93 90.12 115.93 107.19Z" id="cnyrcTQMg"></path><path d="M53.87 47.81L53.87 107.08" id="a4G9Rr6VQL"></path><path d="M115.76 51.27L115.76 110.53" id="d4hytySQxZ"></path><path d="M69.06 79.69L69.06 47.81" id="b1WQ83ELAA"></path><path d="M100.94 79.69L100.94 47.81" id="f2kNHslaLa"></path><path d="M69.06 58.44L53.13 74.38" id="g67vFJwAFy"></path><path d="M114.68 58.44L100.94 69.06" id="aAcp3z5Kv"></path></defs><g><g><g><use xlink:href="#b3TRNHybxX" opacity="1" fill="#000000" fill-opacity="0"></use><g><use xlink:href="#b3TRNHybxX" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8" stroke-opacity="1"></use></g></g><g><use xlink:href="#aNMkzLwb" opacity="1" fill="#000000" fill-opacity="0"></use><g><use xlink:href="#aNMkzLwb" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8" stroke-opacity="1"></use></g></g><g><use xlink:href="#d1fKKg2Jru" opacity="1" fill="#000000" fill-opacity="0"></use><g><use xlink:href="#d1fKKg2Jru" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8" stroke-opacity="1"></use></g></g><g><use xlink:href="#cnyrcTQMg" opacity="1" fill="#1ea5e6" fill-opacity="0"></use><g><use xlink:href="#cnyrcTQMg" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#a4G9Rr6VQL" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#d4hytySQxZ" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#b1WQ83ELAA" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#f2kNHslaLa" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="8" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#g67vFJwAFy" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="6" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#aAcp3z5Kv" opacity="1" fill-opacity="0" stroke="currentColor" stroke-width="6" stroke-opacity="1"></use></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
11
src/kiri.js
Normal file
11
src/kiri.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function () {
|
||||
|
||||
if (self.kiri) return;
|
||||
|
||||
self.kiri = { };
|
||||
|
||||
})();
|
||||
236
src/kiri/codec.js
Normal file
236
src/kiri/codec.js
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.kiri.codec) return;
|
||||
|
||||
const 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)) {
|
||||
let 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 (let 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;
|
||||
let out = {};
|
||||
for (let 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;
|
||||
let out = {};
|
||||
for (let 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) {
|
||||
let 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),
|
||||
// thinner: encode(this.thinner, state),
|
||||
solids: encode(this.solids, state),
|
||||
fill_lines: encodePointArray(this.fill_lines, state),
|
||||
fill_sparse: encode(this.fill_sparse, state),
|
||||
polish: encode(this.polish, state)
|
||||
};
|
||||
};
|
||||
|
||||
registerDecoder('top', function(v, state) {
|
||||
let top = KIRI.newTop(decode(v.poly, state));
|
||||
|
||||
top.traces = decode(v.traces, state);
|
||||
top.inner = decode(v.inner, state);
|
||||
// top.thinner = decode(v.thinner, state);
|
||||
top.solids = decode(v.solids, state);
|
||||
top.fill_lines = decodePointArray(v.fill_lines, state);
|
||||
top.fill_sparse = decode(v.fill_sparse, state);
|
||||
top.polish = decode(v.polish, state);
|
||||
|
||||
return top;
|
||||
});
|
||||
|
||||
function encodePointArray(points, state) {
|
||||
if (!points) return null;
|
||||
|
||||
let array = new Float32Array(points.length * 3),
|
||||
pos = 0;
|
||||
|
||||
points.forEach(function(point) {
|
||||
if (state.rotate) {
|
||||
if (state.centerz) {
|
||||
point.z -= state.centerz;
|
||||
}
|
||||
let v = new THREE.Vector3(point.x, point.y, point.z);
|
||||
v.applyMatrix4(state.rotate);
|
||||
point = {x: v.x, y: v.y, z: v.z};
|
||||
if (state.centerz) {
|
||||
point.z += state.centerz;
|
||||
}
|
||||
if (state.movez) {
|
||||
point.z -= state.movez;
|
||||
}
|
||||
}
|
||||
array[pos++] = point.x;
|
||||
array[pos++] = point.y;
|
||||
array[pos++] = point.z;
|
||||
});
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
function decodePointArray(array) {
|
||||
if (!array) return null;
|
||||
|
||||
let 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 = {};
|
||||
|
||||
let 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, state),
|
||||
open: this.isOpen(),
|
||||
inner: encode(this.inner, state),
|
||||
parent: encode(this.parent, state),
|
||||
fills: encodePointArray(this.fills, state),
|
||||
depth: this.depth
|
||||
};
|
||||
};
|
||||
|
||||
registerDecoder('poly', function(v, state) {
|
||||
if (!state.poly) state.poly = {};
|
||||
|
||||
if (v.ref) return state.poly[v.ref];
|
||||
|
||||
let 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;
|
||||
});
|
||||
|
||||
|
||||
})();
|
||||
575
src/kiri/conf.js
Normal file
575
src/kiri/conf.js
Normal file
|
|
@ -0,0 +1,575 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.kiri.conf) return;
|
||||
|
||||
const KIRI = self.kiri, CVER = 185, clone = Object.clone;
|
||||
|
||||
function genID() {
|
||||
while (true) {
|
||||
let k = Math.round(Math.random() * 9999999999).toString(36);
|
||||
if (k.length >= 4 && k.length <= 8) return k;
|
||||
}
|
||||
}
|
||||
|
||||
// add fields to o(bject) from d(efault) that are missing
|
||||
// remove fields from o(bject) that don't exist in d(efault)
|
||||
function fill_cull_once(obj, def) {
|
||||
if (!obj) return;
|
||||
// add missing
|
||||
for (let k in def) {
|
||||
if (def.hasOwnProperty(k)) {
|
||||
let okv = obj[k];
|
||||
if ((okv === undefined || okv === null)) {
|
||||
// console.log({fill: k});
|
||||
obj[k] = def[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
// remove invalid
|
||||
for (let k in obj) {
|
||||
if (!def.hasOwnProperty(k)) {
|
||||
// console.log({cull: k});
|
||||
delete obj[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fill_cull_many(map, def) {
|
||||
forValues(map, (obj) => { fill_cull_once(obj, def) });
|
||||
}
|
||||
|
||||
function objectMap(o, fn) {
|
||||
for (let [key, val] of Object.entries(o)) {
|
||||
o[key] = fn(val) || val;
|
||||
}
|
||||
}
|
||||
|
||||
function valueOf(val, dv) {
|
||||
return typeof(val) !== 'undefined' ? val : dv;
|
||||
}
|
||||
|
||||
function forValues(o, fn) {
|
||||
Object.values(o).forEach(v => fn(v));
|
||||
}
|
||||
|
||||
function device_v1_to_v2(device) {
|
||||
if (device && device.filamentSize) {
|
||||
device.extruders = [{
|
||||
extFilament: device.filamentSize,
|
||||
extNozzle: device.nozzleSize,
|
||||
extSelect: ["T0"],
|
||||
extOffsetX: 0,
|
||||
extOffsetY: 0
|
||||
}];
|
||||
delete device.filamentSize;
|
||||
delete device.nozzleSize;
|
||||
}
|
||||
}
|
||||
|
||||
// convert default filter (from server) into device structure
|
||||
function device_from_code(code,mode) {
|
||||
// presence of internal field indicates already converted
|
||||
if (code.internal >= 0) return code;
|
||||
|
||||
let API = KIRI.api,
|
||||
cmd = code.cmd || {},
|
||||
set = code.settings || {},
|
||||
ext = code.extruders;
|
||||
|
||||
// currently causes unecessary fills and culls because
|
||||
// it's not mode and device type sensitive
|
||||
let device = {
|
||||
noclone: valueOf(code.no_clone, false),
|
||||
mode: mode || code.mode || '',
|
||||
internal: 0,
|
||||
bedHeight: valueOf(set.bed_height, 2.5),
|
||||
bedWidth: valueOf(set.bed_width, 300),
|
||||
bedDepth: valueOf(set.bed_depth, 175),
|
||||
bedRound: valueOf(set.bed_circle, false),
|
||||
maxHeight: valueOf(set.build_height, 150),
|
||||
originCenter: valueOf(set.origin_center, false),
|
||||
extrudeAbs: valueOf(set.extrude_abs, false),
|
||||
spindleMax: valueOf(set.spindle_max, 0),
|
||||
gcodeFan: valueOf(cmd.fan_power, ''),
|
||||
gcodeTrack: valueOf(cmd.progress, ''),
|
||||
gcodeLayer: valueOf(cmd.layer, []),
|
||||
gcodePre: valueOf(code.pre, []),
|
||||
gcodePost: valueOf(code.post, []),
|
||||
gcodeProc: valueOf(code.proc, ''),
|
||||
gcodePause: valueOf(code.pause, []),
|
||||
gcodeDwell: valueOf(code.dwell, []),
|
||||
gcodeSpindle: valueOf(cmd.spindle, []),
|
||||
gcodeChange: valueOf(code['tool-change'], []),
|
||||
gcodeFExt: valueOf(code['file-ext'], 'gcode'),
|
||||
gcodeSpace: valueOf(code['token-space'], true),
|
||||
gcodeStrip: valueOf(code['strip-comments'], false),
|
||||
gcodeLaserOn: valueOf(code['laser-on'], []),
|
||||
gcodeLaserOff: valueOf(code['laser-off'], []),
|
||||
extruders: []
|
||||
};
|
||||
|
||||
if (ext) {
|
||||
// synthesize extruders from new style settings
|
||||
ext.forEach(rec => {
|
||||
let e = API.clone(CONF.defaults.fdm.d.extruders[0]);
|
||||
if (rec.nozzle) e.extNozzle = rec.nozzle;
|
||||
if (rec.filament) e.extFilament = rec.filament;
|
||||
if (rec.offset_x) e.extOffsetX = rec.offset_x;
|
||||
if (rec.offset_y) e.extOffsetY = rec.offset_y;
|
||||
if (rec.select) e.extSelect = rec.select;
|
||||
device.extruders.push(e);
|
||||
});
|
||||
} else {
|
||||
// synthesize extruders from old style settings
|
||||
device.extruders = [API.clone(CONF.defaults.fdm.d.extruders[0])];
|
||||
device.extruders[0].extNozzle = valueOf(set.nozzle_size, 0.4);
|
||||
device.extruders[0].extFilament = valueOf(set.filament_diameter, 1.75);
|
||||
}
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
// ensure settings structure is up-to-date
|
||||
function normalize(settings) {
|
||||
let API = KIRI.api,
|
||||
defaults = CONF.defaults,
|
||||
template = CONF.template,
|
||||
mode = settings.mode.toLowerCase(),
|
||||
default_dev = defaults[mode].d,
|
||||
default_pro = defaults[mode].p;
|
||||
|
||||
// v1 to v2 changed FDM extruder / nozzle / filament structure
|
||||
if (settings.ver != CVER) {
|
||||
// backup settings before upgrade
|
||||
API.sdb.setItem(`ws-settings-${Date.now()}`, JSON.stringify(settings));
|
||||
device_v1_to_v2(settings.device);
|
||||
device_v1_to_v2(settings.cdev.FDM);
|
||||
objectMap(settings.devices, dev => {
|
||||
return dev ? device_from_code(dev) : dev;
|
||||
});
|
||||
settings.ver = CVER;
|
||||
}
|
||||
|
||||
fill_cull_once(settings, template);
|
||||
fill_cull_once(settings.device, default_dev);
|
||||
fill_cull_once(settings.process, default_pro);
|
||||
fill_cull_once(settings.cdev, template.cdev);
|
||||
fill_cull_once(settings.cproc, template.cproc);
|
||||
fill_cull_once(settings.sproc, template.sproc);
|
||||
fill_cull_once(settings.defaults, template.defaults);
|
||||
fill_cull_once(settings.cdev.FDM, defaults.fdm.d);
|
||||
fill_cull_once(settings.cdev.SLA, defaults.sla.d);
|
||||
fill_cull_once(settings.cdev.CAM, defaults.cam.d);
|
||||
fill_cull_once(settings.cdev.LASER, defaults.laser.d);
|
||||
fill_cull_many(settings.sproc.FDM, defaults.fdm.p);
|
||||
fill_cull_many(settings.sproc.SLA, defaults.sla.p);
|
||||
fill_cull_many(settings.sproc.CAM, defaults.cam.p);
|
||||
fill_cull_many(settings.sproc.LASER, defaults.laser.p);
|
||||
fill_cull_once(settings.controller, template.controller);
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
let CONF = KIRI.conf = {
|
||||
// --------------- helper functions
|
||||
normalize: normalize,
|
||||
device_from_code: device_from_code,
|
||||
// ---------------
|
||||
MODES: {
|
||||
FDM: 1, // fused deposition modeling (also FFF)
|
||||
LASER: 2, // laser cutters
|
||||
CAM: 3, // 3 axis milling/machining
|
||||
SLA: 4 // cured resin printers
|
||||
},
|
||||
VIEWS: {
|
||||
ARRANGE: 1,
|
||||
SLICE: 2,
|
||||
PREVIEW: 3
|
||||
},
|
||||
// --------------- device and process defaults
|
||||
defaults: {
|
||||
fdm:{
|
||||
// device defaults FDM:Device
|
||||
d:{
|
||||
new: true,
|
||||
mode: "",
|
||||
internal: 0,
|
||||
bedWidth: 300,
|
||||
bedDepth: 175,
|
||||
bedHeight: 2.5,
|
||||
bedRound: false,
|
||||
originCenter: false,
|
||||
maxHeight: 150,
|
||||
gcodePre: [],
|
||||
gcodePost: [],
|
||||
gcodePause: [],
|
||||
gcodeProc: "",
|
||||
gcodeFan: "",
|
||||
gcodeTrack: "",
|
||||
gcodeLayer: [],
|
||||
gcodeFExt: "",
|
||||
extruders:[{
|
||||
extFilament: 1.75,
|
||||
extNozzle: 0.4,
|
||||
extSelect: ["T0"],
|
||||
extOffsetX: 0,
|
||||
extOffsetY: 0
|
||||
}]
|
||||
},
|
||||
// process defaults FDM:Process
|
||||
p:{
|
||||
processName: "default",
|
||||
sliceHeight: 0.25,
|
||||
sliceShells: 3,
|
||||
sliceFillAngle: 45,
|
||||
sliceFillOverlap: 0.3,
|
||||
sliceFillSparse: 0.5,
|
||||
sliceFillType: "hex",
|
||||
sliceSupportEnable: false,
|
||||
sliceSupportDensity: 0.25,
|
||||
sliceSupportOffset: 1.0,
|
||||
sliceSupportGap: 1,
|
||||
sliceSupportSize: 10,
|
||||
sliceSupportArea: 1,
|
||||
sliceSupportExtra: 0,
|
||||
sliceSupportSpan: 6,
|
||||
sliceSupportNozzle: 0,
|
||||
sliceSolidMinArea: 1,
|
||||
sliceSolidLayers: 3,
|
||||
sliceBottomLayers: 3,
|
||||
sliceTopLayers: 3,
|
||||
firstSliceHeight: 0.25,
|
||||
firstLayerRate: 30,
|
||||
firstLayerFillRate: 35,
|
||||
firstLayerPrintMult: 1.0,
|
||||
firstLayerNozzleTemp: 200,
|
||||
firstLayerBedTemp: 60,
|
||||
outputRaft: false,
|
||||
outputRaftSpacing: 0.2,
|
||||
outputTemp: 200,
|
||||
outputFanMax: 255,
|
||||
outputBedTemp: 60,
|
||||
outputFeedrate: 50,
|
||||
outputFinishrate: 50,
|
||||
outputSeekrate: 80,
|
||||
outputShellMult: 1.25,
|
||||
outputFillMult: 1.25,
|
||||
outputSparseMult: 1.25,
|
||||
outputFanLayer: 1,
|
||||
outputRetractDist: 1.5,
|
||||
outputRetractSpeed: 40,
|
||||
outputRetractDwell: 20,
|
||||
outputBrimCount: 2,
|
||||
outputBrimOffset: 2,
|
||||
outputShortPoly: 100.0,
|
||||
outputMinSpeed: 10.0,
|
||||
outputCoastDist: 0,
|
||||
outputWipeDistance: 0,
|
||||
outputLayerRetract: false,
|
||||
outputOriginCenter: true,
|
||||
outputInvertX: false,
|
||||
outputInvertY: false,
|
||||
detectThinWalls: false,
|
||||
sliceMinHeight: 0,
|
||||
zHopDistance: 0.2,
|
||||
antiBacklash: 1,
|
||||
gcodePause: "",
|
||||
sliceRotation: 0
|
||||
}
|
||||
},
|
||||
sla:{
|
||||
// device defaults SLA:Device
|
||||
d:{
|
||||
new: true,
|
||||
deviceName: "",
|
||||
noclone: false,
|
||||
mode: "",
|
||||
internal: 0,
|
||||
bedWidth: 150,
|
||||
bedDepth: 150,
|
||||
bedHeight: 1.5,
|
||||
maxHeight: 150
|
||||
},
|
||||
// process defaults SLA:Process
|
||||
p:{
|
||||
processName: "default",
|
||||
slaSlice: 0.05,
|
||||
slaShell: 0.00,
|
||||
slaOpenTop: false,
|
||||
slaOpenBase: false,
|
||||
slaAntiAlias: 1,
|
||||
slaLayerOff: 0.1,
|
||||
slaLayerOn: 7,
|
||||
slaPeelDist: 6,
|
||||
slaPeelLiftRate: 1.5,
|
||||
slaPeelDropRate: 3,
|
||||
slaBaseLayers: 5,
|
||||
slaBaseOff: 0.1,
|
||||
slaBaseOn: 30,
|
||||
slaBasePeelDist: 6,
|
||||
slaBasePeelLiftRate: 1.5,
|
||||
slaFillDensity: 0,
|
||||
slaFillLine: 0.5,
|
||||
slaFirstOffset: 0,
|
||||
slaSupportLayers: 10,
|
||||
slaSupportDensity: 0.5,
|
||||
slaSupportSize: 0.6,
|
||||
slaSupportPoints: 4,
|
||||
slaSupportGap: 10,
|
||||
slaSupportEnable: false
|
||||
}
|
||||
},
|
||||
cam:{
|
||||
// device defaults CAM:Device
|
||||
d:{
|
||||
new: true,
|
||||
mode: "",
|
||||
internal: 0,
|
||||
bedWidth: 300,
|
||||
bedDepth: 175,
|
||||
bedHeight: 2.5,
|
||||
originCenter: false,
|
||||
spindleMax: 0,
|
||||
gcodePre: [],
|
||||
gcodePost: [],
|
||||
gcodeFExt: "",
|
||||
gcodeSpace: true,
|
||||
gcodeStrip: true,
|
||||
gcodeDwell: ["G4 P{time}"],
|
||||
gcodeChange: ["M6 T{tool}"],
|
||||
gcodeSpindle: ["M3 S{speed}"]
|
||||
},
|
||||
// process defaults CAM:Process
|
||||
p:{
|
||||
processName: "default",
|
||||
camFastFeed: 6000,
|
||||
camFastFeedZ: 300,
|
||||
roughingTool: 1000,
|
||||
roughingSpindle: 1000,
|
||||
roughingDown: 2,
|
||||
roughingOver: 0.5,
|
||||
roughingSpeed: 1000,
|
||||
roughingPlunge: 250,
|
||||
roughingStock: 0,
|
||||
roughingPocket: true,
|
||||
roughingOn: true,
|
||||
finishingTool: 1000,
|
||||
finishingSpindle: 1000,
|
||||
finishingDown: 3,
|
||||
finishingOver: 0.5,
|
||||
finishingAngle: 85,
|
||||
finishingSpeed: 800,
|
||||
finishingPlunge: 250,
|
||||
finishingOn: true,
|
||||
finishingXOn: true,
|
||||
finishingYOn: true,
|
||||
finishCurvesOnly: false,
|
||||
drillTool: 1000,
|
||||
drillSpindle: 1000,
|
||||
drillDownSpeed: 250,
|
||||
drillDown: 5,
|
||||
drillDwell: 250,
|
||||
drillLift: 2,
|
||||
drillingOn: false,
|
||||
camTabsAngle: 0,
|
||||
camTabsCount: 4,
|
||||
camTabsWidth: 5,
|
||||
camTabsHeight: 5,
|
||||
camTabsOn: false,
|
||||
camPocketOnlyRough: false,
|
||||
camPocketOnlyFinish: false,
|
||||
camDepthFirst: true,
|
||||
camEaseDown: false,
|
||||
camOriginTop: true,
|
||||
camTolerance: 0.15,
|
||||
camZTopOffset: 0,
|
||||
camZBottom: 0,
|
||||
camZClearance: 1,
|
||||
camStockX: 0,
|
||||
camStockY: 0,
|
||||
camStockZ: 0,
|
||||
camStockOffset: true,
|
||||
outputClockwise: false,
|
||||
outputOriginCenter: true,
|
||||
outputInvertX: false,
|
||||
outputInvertY: false
|
||||
}
|
||||
},
|
||||
laser: {
|
||||
// device defaults Laser:Device
|
||||
d:{
|
||||
new: true,
|
||||
mode: "",
|
||||
internal: 0,
|
||||
bedWidth: 300,
|
||||
bedDepth: 175,
|
||||
bedHeight: 2.5,
|
||||
gcodePre: [],
|
||||
gcodePost: [],
|
||||
gcodeFExt: "",
|
||||
gcodeSpace: true,
|
||||
gcodeLaserOn: ["M106 S{power}"],
|
||||
gcodeLaserOff: ["M107"]
|
||||
},
|
||||
// process defaults Laser:Process
|
||||
p:{
|
||||
processName: "default",
|
||||
laserOffset: 0.25,
|
||||
laserSliceHeight: 1,
|
||||
laserSliceSingle: false,
|
||||
outputTileSpacing: 1,
|
||||
outputTileScaling: 1,
|
||||
outputLaserPower: 100,
|
||||
outputLaserSpeed: 1000,
|
||||
outputLaserGroup: true,
|
||||
outputLaserMerged: false,
|
||||
outputOriginCenter: true,
|
||||
outputInvertX: false,
|
||||
outputInvertY: false
|
||||
}
|
||||
}
|
||||
},
|
||||
// --------------- settings template
|
||||
template: {
|
||||
// CAM only
|
||||
bounds: {},
|
||||
origin: {},
|
||||
stock: {},
|
||||
tools:[
|
||||
{
|
||||
id: 1000,
|
||||
number: 1,
|
||||
type: "endmill",
|
||||
name: "end 1/4",
|
||||
metric: false,
|
||||
shaft_diam: 0.25,
|
||||
shaft_len: 1,
|
||||
flute_diam: 0.25,
|
||||
flute_len: 2,
|
||||
// taper_angle: 70,
|
||||
taper_tip: 0,
|
||||
},
|
||||
{
|
||||
id: 1001,
|
||||
number: 2,
|
||||
type: "endmill",
|
||||
name: "end 1/8",
|
||||
metric: false,
|
||||
shaft_diam: 0.125,
|
||||
shaft_len: 1,
|
||||
flute_diam: 0.125,
|
||||
flute_len: 1.5,
|
||||
// taper_angle: 70,
|
||||
taper_tip: 0,
|
||||
},
|
||||
{
|
||||
id: 1002,
|
||||
number: 3,
|
||||
type: "endmill",
|
||||
name: "end 1/16",
|
||||
metric: false,
|
||||
shaft_diam: 0.0625,
|
||||
shaft_len: 1,
|
||||
flute_diam: 0.0625,
|
||||
flute_len: 1.5,
|
||||
// taper_angle: 70,
|
||||
taper_tip: 0,
|
||||
}
|
||||
],
|
||||
// currently selected device
|
||||
device:{},
|
||||
// currently selected process
|
||||
process:{},
|
||||
// current process name by mode
|
||||
cproc:{
|
||||
FDM: "default",
|
||||
SLA: "default",
|
||||
CAM: "default",
|
||||
LASER: "default"
|
||||
},
|
||||
// stored processes by mode
|
||||
sproc:{
|
||||
FDM: {},
|
||||
SLA: {},
|
||||
CAM: {},
|
||||
LASER: {}
|
||||
},
|
||||
// current device name by mode
|
||||
filter:{
|
||||
FDM: "Any.Generic.Marlin",
|
||||
SLA: "Anycubic.Photon",
|
||||
CAM: "Any.Generic.Grbl",
|
||||
LASER: "Any.Generic.Laser"
|
||||
},
|
||||
// stored device by mode
|
||||
cdev: {
|
||||
FDM: null,
|
||||
SLA: null,
|
||||
CAM: null,
|
||||
LASER: null
|
||||
},
|
||||
// custom devices by name (all modes)
|
||||
devices:{},
|
||||
// favorited devices (all modes)
|
||||
favorites:{},
|
||||
// map of device to last process setting (name)
|
||||
devproc: {},
|
||||
layers:{
|
||||
layerOutline: true,
|
||||
layerTrace: true,
|
||||
layerFacing: true,
|
||||
layerRough: true,
|
||||
layerFinish: true,
|
||||
layerFinishX: true,
|
||||
layerFinishY: true,
|
||||
layerDelta: false,
|
||||
layerSolid: false,
|
||||
layerSparse: true,
|
||||
layerFill: true,
|
||||
layerSupport: true,
|
||||
layerPrint: false,
|
||||
layerMoves: false
|
||||
},
|
||||
controller:{
|
||||
view: null,
|
||||
dark: false,
|
||||
expert: false,
|
||||
zoomSpeed: 1.0,
|
||||
thinRender: false,
|
||||
reverseZoom: true,
|
||||
showOrigin: false,
|
||||
freeLayout: true,
|
||||
autoLayout: true,
|
||||
alignTop: true,
|
||||
units: "mm",
|
||||
exportOcto: true,
|
||||
exportGhost: false,
|
||||
exportLocal: false
|
||||
},
|
||||
// for passing temporary slice hints (topo currently)
|
||||
synth: {},
|
||||
// widget extra info for slicing (extruder mapping)
|
||||
widget: {},
|
||||
mode: 'FDM',
|
||||
id: genID(),
|
||||
ver: CVER
|
||||
}
|
||||
};
|
||||
|
||||
let settings = CONF.template;
|
||||
|
||||
// seed defaults. will get culled on save
|
||||
settings.sproc.FDM.default = clone(settings.process);
|
||||
settings.sproc.SLA.default = clone(settings.process);
|
||||
settings.sproc.CAM.default = clone(settings.process);
|
||||
settings.sproc.LASER.default = clone(settings.process);
|
||||
settings.cdev.FDM = clone(settings.device);
|
||||
settings.cdev.SLA = clone(settings.device);
|
||||
settings.cdev.CAM = clone(settings.device);
|
||||
settings.cdev.LASER = clone(settings.device);
|
||||
|
||||
})();
|
||||
190
src/kiri/db.js
Normal file
190
src/kiri/db.js
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
const KIRI = self.kiri, DP = Catalog.prototype;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
function Catalog(motodb, decimate) {
|
||||
let store = this;
|
||||
store.db = motodb;
|
||||
store.files = {};
|
||||
store.listeners = [];
|
||||
store.autodec = decimate;
|
||||
store.deferredHandler = null;
|
||||
store.refresh();
|
||||
}
|
||||
|
||||
KIRI.openCatalog = function(motodb,decimate) {
|
||||
return new Catalog(motodb,decimate);
|
||||
};
|
||||
|
||||
DP.refresh = function() {
|
||||
let store = this;
|
||||
store.db.get('files', function(files) {
|
||||
if (files) {
|
||||
store.files = files;
|
||||
notifyFileListeners(store);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
DP.wipe = function() {
|
||||
let 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 (let 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) {
|
||||
let 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);
|
||||
});
|
||||
};
|
||||
|
||||
DP.rename = function(name, newname, callback) {
|
||||
if (!this.files[name]) return callback({error: 'no such file'});
|
||||
if (!newname || newname == name) return callback({error: 'invalid new name'});
|
||||
let done = 0;
|
||||
let error = [];
|
||||
let store = this;
|
||||
function complete(ok, err) {
|
||||
if (err) error.push(err);
|
||||
if (++done === 2) {
|
||||
store.files[newname] = store.files[name];
|
||||
delete store.files[name];
|
||||
saveFileList(store);
|
||||
store.db.remove(`fdec-${name}`);
|
||||
store.db.remove(`file-${name}`);
|
||||
callback(error.length ? {error} : {});
|
||||
}
|
||||
}
|
||||
store.db.get(`fdec-${name}`, (vertices) => {
|
||||
if (!vertices) return complete(false, 'no decimation');
|
||||
store.db.put(`fdec-${newname}`, vertices, complete);
|
||||
});
|
||||
store.db.get(`file-${name}`, (vertices) => {
|
||||
if (!vertices) return complete(false, 'no raw file');
|
||||
store.db.put(`file-${newname}`, vertices, complete);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {String} name
|
||||
* @param {Function} callback
|
||||
*/
|
||||
DP.getFile = function(name, callback) {
|
||||
let 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) {
|
||||
let 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);
|
||||
};
|
||||
|
||||
})();
|
||||
504
src/kiri/export.js
Normal file
504
src/kiri/export.js
Normal file
|
|
@ -0,0 +1,504 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.kiri.export) return;
|
||||
|
||||
const KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
API = KIRI.api,
|
||||
SDB = API.sdb,
|
||||
UI = API.ui,
|
||||
STATS = API.stats,
|
||||
MODES = API.const.MODES;
|
||||
|
||||
KIRI.export = exportPrint;
|
||||
|
||||
let printSeq = parseInt(SDB['kiri-print-seq'] || SDB['print-seq'] || "0") + 1;
|
||||
|
||||
function exportPrint() {
|
||||
let currentPrint = API.print.get();
|
||||
if (currentPrint) {
|
||||
API.event.emit('export', API.mode.get());
|
||||
switch (API.mode.get()) {
|
||||
case 'LASER': return exportPrintLaser(currentPrint);
|
||||
case 'FDM': return exportPrintGCODE(currentPrint);
|
||||
case 'CAM': return exportPrintGCODE(currentPrint);
|
||||
case 'SLA': return exportPrintSLA(currentPrint);
|
||||
}
|
||||
} else {
|
||||
API.function.print(exportPrint);
|
||||
}
|
||||
}
|
||||
|
||||
function exportPrintSLA(currentPrint) {
|
||||
if (currentPrint) {
|
||||
if (currentPrint.exported) {
|
||||
KIRI.driver.SLA.printDownload(currentPrint);
|
||||
return;
|
||||
}
|
||||
let lines = [];
|
||||
let times = {};
|
||||
let mark = Date.now();
|
||||
let seq = 1;
|
||||
let segment;
|
||||
currentPrint.export(true, function(line) {
|
||||
if (line.message) {
|
||||
if (segment && segment !== line.message) {
|
||||
let now = Date.now();
|
||||
times[`${seq++}_${segment}`] = now - mark;
|
||||
mark = now;
|
||||
}
|
||||
segment = line.message;
|
||||
}
|
||||
if (line.progress) {
|
||||
API.show.progress(line.progress, "exporting");
|
||||
}
|
||||
if (line.data) {
|
||||
lines.push(line.data);
|
||||
}
|
||||
}, function(done) {
|
||||
currentPrint.exported = true;
|
||||
times[`${seq++}_${segment}`] = Date.now() - mark;
|
||||
console.log(times);
|
||||
API.show.progress(0);
|
||||
currentPrint.sla = { lines, done, API };
|
||||
KIRI.driver.SLA.printDownload(currentPrint);
|
||||
});
|
||||
} else {
|
||||
API.function.print(exportPrintSLA);
|
||||
}
|
||||
}
|
||||
|
||||
function exportPrintGCODE(currentPrint) {
|
||||
if (currentPrint) {
|
||||
currentPrint.exportGCode(true, function(gcode) {
|
||||
exportGCode(gcode,currentPrint);
|
||||
});
|
||||
} else {
|
||||
API.function.print(exportPrint);
|
||||
}
|
||||
}
|
||||
|
||||
function exportPrintLaser(currentPrint) {
|
||||
if (!currentPrint) {
|
||||
return API.function.print(exportPrintLaser);
|
||||
}
|
||||
|
||||
let filename = "laser-"+(new Date().getTime().toString(36));
|
||||
|
||||
function download_svg() {
|
||||
saveAs(new Blob(
|
||||
[currentPrint.exportSVG()],
|
||||
{type:"application/octet-stream"}),
|
||||
$('print-filename').value + ".svg");
|
||||
}
|
||||
|
||||
function download_dxf() {
|
||||
saveAs(new Blob(
|
||||
[currentPrint.exportDXF()],
|
||||
{type:"application/octet-stream"}),
|
||||
$('print-filename').value + ".dxf");
|
||||
}
|
||||
|
||||
function download_gcode() {
|
||||
saveAs(new Blob(
|
||||
[currentPrint.exportLaserGCode()],
|
||||
{type:"application/octet-stream"}),
|
||||
$('print-filename').value + ".gcode");
|
||||
}
|
||||
|
||||
API.ajax("/kiri/output-laser.html", function(html) {
|
||||
let segments = 0;
|
||||
currentPrint.output.forEach(layer => { segments += layer.length });
|
||||
UI.print.innerHTML = html;
|
||||
$('print-filename').value = filename;
|
||||
$('print-lines').value = UTIL.comma(segments);
|
||||
$('print-svg').onclick = download_svg;
|
||||
$('print-dxf').onclick = download_dxf;
|
||||
$('print-lg').onclick = download_gcode;
|
||||
API.modal.show('print');
|
||||
});
|
||||
}
|
||||
|
||||
function exportGCode(gcode, currentPrint) {
|
||||
SDB['kiri-print-seq'] = printSeq++;
|
||||
|
||||
let settings = API.conf.get(),
|
||||
MODE = API.mode.get_id(),
|
||||
pre = (MODE === MODES.CAM ? "cnc-" : "print-") + (printSeq.toString().padStart(3,"0")),
|
||||
filename = pre,// + (new Date().getTime().toString(36)),
|
||||
fileext = settings.device.gcodeFExt || "gcode",
|
||||
codeproc = settings.device.gcodeProc,
|
||||
octo_host,
|
||||
octo_apik,
|
||||
grid_host,
|
||||
grid_apik,
|
||||
grid_target,
|
||||
grid_targets = {},
|
||||
grid_local,
|
||||
grid_uuid;
|
||||
|
||||
// run gcode post processor function (when supplied and valid)
|
||||
if (codeproc && self[codeproc]) {
|
||||
gcode = self[codeproc](gcode);
|
||||
}
|
||||
|
||||
function getBlob() {
|
||||
return new Blob(
|
||||
[gcode],
|
||||
{type:"application/octet-stream"});
|
||||
}
|
||||
|
||||
function sendto_octoprint() {
|
||||
if (!(octo_host && octo_apik)) return;
|
||||
|
||||
let form = new FormData(),
|
||||
ajax = new XMLHttpRequest(),
|
||||
host = octo_host.value.toLowerCase(),
|
||||
apik = octo_apik.value;
|
||||
|
||||
if (host.indexOf("http") !== 0) {
|
||||
API.show.alert("host missing protocol (http:// or https://)");
|
||||
return;
|
||||
}
|
||||
if (API.const.SECURE && !API.util.isSecure(host)) {
|
||||
API.show.alert("host must begin with 'https' on a secure site");
|
||||
return;
|
||||
}
|
||||
|
||||
SDB['octo-host'] = host.trim();
|
||||
SDB['octo-apik'] = apik.trim();
|
||||
|
||||
filename = $('print-filename').value;
|
||||
form.append("file", getBlob(), filename+"."+fileext);
|
||||
ajax.onreadystatechange = function() {
|
||||
if (ajax.readyState === 4) {
|
||||
let status = ajax.status;
|
||||
STATS.add(`ua_${API.mode.get_lower()}_print_octo_${status}`);
|
||||
if (status >= 200 && status < 300) {
|
||||
API.modal.hide();
|
||||
} else {
|
||||
API.show.alert("octoprint error\nstatus: "+status+"\nmessage: "+ajax.responseText);
|
||||
}
|
||||
API.show.progress(0);
|
||||
}
|
||||
API.show.progress(0);
|
||||
};
|
||||
ajax.upload.addEventListener('progress', function(evt) {
|
||||
API.show.progress(evt.loaded/evt.total, "sending");
|
||||
});
|
||||
ajax.open("POST", host+"/api/files/local");
|
||||
ajax.setRequestHeader("X-Api-Key", apik);
|
||||
ajax.send(form);
|
||||
}
|
||||
|
||||
function gridhost_tracker(host,key) {
|
||||
ajax(host+"/api/check?key="+key, function(data) {
|
||||
data = js2o(data);
|
||||
DBUG.log(data);
|
||||
if (!(data.done || data.error)) {
|
||||
setTimeout(function() { gridhost_tracker(host,key) }, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function gridlocal_probe(ev, devs) {
|
||||
if (ev && ev.code !== 'Enter') return;
|
||||
|
||||
if (!devs && API.probe.local(gridlocal_probe)) return;
|
||||
|
||||
grid_local = devs;
|
||||
|
||||
let html = [];
|
||||
for (let uuid in devs) {
|
||||
let dev = devs[uuid];
|
||||
html.push(`<option id="gl-${uuid}" value="${uuid}">${dev.stat.device.name}</option>`);
|
||||
}
|
||||
|
||||
if (html.length) {
|
||||
$('grid-local').innerHTML = html.join('\n');
|
||||
$('send-to-gridhead').style.display = 'flex';
|
||||
$('send-to-gridspool').style.display = 'flex';
|
||||
} else {
|
||||
$('send-to-gridhead').style.display = '';
|
||||
$('send-to-gridspool').style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
function sendto_gridlocal() {
|
||||
let uuid = $('grid-local').value;
|
||||
let dev = grid_local[uuid];
|
||||
if (dev) {
|
||||
let file = $('print-filename').value;
|
||||
fetch(
|
||||
`/api/grid_send?uuid=${uuid}&file=${encodeURIComponent(file + "." + fileext)}`,
|
||||
{method: "POST", body: gcode}
|
||||
)
|
||||
.then(t => t.text())
|
||||
.then(t => {
|
||||
STATS.add(`ua_${API.mode.get_lower()}_print_local_ok`);
|
||||
console.log({grid_spool_said: t});
|
||||
})
|
||||
.catch(e => {
|
||||
STATS.add(`ua_${API.mode.get_lower()}_print_local_err`);
|
||||
console.log({grid_local_spool_error: e});
|
||||
})
|
||||
.finally(() => {
|
||||
API.modal.hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function admin_gridlocal() {
|
||||
let dev = grid_local[$('grid-local').value];
|
||||
if (dev && dev.stat && dev.stat.device) {
|
||||
let dsd = dev.stat.device;
|
||||
window.open(`http://${dsd.addr[0]}:${dsd.port || 4080}`, "_grid_admin");
|
||||
}
|
||||
}
|
||||
|
||||
function gridhost_probe(ev, set_host) {
|
||||
if (ev && ev.code !== 'Enter') return;
|
||||
if (!(grid_host && grid_apik)) return;
|
||||
|
||||
if (set_host) grid_host.value = set_host;
|
||||
|
||||
let xhtr = new XMLHttpRequest(),
|
||||
host = grid_host.value,
|
||||
apik = grid_apik.value,
|
||||
target = grid_target.value;
|
||||
|
||||
if (!apik) $('gpapik').style.display = 'none';
|
||||
|
||||
if (!host && API.probe.grid(gridhost_probe)) return;
|
||||
|
||||
if (!host) return;
|
||||
|
||||
xhtr.onreadystatechange = function() {
|
||||
if (xhtr.readyState === 4) {
|
||||
if (xhtr.status >= 200 && xhtr.status < 300) {
|
||||
SDB['grid-host'] = host;
|
||||
SDB['grid-apik'] = apik;
|
||||
let res = JSON.parse(xhtr.responseText);
|
||||
let sel = false;
|
||||
let match = false;
|
||||
let first = null;
|
||||
let html = [];
|
||||
grid_targets = {};
|
||||
for (let key in res) {
|
||||
first = first || key;
|
||||
if (!SDB['grid-target']) {
|
||||
SDB['grid-target'] = key;
|
||||
sel = true;
|
||||
} else {
|
||||
sel = SDB['grid-target'] === key;
|
||||
}
|
||||
match = match || sel;
|
||||
grid_targets[html.length] = key;
|
||||
html.push(
|
||||
"<option id='gpo-'" + key + " value='" +key + "'" +
|
||||
(sel ? " selected" : "") +
|
||||
">" +
|
||||
(res[key].comment || key) +
|
||||
"</option>"
|
||||
);
|
||||
}
|
||||
if (!match) {
|
||||
SDB['grid-target'] = first;
|
||||
}
|
||||
grid_target.innerHTML = html.join('\n');
|
||||
} else if (xhtr.status === 401) {
|
||||
$('gpapik').style.display = '';
|
||||
} else {
|
||||
SDB.removeItem('grid-host');
|
||||
SDB.removeItem('grid-apik');
|
||||
console.log("invalid grid:host url");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhtr.open("GET", host + "/api/active?key=" + apik);
|
||||
xhtr.send();
|
||||
}
|
||||
|
||||
function sendto_gridhost() {
|
||||
if (!(grid_host && grid_apik)) return;
|
||||
|
||||
let xhtr = new XMLHttpRequest(),
|
||||
host = grid_host.value,
|
||||
apik = grid_apik.value,
|
||||
target = SDB['grid-target'] || '';
|
||||
|
||||
if (target === '') {
|
||||
API.show.alert('invalid or missing target');
|
||||
return;
|
||||
}
|
||||
if (host.indexOf("http") !== 0) {
|
||||
API.show.alert("host missing protocol (http:// or https://)");
|
||||
return;
|
||||
}
|
||||
if (host.indexOf("://") < 0) {
|
||||
API.show.alert("host:port malformed");
|
||||
return;
|
||||
}
|
||||
if (API.const.SECURE && !API.util.isSecure(host)) {
|
||||
API.show.alert("host must begin with 'https' on a secure site");
|
||||
return;
|
||||
}
|
||||
|
||||
SDB['grid-host'] = host.trim();
|
||||
SDB['grid-apik'] = apik.trim();
|
||||
|
||||
xhtr.onreadystatechange = function() {
|
||||
if (xhtr.readyState === 4) {
|
||||
let status = xhtr.status;
|
||||
STATS.add(`ua_${API.mode.get_lower()}_print_grid_${status}`);
|
||||
if (status >= 200 && status < 300) {
|
||||
let json = js2o(xhtr.responseText);
|
||||
gridhost_tracker(host,json.key);
|
||||
API.ajax(host+"/api/wait?key="+json.key, function(data) {
|
||||
data = js2o(data);
|
||||
DBUG.log(data);
|
||||
API.show.alert("print to "+target+": "+data.status, 600);
|
||||
});
|
||||
} else {
|
||||
API.show.alert("grid:host error\nstatus: "+status+"\nmessage: "+xhtr.responseText, 10000);
|
||||
}
|
||||
API.show.progress(0);
|
||||
}
|
||||
};
|
||||
xhtr.upload.addEventListener('progress', function(evt) {
|
||||
API.show.progress(evt.loaded/evt.total, "sending");
|
||||
});
|
||||
filename = $('print-filename').value;
|
||||
xhtr.open("POST",
|
||||
host + "/api/print?" +
|
||||
"filename=" + filename +
|
||||
"&target=" + target +
|
||||
"&key=" + apik +
|
||||
"&time=" + Math.round(currentPrint.time) +
|
||||
"&length=" + Math.round(currentPrint.distance) +
|
||||
"&image=" + filename
|
||||
);
|
||||
xhtr.setRequestHeader("Content-Type", "text/plain");
|
||||
let snapshot = API.view.snapshot;
|
||||
xhtr.send(snapshot ? [gcode,snapshot].join("\0") : gcode);
|
||||
API.modal.hide();
|
||||
}
|
||||
|
||||
function download() {
|
||||
filename = $('print-filename').value;
|
||||
saveAs(getBlob(), filename + "." + fileext);
|
||||
}
|
||||
|
||||
function pad(v) {
|
||||
v = v.toString();
|
||||
return v.length < 2 ? '0' + v : v;
|
||||
}
|
||||
|
||||
function calcWeight() {
|
||||
try {
|
||||
let density = $('print-density');
|
||||
$('print-weight').value = (
|
||||
(Math.PI * UTIL.sqr(
|
||||
currentPrint.settings.device.extruders[0].extFilament / 2
|
||||
)) *
|
||||
currentPrint.distance *
|
||||
(parseFloat(density.value) || 1.25) /
|
||||
1000
|
||||
).toFixed(2);
|
||||
density.onkeyup = (ev) => {
|
||||
if (ev.key === 'Enter') calcWeight();
|
||||
};
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
function calcTime() {
|
||||
let floor = Math.floor,
|
||||
time = floor(currentPrint.time),
|
||||
hours = floor(time / 3600),
|
||||
newtime = time - hours * 3600,
|
||||
mins = floor(newtime / 60),
|
||||
secs = newtime - mins * 60;
|
||||
|
||||
$('output-time').value = [pad(hours),pad(mins),pad(secs)].join(':');
|
||||
}
|
||||
|
||||
fetch("/kiri/output-gcode.html").then(r => r.text()).then(html => {
|
||||
UI.print.innerHTML = html;
|
||||
let set = API.conf.get();
|
||||
let fdm = MODE === MODES.FDM;
|
||||
let octo = set.controller.exportOcto && MODE !== MODES.CAM;
|
||||
let ghost = set.controller.exportGhost;
|
||||
let local = set.controller.exportLocal;
|
||||
$('print-download').onclick = download;
|
||||
$('print-filament-head').style.display = fdm ? '' : 'none';
|
||||
$('print-filament-info').style.display = fdm ? '' : 'none';
|
||||
$('print-filename').value = filename;
|
||||
$('print-filesize').value = UTIL.comma(currentPrint.bytes);
|
||||
$('print-filament').value = Math.round(currentPrint.distance);
|
||||
calcTime();
|
||||
if (fdm) {
|
||||
calcWeight();
|
||||
}
|
||||
|
||||
// octoprint setup
|
||||
$('send-to-octohead').style.display = octo ? '' : 'none';
|
||||
$('send-to-octoprint').style.display = octo ? '' : 'none';
|
||||
if (octo) try {
|
||||
$('print-octoprint').onclick = sendto_octoprint;
|
||||
octo_host = $('octo-host');
|
||||
octo_apik = $('octo-apik');
|
||||
if (MODE === MODES.CAM) {
|
||||
$('send-to-octohead').style.display = 'none';
|
||||
$('send-to-octoprint').style.display = 'none';
|
||||
} else {
|
||||
$('send-to-octohead').style.display = '';
|
||||
$('send-to-octoprint').style.display = '';
|
||||
}
|
||||
// hide octoprint when hard-coded in the url
|
||||
if (API.const.OCTO) {
|
||||
$('ophost').style.display = 'none';
|
||||
$('opapik').style.display = 'none';
|
||||
$('ophint').style.display = 'none';
|
||||
$('send-to-gridhost').style.display = 'none';
|
||||
}
|
||||
octo_host.value = SDB['octo-host'] || '';
|
||||
octo_apik.value = SDB['octo-apik'] || '';
|
||||
} catch (e) { console.log(e) }
|
||||
|
||||
// grid:host setup
|
||||
$('send-to-hosthead').style.display = ghost ? '' : 'none';
|
||||
$('send-to-gridhost').style.display = ghost ? '' : 'none';
|
||||
if (ghost) try {
|
||||
$('print-gridhost').onclick = sendto_gridhost;
|
||||
$('grid-host').onkeyup = gridhost_probe;
|
||||
$('grid-apik').onkeyup = gridhost_probe;
|
||||
grid_host = $('grid-host');
|
||||
grid_apik = $('grid-apik');
|
||||
grid_target = $('grid-target');
|
||||
grid_target.onchange = function(ev) {
|
||||
SDB['grid-target'] = grid_targets[grid_target.selectedIndex];
|
||||
};
|
||||
grid_host.value = SDB['grid-host'] || '';
|
||||
grid_apik.value = SDB['grid-apik'] || '';
|
||||
gridhost_probe();
|
||||
} catch (e) { console.log(e) }
|
||||
|
||||
// grid:local setup
|
||||
if (local) try {
|
||||
gridlocal_probe();
|
||||
$('print-gridlocal').onclick = sendto_gridlocal;
|
||||
$('admin-gridlocal').onclick = admin_gridlocal;
|
||||
} catch (e) { console.log(e) }
|
||||
|
||||
// show dialog
|
||||
API.modal.show('print');
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
256
src/kiri/fill.js
Normal file
256
src/kiri/fill.js
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.kiri.fill) return;
|
||||
|
||||
const KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
ROUND = UTIL.round,
|
||||
DEG2RAD = Math.PI / 180,
|
||||
FILL = self.kiri.fill = {
|
||||
hex: fillHexFull,
|
||||
grid: fillGrid,
|
||||
gyroid: fillGyroid,
|
||||
triangle: fillTriangle,
|
||||
linear: fillLinear,
|
||||
bubbles: fillBubbles
|
||||
},
|
||||
CACHE = self.kiri.fill_fixed = {
|
||||
hex: fillHexFull,
|
||||
grid: fillGrid,
|
||||
triangle: fillTriangle
|
||||
};
|
||||
|
||||
function fillHexFull(target) {
|
||||
fillHex(target, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* emitter creates a hex infill pattern and sends to target
|
||||
*
|
||||
* @param {Object} target
|
||||
* @param {boolean} full continuous walls
|
||||
*/
|
||||
function fillHex(target, full) {
|
||||
// compute segment lengths (vert/horiz and 45)
|
||||
let spacing = target.offset();
|
||||
let vhlen = (1 - target.density()) * 4 + spacing;
|
||||
let anxlen = ROUND(Math.cos(30 * DEG2RAD) * vhlen, 7);
|
||||
let anylen = ROUND(Math.sin(30 * DEG2RAD) * vhlen, 7);
|
||||
let bounds = target.bounds();
|
||||
let even = true;
|
||||
let evenZ = target.zIndex() % 2 === 0;
|
||||
let maxy = bounds.max.y + (vhlen + anylen * 2);
|
||||
let x, y;
|
||||
|
||||
if (full || evenZ) {
|
||||
x = bounds.min.x;
|
||||
for (;;) {
|
||||
if (even && x > bounds.max.x) break;
|
||||
if (!even && x > bounds.max.x + anxlen + spacing) break;
|
||||
y = bounds.min.y;
|
||||
target.newline();
|
||||
while (y <= maxy) {
|
||||
target.emit(x,y);
|
||||
y += vhlen;
|
||||
target.emit(x,y);
|
||||
if (even) x += anxlen; else x -= anxlen;
|
||||
y += anylen;
|
||||
target.emit(x,y);
|
||||
y += vhlen;
|
||||
target.emit(x,y);
|
||||
if (even) x -= anxlen; else x += anxlen;
|
||||
y += anylen;
|
||||
}
|
||||
x += spacing;
|
||||
if (even) x += (anxlen * 2);
|
||||
even = !even;
|
||||
target.newline();
|
||||
}
|
||||
} else {
|
||||
y = bounds.min.y + vhlen;
|
||||
for (;;) {
|
||||
if (even && y > bounds.max.y) break;
|
||||
if (!even && y > bounds.max.y + anylen) break;
|
||||
x = bounds.min.x;
|
||||
target.newline();
|
||||
while (x < bounds.max.x) {
|
||||
target.emit(x,y);
|
||||
if (even) y += anylen; else y -= anylen;
|
||||
x += anxlen;
|
||||
target.emit(x,y);
|
||||
x += spacing;
|
||||
target.emit(x,y);
|
||||
if (even) y -= anylen; else y += anylen;
|
||||
x += anxlen;
|
||||
target.emit(x,y);
|
||||
x += spacing;
|
||||
}
|
||||
y += vhlen;
|
||||
if (even) y += (anylen * 2);
|
||||
even = !even;
|
||||
target.newline();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fillGyroid(target) {
|
||||
let bounds = target.bounds();
|
||||
let height = target.zHeight();
|
||||
let span_x = bounds.max.x - bounds.min.x;
|
||||
let span_y = bounds.max.y - bounds.min.y;
|
||||
let density = target.density();
|
||||
let tile = 1 + (1 - density) * 15;
|
||||
let tile_x = span_x / tile;
|
||||
let tile_y = span_y / tile;
|
||||
let tile_z = 1 / tile;
|
||||
let gyroid = BASE.gyroid.slice(target.zValue() * tile_z, (1 - density) * 500);
|
||||
|
||||
gyroid.polys.forEach(poly => {
|
||||
for (let tx=0; tx<=tile_x; tx++) {
|
||||
for (let ty=0; ty<=tile_y; ty++) {
|
||||
target.newline();
|
||||
let bx = tx * tile + bounds.min.x;
|
||||
let by = ty * tile + bounds.min.y;
|
||||
poly.forEach(point => {
|
||||
target.emit(bx + point.x * tile, by + point.y * tile);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fillGrid(target) {
|
||||
let bounds = target.bounds();
|
||||
let height = target.zHeight();
|
||||
let span_x = bounds.max.x - bounds.min.x;
|
||||
let span_y = bounds.max.y - bounds.min.y;
|
||||
let density = target.density();
|
||||
let offset = target.offset() / 2;
|
||||
let tile = 1 + (1 - density) * 3;
|
||||
let tile_x = tile + offset;
|
||||
let tile_xc = span_x / tile_x;
|
||||
let tile_yc = span_y / tile;
|
||||
|
||||
for (let tx=0; tx<=tile_xc; tx++) {
|
||||
target.newline();
|
||||
for (let ty=0; ty<=tile_yc; ty++) {
|
||||
let bx = tx * tile_x + bounds.min.x;
|
||||
let by = ty * tile + bounds.min.y;
|
||||
if ((tx + ty) % 2) {
|
||||
target.emit(bx, by);
|
||||
target.emit(bx + tile_x - offset, by + tile);
|
||||
} else {
|
||||
target.emit(bx + tile_x - offset, by);
|
||||
target.emit(bx, by + tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fillLinear(target) {
|
||||
let bounds = target.bounds();
|
||||
let height = target.zHeight();
|
||||
let span_x = bounds.max.x - bounds.min.x;
|
||||
let span_y = bounds.max.y - bounds.min.y;
|
||||
let density = target.density();
|
||||
let offset = target.offset() / 2;
|
||||
let tile = 1 + (1 - density);
|
||||
let tile_xc = Math.ceil(span_x / tile);
|
||||
let tile_yc = Math.ceil(span_y / tile);
|
||||
|
||||
if (target.zIndex() % 2 === 1) {
|
||||
for (let tx=0; tx<=tile_xc; tx++) {
|
||||
target.newline();
|
||||
target.emit(tx * tile + bounds.min.x, bounds.min.y);
|
||||
target.emit(tx * tile + bounds.min.x, bounds.max.y);
|
||||
}
|
||||
} else {
|
||||
for (let ty=0; ty<=tile_yc; ty++) {
|
||||
target.newline();
|
||||
target.emit(bounds.min.x, ty * tile + bounds.min.y);
|
||||
target.emit(bounds.max.x, ty * tile + bounds.min.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fillTriangle(target) {
|
||||
let bounds = target.bounds();
|
||||
let height = target.zHeight();
|
||||
let span_x = bounds.max.x - bounds.min.x;
|
||||
let span_y = bounds.max.y - bounds.min.y;
|
||||
let density = target.density();
|
||||
let offset = target.offset();
|
||||
let line_w = target.lineWidth() / 2;
|
||||
let tile = 1 + (1 - density) * 5;
|
||||
let tile_x = tile + offset*2 + line_w;
|
||||
let tile_xc = span_x / tile_x;
|
||||
let tile_yc = span_y / tile;
|
||||
|
||||
for (let tx=0; tx<=tile_xc; tx++) {
|
||||
target.newline();
|
||||
for (let ty=0; ty<=tile_yc; ty++) {
|
||||
let bx = tx * tile_x + bounds.min.x;
|
||||
let by = ty * tile + bounds.min.y;
|
||||
if ((tx + ty) % 2) {
|
||||
target.emit(bx, by);
|
||||
target.emit(bx + tile_x - offset - line_w, by + tile);
|
||||
} else {
|
||||
target.emit(bx + tile_x - offset - line_w, by);
|
||||
target.emit(bx, by + tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let tx=0; tx<=tile_xc; tx++) {
|
||||
let bx = tx * tile_x + bounds.min.x;
|
||||
let xp = bx + tile_x - line_w/2 - offset/2;
|
||||
target.newline();
|
||||
target.emit(xp, bounds.min.y);
|
||||
target.emit(xp, bounds.max.y);
|
||||
}
|
||||
}
|
||||
|
||||
function fillBubbles(api) {
|
||||
let {min, max} = api.bounds();
|
||||
let slice = api.slice(); // slice object (for adding solids)
|
||||
let height = api.zHeight(); // layer height
|
||||
let offset = api.lineWidth() / 2; // offset size by nozzle width
|
||||
let size = 3 / api.density(); // circle diameter from density
|
||||
let rad = size / 2 - offset; // max circle radius
|
||||
let minr = offset + (2 - api.density() * 2) * offset;
|
||||
let zrep = size / height; // # layers to repeat pattern
|
||||
let zpad = zrep * 0.3; // # pad layers between pattern
|
||||
let bind = api.zIndex() % (zrep + zpad); // index into pattern
|
||||
let brad = bind < zrep ? Math.max(minr,(rad * Math.sin(((bind + 1) / zrep) * Math.PI))) : minr;
|
||||
let sind = (api.zIndex() + (zrep + zpad)/2) % (zrep + zpad); // index into pattern
|
||||
let srad = sind < zrep ? Math.max(minr,(rad * Math.sin(((sind + 1) / (zrep + 0)) * Math.PI))) : minr;
|
||||
for (let x=min.x-size; x<max.x+size; x += size) {
|
||||
let eoy = 0;
|
||||
for (let y=min.y-size; y<max.y+size; y += size) {
|
||||
let xo = (eoy++ % 2 === 0) ? (size / 2) : 0;
|
||||
// primary pattern
|
||||
self.base
|
||||
.newPolygon()
|
||||
.centerCircle({x:x+xo, y:y*0.85}, brad, 20, true)
|
||||
.forEachPoint(p => {
|
||||
api.emit(p.x, p.y);
|
||||
}, true);
|
||||
api.newline();
|
||||
// alternate pattern
|
||||
self.base
|
||||
.newPolygon()
|
||||
.centerCircle({x:x+xo, y:y*0.85+(size/1.75)}, srad, 20, true)
|
||||
.forEachPoint(p => {
|
||||
api.emit(p.x, p.y);
|
||||
}, true);
|
||||
api.newline();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
1932
src/kiri/init.js
Normal file
1932
src/kiri/init.js
Normal file
File diff suppressed because it is too large
Load diff
1
src/kiri/lang-en.js
Symbolic link
1
src/kiri/lang-en.js
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../web/kiri/lang/en.js
|
||||
58
src/kiri/lang.js
Normal file
58
src/kiri/lang.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.kiri.lang) return;
|
||||
|
||||
const KIRI = self.kiri, LANG = self.kiri.lang = { current: {} };
|
||||
|
||||
let lset = navigator.language.toLocaleLowerCase();
|
||||
|
||||
LANG.get = function() {
|
||||
return lset;
|
||||
};
|
||||
|
||||
LANG.set = function() {
|
||||
let map, key, keys = [...arguments];
|
||||
// provide default if none given
|
||||
if (keys.length === 0) {
|
||||
keys = [lset, lset.split('-')[0], 'en'];
|
||||
}
|
||||
for (let i=0; i<keys.length; i++)
|
||||
{
|
||||
key = keys[i]
|
||||
if (!(map = LANG[key])) {
|
||||
continue;
|
||||
}
|
||||
lset = key;
|
||||
let missing = [];
|
||||
let invalid = [];
|
||||
// default to EN values when missing
|
||||
Object.keys(LANG.en).forEach(key => {
|
||||
if (!map[key]) {
|
||||
map[key] = LANG.en[key];
|
||||
missing.push(key);
|
||||
}
|
||||
});
|
||||
// update current map from chosen map
|
||||
Object.keys(map).forEach(key => {
|
||||
if (LANG.en[key]) {
|
||||
LANG.current[key] = map[key];
|
||||
} else {
|
||||
invalid.push(key);
|
||||
}
|
||||
});
|
||||
if (missing.length) {
|
||||
console.log(`language map "${key}" missing keys [${missing.length}]: ${missing.join(', ')}`);
|
||||
}
|
||||
if (invalid.length) {
|
||||
console.log(`language map "${key}" invalid keys [${invalid.length}]: ${invalid.join(', ')}`);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
})();
|
||||
305
src/kiri/layer.js
Normal file
305
src/kiri/layer.js
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.kiri.Layer) return;
|
||||
|
||||
const KIRI = self.kiri,
|
||||
POLY = self.base.polygons,
|
||||
LP = Layer.prototype,
|
||||
mcache = {};
|
||||
|
||||
KIRI.Layer = Layer;
|
||||
KIRI.newLayer = function(view) { return new Layer(view) };
|
||||
|
||||
function materialFor(layer, color, mesh, linewidth) {
|
||||
let m = mcache[color];
|
||||
if (!m) {
|
||||
m = mcache[color] = mesh ? new THREE.MeshPhongMaterial({
|
||||
transparent: layer.transparent,
|
||||
shininess: layer.shininess,
|
||||
specular: layer.specular,
|
||||
opacity: layer.opacity,
|
||||
color: color,
|
||||
side: THREE.DoubleSide
|
||||
}) : 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) {
|
||||
let points = poly.points,
|
||||
len = points.length;
|
||||
|
||||
if (len < 2) return;
|
||||
|
||||
let 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);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function addSolid(arr, poly) {
|
||||
let faces = poly.earcut();
|
||||
faces.forEach(p => {
|
||||
p.forEachPoint(p => {
|
||||
arr.push(p.x, p.y, p.z);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {THREE.Group} view
|
||||
*/
|
||||
function Layer(view) {
|
||||
this.changed = false;
|
||||
this.group = null;
|
||||
this.solids = null;
|
||||
this.view = view;
|
||||
this.bycolor = {};
|
||||
this.opacity = 0.15,
|
||||
this.specular = 0x181818;
|
||||
this.shininess = 100;
|
||||
this.transparent = true;
|
||||
};
|
||||
|
||||
LP.setOpacity = function(v) {
|
||||
this.opacity = v;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.setSpecular = function(v) {
|
||||
this.specular = v;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.setShininess = function(v) {
|
||||
this.shininess = v;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.setTransparent = function(b) {
|
||||
this.transparent = b;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.setVisible = function(vis) {
|
||||
if (this.group) this.group.visible = vis;
|
||||
if (this.solids) this.solids.visible = vis;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.clear = function() {
|
||||
this.changed = true;
|
||||
this.bycolor = {};
|
||||
};
|
||||
|
||||
LP.add = function(color, obj) {
|
||||
let ca = this.bycolor[color];
|
||||
if (!ca) ca = this.bycolor[color] = [];
|
||||
ca.push(obj);
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.poly = function(poly, color, deep, open) {
|
||||
let 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;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.solid = function(poly, color) {
|
||||
this.add(color, {solid: poly});
|
||||
this.changed = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.noodle = function(poly, offset, color, traceColor, open) {
|
||||
if (!poly) return;
|
||||
if (Array.isArray(poly)) {
|
||||
poly = POLY.flatten(poly, [], true);
|
||||
} else {
|
||||
poly = POLY.flatten([poly], [], true);
|
||||
}
|
||||
if (!poly.length) return;
|
||||
let z = poly[0].getZ();
|
||||
poly.forEach(p => {
|
||||
let exp = [];
|
||||
POLY.expand([p], offset, z-0.01, exp, 1);
|
||||
POLY.expand([p], -offset, z-0.01, exp, 1);
|
||||
POLY.nest(exp).forEach((p,i) => {
|
||||
this.add(color, {solid: p});
|
||||
this.add(traceColor, {poly: p.clone(true).setZ(z + 0.01), deep: true, open});
|
||||
});
|
||||
})
|
||||
this.changed = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.noodle_open = function(poly, offset, color, traceColor, z) {
|
||||
poly = POLY.expand_lines(poly, offset, z);
|
||||
poly.forEach(p => {
|
||||
this.add(color, {solid: p});
|
||||
this.add(traceColor, {poly: p.clone().setZ(z + 0.01), deep: false, open: false});
|
||||
})
|
||||
this.changed = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.noodle_lines = function(points, offset, color, traceColor, z) {
|
||||
if (!(points && points.length)) return;
|
||||
for (let i=0; i<points.length; i += 2) {
|
||||
let p1 = points[i];
|
||||
let p2 = points[i+1];
|
||||
let l1 = p1.offsetLineTo(p2, offset);
|
||||
let l2 = p2.offsetLineTo(p1, offset);
|
||||
let poly = base.newPolygon().addPoints([
|
||||
l1.p1, l1.p2, l2.p1, l2.p2
|
||||
]);
|
||||
this.add(color, {solid: poly});
|
||||
this.add(traceColor, {poly: poly.clone().setZ(z + 0.01), deep: false, open: false});
|
||||
}
|
||||
this.changed = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.points = function(points, color, size, opacity) {
|
||||
let 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);
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.lines = function(points, color) {
|
||||
this.add(color, {lines:points});
|
||||
this.changed = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.renderAll = function() {
|
||||
this.renderSolid();
|
||||
this.render();
|
||||
};
|
||||
|
||||
LP.renderSolid = function() {
|
||||
if (!(this.view)) return;
|
||||
if (this.solids) this.view.remove(this.solids);
|
||||
this.solids = this.view.newGroup();
|
||||
|
||||
let bycolor = this.bycolor;
|
||||
|
||||
for (let key in bycolor) {
|
||||
if (!bycolor.hasOwnProperty(key)) continue;
|
||||
|
||||
let arr = [], mat = materialFor(this, parseInt(key), true);
|
||||
|
||||
bycolor[key].forEach(function(obj) {
|
||||
if (obj.solid) {
|
||||
addSolid(arr, obj.solid);
|
||||
}
|
||||
});
|
||||
|
||||
if (arr.length > 0) {
|
||||
let geo = new THREE.BufferGeometry();
|
||||
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(arr.toFloat32(), 3));
|
||||
geo.computeFaceNormals();
|
||||
geo.computeVertexNormals();
|
||||
|
||||
let mesh = new THREE.Mesh(geo, mat);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
|
||||
this.solids.add(mesh);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
LP.render = function() {
|
||||
if (!(this.view && this.changed)) return;
|
||||
if (this.group) this.view.remove(this.group);
|
||||
this.group = this.view.newGroup();
|
||||
|
||||
let bycolor = this.bycolor,
|
||||
key, added;
|
||||
|
||||
for (key in bycolor) {
|
||||
if (!bycolor.hasOwnProperty(key)) continue;
|
||||
|
||||
let geo = new THREE.Geometry(),
|
||||
arr = geo.vertices,
|
||||
mat = materialFor(this, 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;
|
||||
}
|
||||
|
||||
})();
|
||||
1989
src/kiri/main.js
Normal file
1989
src/kiri/main.js
Normal file
File diff suppressed because it is too large
Load diff
1157
src/kiri/print.js
Normal file
1157
src/kiri/print.js
Normal file
File diff suppressed because it is too large
Load diff
1402
src/kiri/slice.js
Normal file
1402
src/kiri/slice.js
Normal file
File diff suppressed because it is too large
Load diff
899
src/kiri/slicer.js
Normal file
899
src/kiri/slicer.js
Normal file
|
|
@ -0,0 +1,899 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.kiri.slicer) return;
|
||||
|
||||
self.kiri.slicer = {
|
||||
slice: slice,
|
||||
sliceWidget: sliceWidget,
|
||||
connectLines: connectLines
|
||||
};
|
||||
|
||||
let 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) {
|
||||
let topoMode = options.topo,
|
||||
simpleMode = options.simple,
|
||||
swap = options.swapX || options.swapY,
|
||||
isCam = options.cam,
|
||||
ox = 0,
|
||||
oy = 0;
|
||||
|
||||
// handle rotating meshes for CAM finishing.
|
||||
// slicer expects things just so, so we alter
|
||||
// geometry to satisfy
|
||||
if (swap) {
|
||||
points = points.slice();
|
||||
|
||||
let 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 (let 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 (let 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;
|
||||
}
|
||||
|
||||
let zMin = options.zmin || options.firstHeight || Math.floor(bounds.min.z),
|
||||
zMax = options.zmax || Math.ceil(bounds.max.z),
|
||||
zInc = options.height,
|
||||
zIncMin = options.minHeight,
|
||||
zIncFirst = options.firstHeight || zInc,
|
||||
zOff = true ? zInc / 2 : 0,
|
||||
zHeights = [], // heights for zIndexes in adaptive mode
|
||||
zIndexes = [], // auto-detected z slicing offsets (laser/cam)
|
||||
zOrdered = [], // ordered list of Z indexes
|
||||
zList = {}, // map count of z index points for adaptive slicing
|
||||
zFlat = {}, // map area of z index flat areas (cam)
|
||||
zLines = {}, // map count of z index lines
|
||||
zScale, // bucket span in z units
|
||||
timeStart = time(),
|
||||
slices = [],
|
||||
zSum = 0.0,
|
||||
buckets = [],
|
||||
i, j = 0, k, p1, p2, p3, px,
|
||||
CPRO = KIRI.driver.CAM.process;
|
||||
|
||||
if (options.add) zMax += zInc;
|
||||
|
||||
function countZ(z) {
|
||||
z = UTIL.round(z,5);
|
||||
zList[z] = (zList[z] || 0) + 1;
|
||||
}
|
||||
|
||||
// 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 || zIncMin) {
|
||||
countZ(p1.z);
|
||||
countZ(p2.z);
|
||||
countZ(p3.z);
|
||||
}
|
||||
// use co-flat and co-line detection to adjust slice Z
|
||||
if (p1.z === p2.z && p2.z === p3.z && p1.z > bounds.min.z) {
|
||||
// detect zFlat faces to avoid slicing directly on them
|
||||
let zkey = p1.z.toFixed(5),
|
||||
area = Math.abs(UTIL.area2(p1,p2,p3)) / 2;
|
||||
if (!zFlat[zkey]) {
|
||||
zFlat[zkey] = area;
|
||||
} else {
|
||||
zFlat[zkey] += area;
|
||||
}
|
||||
} else if (true || options.trace) {
|
||||
// detect zLines (curved region tops/bottoms)
|
||||
// mark these layers for ball and v mill tracing
|
||||
if (p1.z === p2.z && p1.z > bounds.min.z) {
|
||||
let zkey = p1.z.toFixed(5);
|
||||
let zval = zLines[zkey];
|
||||
zLines[zkey] = (zval || 0) + 1;
|
||||
}
|
||||
if (p2.z === p3.z && p2.z > bounds.min.z) {
|
||||
let zkey = p2.z.toFixed(5);
|
||||
let zval = zLines[zkey];
|
||||
zLines[zkey] = (zval || 0) + 1;
|
||||
}
|
||||
if (p3.z === p1.z && p3.z > bounds.min.z) {
|
||||
let zkey = p3.z.toFixed(5);
|
||||
let zval = zLines[zkey];
|
||||
zLines[zkey] = (zval || 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CAM: find slice candidates to trace for ballmills and tapermills
|
||||
if (options.trace) {
|
||||
let zl = {};
|
||||
let le;
|
||||
let zs = Object.entries(zLines).map(oe => {
|
||||
return oe.map(v => parseFloat(v));
|
||||
}).sort((a,b) => {
|
||||
return b[0] - a[0];
|
||||
}).forEach((e,i) => {
|
||||
if (i > 0) {
|
||||
let zd = le[0]-e[0];
|
||||
if (zd > 0.1 && e[1] > 100) {
|
||||
// zl.push(e)
|
||||
zl[e[0].toFixed(5)] = e[1];
|
||||
zFlat[e[0].toFixed(5)] = e[1];
|
||||
}
|
||||
}
|
||||
if (e[1] > 10) {
|
||||
le = e;
|
||||
}
|
||||
});
|
||||
zLines = zl;
|
||||
}
|
||||
|
||||
/**
|
||||
* bucket polygons into z-bounded groups (inside or crossing)
|
||||
* to reduce the search space in complex models
|
||||
*/
|
||||
let 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++];
|
||||
let 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we need Z ordered list for laser auto or adaptive fdm slicing
|
||||
if (zInc === 0 || zIncMin) {
|
||||
for (let key in zList) {
|
||||
if (!zList.hasOwnProperty(key)) continue;
|
||||
zOrdered.push(parseFloat(key));
|
||||
}
|
||||
zOrdered.sort(function(a,b) { return a - b});
|
||||
}
|
||||
|
||||
if (options.single) {
|
||||
// usually for laser single slice
|
||||
zIndexes.push(zMin + zInc);
|
||||
} else if (zInc === 0) {
|
||||
// use Z indices in auto slice mode for laser
|
||||
// find unique z-index offsets for slicing
|
||||
let zl = zOrdered
|
||||
for (i = 0; i < zl.length - 1; i++) {
|
||||
zIndexes.push((zl[i] + zl[i+1]) / 2);
|
||||
}
|
||||
} else if (isCam) {
|
||||
// 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 (let key in zFlat) {
|
||||
// todo make threshold for flat detection configurable
|
||||
if (!zFlat.hasOwnProperty(key) || zFlat[key] < 10){
|
||||
continue;
|
||||
}
|
||||
key = parseFloat(key);
|
||||
if (!zIndexes.contains(key) && key >= zMin) {
|
||||
zIndexes.push(key);
|
||||
}
|
||||
}
|
||||
// sort top down
|
||||
zIndexes.sort(function(a,b) {
|
||||
return b-a;
|
||||
});
|
||||
// copy to store original z
|
||||
zHeights = zIndexes.slice();
|
||||
} else if (zIncMin) {
|
||||
// console.log('adaptive slicing', zIncMin, ':', zInc, 'from', zMin, 'to', zMax);
|
||||
// FDM adaptive slicing
|
||||
let zPos = zMin + zIncFirst,
|
||||
zOI = 0,
|
||||
zDelta,
|
||||
zDivMin,
|
||||
zDivMax,
|
||||
zStep,
|
||||
nextZ,
|
||||
lzp = zPos;
|
||||
|
||||
// first slice is fixed
|
||||
zHeights.push(zIncFirst);
|
||||
zIndexes.push(zIncFirst / 2);
|
||||
|
||||
while (zPos < zMax && zOI < zOrdered.length) {
|
||||
nextZ = zOrdered[zOI++];
|
||||
if (zPos >= nextZ) continue;
|
||||
zDelta = nextZ - zPos;
|
||||
if (zDelta < zIncMin) continue;
|
||||
|
||||
zDivMin = Math.floor(zDelta / zIncMin);
|
||||
zDivMax = Math.floor(zDelta / zInc);
|
||||
|
||||
if (zDivMax && zDivMax <= zDivMin) {
|
||||
if (zDelta % zInc > 0.01) zDivMax++;
|
||||
zStep = zDelta / zDivMax;
|
||||
// console.log(`--- zDivMax <= zDivMin ---`, zStep, zDelta % zInc)
|
||||
} else {
|
||||
zStep = zDelta;
|
||||
}
|
||||
// console.log({nextZ, zPos, zDelta, zStep, zDivMin, zDivMax})
|
||||
while (zPos < nextZ) {
|
||||
zHeights.push(zStep);
|
||||
zIndexes.push(zPos + zStep / 2);
|
||||
zPos += zStep;
|
||||
// console.log({D: zPos - lzp, zPos})
|
||||
// lzp = zPos;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// console.log('fixed slicing', zInc, 'from', zMin, 'to', zMax);
|
||||
// FDM fixed slicing
|
||||
if (options.firstHeight) {
|
||||
zIndexes.push(options.firstHeight / 2);
|
||||
zHeights.push(options.firstHeight);
|
||||
zMin = options.firstHeight;
|
||||
}
|
||||
for (i = zMin + zOff; i < zMax; i += zInc) {
|
||||
zIndexes.push(i);
|
||||
zHeights.push(zInc);
|
||||
}
|
||||
}
|
||||
|
||||
// create a Slice for each z offset in the zIndexes array
|
||||
for (let i = 0; i < zIndexes.length; i++) {
|
||||
let ik = zIndexes[i].toFixed(5),
|
||||
onFlat = false,
|
||||
onLine = false;
|
||||
// ensure no slice through horizontal lines or planes
|
||||
if (zFlat[ik]) onFlat = true;
|
||||
if (zLines[ik]) onLine = true;
|
||||
if (onFlat || onLine) {
|
||||
zIndexes[i] += isCam ? 0.001 : -0.001;
|
||||
}
|
||||
// slice next layer and add to slices[] array
|
||||
let slice = sliceZ(zIndexes[i], zHeights[i], onFlat, onLine);
|
||||
// override z in cam in case it was moved for a flat or line
|
||||
if (isCam && slice) {
|
||||
slice.z = zHeights[i];
|
||||
}
|
||||
onupdate(i / zIndexes.length);
|
||||
}
|
||||
|
||||
// for cam, bottom as mandatory (hasFlats)
|
||||
if (isCam && 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) {
|
||||
let 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) {
|
||||
let ip = [];
|
||||
for (let i = 0; i < over.length; i++) {
|
||||
for (let 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) {
|
||||
let 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);
|
||||
let 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
|
||||
* @param {number} [height] optional real height (fdm)
|
||||
*/
|
||||
function sliceZ(z, height, onflat, online) {
|
||||
let phash = {},
|
||||
lines = [],
|
||||
slice = newSlice(z, options.view ? options.view.newGroup() : null),
|
||||
bucket = bucketCount == 1 ? points : buckets[Math.floor(z * zScale)];
|
||||
|
||||
if (!bucket) {
|
||||
return;
|
||||
}
|
||||
|
||||
// annotate slices with cam flats for finishing waterlines
|
||||
if (onflat && isCam) {
|
||||
slice.hasFlats = true;
|
||||
}
|
||||
|
||||
// iterate over matching buckets for this z offset
|
||||
for (let i = 0; i < bucket.length;) {
|
||||
p1 = bucket[i++];
|
||||
p2 = bucket[i++];
|
||||
p3 = bucket[i++];
|
||||
let 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 (all 3 above or below)
|
||||
} 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
|
||||
// we drop these because this face is attached to 3 others
|
||||
// that will satisfy the if above (line) with 2 points
|
||||
} else if (where.under.length === 0 || where.over.length === 0) {
|
||||
// does not intersect but one point is on the slice Z plane
|
||||
} else {
|
||||
// compute two point intersections and construct line
|
||||
let 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;
|
||||
if (lines.length == 0 && options.noEmpty) return;
|
||||
|
||||
slice.height = height;
|
||||
slice.index = slices.length;
|
||||
slice.lines = removeDuplicateLines(lines);
|
||||
|
||||
// for topo slices, we just need the raw lines
|
||||
if (!topoMode && !simpleMode) {
|
||||
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) {
|
||||
let move = {x:ox, y:oy, z:0};
|
||||
slice.camMode = options.swapX ? CPRO.FINISH_X : CPRO.FINISH_Y;
|
||||
if (topoMode) {
|
||||
let 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 if (simpleMode) {
|
||||
// fdm polishing mode
|
||||
slice.groups = connectLines(slice.lines, slices.length);
|
||||
slice.groups.forEach(poly => {
|
||||
poly.swap(options.swapX, options.swapY);
|
||||
poly.move(move);
|
||||
poly.inner = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
slices.push(slice);
|
||||
return 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) {
|
||||
// map points to all other points they're connected to
|
||||
let DBUG = BASE.debug,
|
||||
CONF = BASE.config,
|
||||
pmap = {},
|
||||
points = [],
|
||||
output = [],
|
||||
connect = [],
|
||||
search = 1,
|
||||
nextMod = 1,
|
||||
bridge = CONF.bridgeLineGapDistance,
|
||||
p1, p2;
|
||||
|
||||
function cachedPoint(p) {
|
||||
let 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) {
|
||||
let 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) {
|
||||
let stack = [ ];
|
||||
if (paths.length > 10000) {
|
||||
DBUG.log("excessive path options @ "+paths.length+" #"+input.length);
|
||||
return;
|
||||
}
|
||||
for (;;) {
|
||||
stack.push(point);
|
||||
|
||||
let 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 (let 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);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
let longest = null,
|
||||
emitted = 0,
|
||||
closed = 0,
|
||||
open = 0;
|
||||
|
||||
paths.forEach(function(path) {
|
||||
// use longest perimeter vs longest path?
|
||||
if (!longest || path.length > longest.length) longest = path;
|
||||
if (!path.open) closed++; else 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 });
|
||||
|
||||
// emit polygons largest to smallest
|
||||
// omit polygon if it intersects previously emitted (has del points)
|
||||
paths.forEach(function(path) {
|
||||
if (path.length < 3) return;
|
||||
let 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++;
|
||||
});
|
||||
} 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) {
|
||||
let 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) {
|
||||
let 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 (let i=0; i<connect.length; i++) {
|
||||
|
||||
let 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 (let merged=0;;) {
|
||||
let 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) {
|
||||
let output = [],
|
||||
tmplines = [],
|
||||
points = [],
|
||||
pmap = {};
|
||||
|
||||
function cachePoint(p) {
|
||||
let 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;
|
||||
let 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
|
||||
let 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;
|
||||
}
|
||||
|
||||
})();
|
||||
586
src/kiri/ui.js
Normal file
586
src/kiri/ui.js
Normal file
|
|
@ -0,0 +1,586 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (kiri.ui) return;
|
||||
|
||||
let SELF = self,
|
||||
KIRI = self.kiri,
|
||||
lastGroup = null,
|
||||
lastDiv = null,
|
||||
addTo = null,
|
||||
hideAction = null,
|
||||
inputAction = null,
|
||||
groups = {},
|
||||
groupShow = {},
|
||||
groupName = undefined,
|
||||
poppable = {},
|
||||
hasModes = [],
|
||||
isExpert = [],
|
||||
letMode = null,
|
||||
letExpert = false,
|
||||
DOC = SELF.document,
|
||||
state = {},
|
||||
prefix = "tab",
|
||||
NOMODE = "nomode",
|
||||
exitimer = undefined;
|
||||
|
||||
SELF.$ = (SELF.$ || function (id) { return DOC.getElementById(id) } );
|
||||
|
||||
KIRI.ui = {
|
||||
prefix: function(pre) { prefix = pre; return kiri.ui },
|
||||
hideAction: function(fn) { hideAction = fn; return kiri.ui },
|
||||
inputAction: function(fn) { inputAction = fn; return kiri.ui },
|
||||
setMode: setMode,
|
||||
setExpert: setExpert,
|
||||
bound: bound,
|
||||
toInt: toInt,
|
||||
toFloat: toFloat,
|
||||
hidePop: hidePop,
|
||||
isPopped: isPopped,
|
||||
newText: newTextArea,
|
||||
newLabel: newLabel,
|
||||
newRange: newRange,
|
||||
newInput: newInputField,
|
||||
newButton: newButton,
|
||||
newBoolean: newBooleanField,
|
||||
newSelect: newSelect,
|
||||
newTable: newTables,
|
||||
newTableRow: newTableRow,
|
||||
newRow: newRow,
|
||||
newBlank: newBlank,
|
||||
newGroup: newGroup,
|
||||
setGroup: setGroup,
|
||||
hidePoppers: hidePoppers,
|
||||
checkpoint,
|
||||
restore
|
||||
};
|
||||
|
||||
function setMode(mode) {
|
||||
Object.keys(groupShow).forEach(group => {
|
||||
updateGroupShow(group);
|
||||
});
|
||||
letMode = mode;
|
||||
hasModes.forEach(function(div) {
|
||||
div.setMode(div._group && !groupShow[div._group] ? NOMODE : mode);
|
||||
});
|
||||
hidePoppers();
|
||||
}
|
||||
|
||||
function setExpert(bool) {
|
||||
letExpert = bool;
|
||||
setMode(letMode);
|
||||
}
|
||||
|
||||
function checkpoint() {
|
||||
return { addTo, lastDiv, lastGroup, groupName };
|
||||
}
|
||||
|
||||
function restore(opt) {
|
||||
if (opt) {
|
||||
addTo = opt.addTo;
|
||||
lastDiv = opt.lastDiv;
|
||||
lastGroup = opt.lastGroup;
|
||||
groupName = opt.groupName;
|
||||
}
|
||||
}
|
||||
|
||||
// at present only used by the layers popup menu
|
||||
function setGroup(div) {
|
||||
addTo = lastDiv = div;
|
||||
groupName = undefined;
|
||||
lastGroup = [];
|
||||
return div;
|
||||
}
|
||||
|
||||
function newGroup(label, div, options) {
|
||||
lastDiv = div || lastDiv;
|
||||
return addCollapsableGroup(label, lastDiv, options);
|
||||
}
|
||||
|
||||
function addCollapsableGroup(label, div, options) {
|
||||
let opt = options || {};
|
||||
let group = opt.group || label;
|
||||
poppable[group] = opt.inline === true ? false : true;
|
||||
|
||||
let row = DOC.createElement('div'),
|
||||
a = DOC.createElement('a'),
|
||||
dbkey = `beta-${prefix}-show-${group}`,
|
||||
popper = !opt.inline;
|
||||
|
||||
if (popper) {
|
||||
let pop = DOC.createElement('div');
|
||||
pop.classList.add('set-pop');
|
||||
row.appendChild(pop);
|
||||
row.setAttribute("class", "set-group noselect");
|
||||
addTo = pop;
|
||||
} else {
|
||||
row.setAttribute("class", "set-header col");
|
||||
addTo = lastDiv;
|
||||
}
|
||||
|
||||
div.appendChild(row);
|
||||
row.appendChild(a);
|
||||
a.appendChild(DOC.createTextNode(label));
|
||||
addModeControls(row, opt);
|
||||
lastGroup = groups[group] = [];
|
||||
lastGroup.key = dbkey;
|
||||
groupName = group;
|
||||
groupShow[group] = popper ? state[dbkey] === 'true' : state[dbkey] !== 'false';
|
||||
row.onclick = function(ev) {
|
||||
if (ev.target !== a && ev.target !== row) {
|
||||
return;
|
||||
}
|
||||
toggleGroup(group, dbkey);
|
||||
};
|
||||
if (popper) {
|
||||
row.onmouseenter = function(ev) {
|
||||
if (ev.target !== a && ev.target !== row) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(exitimer);
|
||||
showGroup(group);
|
||||
};
|
||||
row.onmouseleave = function(ev) {
|
||||
if (ev.target !== a && ev.target !== row) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(exitimer);
|
||||
exitimer = setTimeout(showGroup, 1000);
|
||||
};
|
||||
addTo._group = group;
|
||||
addModeControls(addTo, opt);
|
||||
lastGroup.push(addTo);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
function hidePoppers() {
|
||||
showGroup(undefined);
|
||||
}
|
||||
|
||||
function showGroup(groupname) {
|
||||
Object.keys(groups).forEach(name => {
|
||||
let group = groups[name];
|
||||
groupShow[name] = state[group.key] = (groupname === name);
|
||||
updateGroupShow(name);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleGroup(groupname, dbkey) {
|
||||
let show = state[dbkey] === 'false';
|
||||
groupShow[groupname] = poppable[groupname] ? state[dbkey] = show : true;
|
||||
updateGroupShow(groupname);
|
||||
}
|
||||
|
||||
function updateGroupShow(groupname) {
|
||||
let show = groupShow[groupname] || !poppable[groupname];
|
||||
let group = groups[groupname];
|
||||
group.forEach(div => {
|
||||
if (show) div.setMode(letMode);
|
||||
else div.setMode(NOMODE);
|
||||
});
|
||||
}
|
||||
|
||||
function toInt() {
|
||||
let nv = this.value !== '' ? parseInt(this.value) : null;
|
||||
if (isNaN(nv)) nv = 0;
|
||||
if (nv !== null && this.bound) nv = this.bound(nv);
|
||||
this.value = nv;
|
||||
return nv;
|
||||
}
|
||||
|
||||
function toFloat() {
|
||||
let 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) {
|
||||
if (isNaN(v)) return low;
|
||||
return v < low ? low : v > high ? high : v;
|
||||
};
|
||||
}
|
||||
|
||||
function raw() {
|
||||
return this.value !== '' ? this.value : null;
|
||||
}
|
||||
|
||||
function newLabel(text) {
|
||||
let 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) {
|
||||
options = 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) {
|
||||
let show = options.expert === undefined || (options.expert === letExpert);
|
||||
el.setVisible(el.hasMode(mode) && show);
|
||||
}
|
||||
el.hasMode = function(mode) {
|
||||
if (mode === NOMODE) return false;
|
||||
if (!el.modes) return true;
|
||||
return el.modes.contains(mode);
|
||||
}
|
||||
if (options.modes) {
|
||||
el.modes = options.modes;
|
||||
hasModes.push(el);
|
||||
}
|
||||
}
|
||||
|
||||
function newDiv(options) {
|
||||
let div = DOC.createElement('div');
|
||||
addModeControls(div, options);
|
||||
addTo.appendChild(div);
|
||||
lastGroup.push(div);
|
||||
div._group = groupName;
|
||||
return div;
|
||||
}
|
||||
|
||||
let lastTxt = null;
|
||||
let lastPop = null;
|
||||
let savePop = null;
|
||||
|
||||
function hidePop() {
|
||||
if (lastPop) {
|
||||
lastPop.style.display = "none";
|
||||
}
|
||||
if (savePop) {
|
||||
savePop();
|
||||
savePop = null;
|
||||
}
|
||||
lastPop = null;
|
||||
}
|
||||
|
||||
function isPopped() {
|
||||
return lastPop !== null;
|
||||
}
|
||||
|
||||
function newTextArea(label, options) {
|
||||
let opt = options || {},
|
||||
row = newDiv(options),
|
||||
btn = DOC.createElement("button"),
|
||||
pop = DOC.createElement("div"),
|
||||
txt = DOC.createElement("textarea"),
|
||||
area = opt.area;
|
||||
|
||||
txt.setAttribute("wrap", "off");
|
||||
txt.setAttribute("spellcheck", "false");
|
||||
txt.setAttribute("style", "resize: none");
|
||||
txt.onblur = inputAction;
|
||||
|
||||
btn.appendChild(DOC.createTextNode("edit"));
|
||||
|
||||
btn.onclick = function(ev) {
|
||||
ev.stopPropagation();
|
||||
if (ev.target === txt) {
|
||||
// drop clicks on TextArea
|
||||
ev.target.focus();
|
||||
} else {
|
||||
let fc = area.firstChild;
|
||||
if (fc) area.removeChild(fc);
|
||||
area.appendChild(txt);
|
||||
// first time, button click / show
|
||||
btn.parentNode.onclick = btn.onclick;
|
||||
txt.scrollTop = 0;
|
||||
txt.scrollLeft = 0;
|
||||
txt.selectionEnd = 0;
|
||||
let rows = txt.value.split('\n');
|
||||
let cols = 0;
|
||||
rows.forEach(row => {
|
||||
cols = Math.max(cols, row.length);
|
||||
});
|
||||
txt.setAttribute("cols", Math.max(30, cols + 1));
|
||||
txt.setAttribute("rows", Math.max(10, rows.length + 1));
|
||||
|
||||
let showing = pop === lastPop;
|
||||
hidePop();
|
||||
if (!showing) {
|
||||
if (lastTxt) {
|
||||
lastTxt.classList.remove('txt-sel');
|
||||
}
|
||||
row.classList.add('txt-sel');
|
||||
savePop = inputAction;
|
||||
pop.style.display = "flex";
|
||||
lastPop = pop;
|
||||
lastTxt = row;
|
||||
txt.focus();
|
||||
} else {
|
||||
inputAction();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addModeControls(btn, opt);
|
||||
addId(btn, opt);
|
||||
|
||||
row.appendChild(newLabel(label));
|
||||
row.appendChild(btn);
|
||||
row.setAttribute("class", "var-row");
|
||||
if (opt.title) row.setAttribute("title", options.title);
|
||||
btn.setVisible = row.setVisible;
|
||||
|
||||
return txt;
|
||||
}
|
||||
|
||||
function newInputField(label, options) {
|
||||
let opt = options || {},
|
||||
row = newDiv(opt),
|
||||
hide = opt && opt.hide,
|
||||
size = opt ? opt.size || 5 : 5,
|
||||
height = opt ? opt.height : 0,
|
||||
ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input'),
|
||||
action = opt.action || inputAction;
|
||||
|
||||
row.appendChild(newLabel(label));
|
||||
row.appendChild(ip);
|
||||
row.setAttribute("class", "var-row");
|
||||
if (height > 1) {
|
||||
ip.setAttribute("cols", size);
|
||||
ip.setAttribute("rows", height);
|
||||
ip.setAttribute("wrap", "off");
|
||||
} else {
|
||||
if (Number.isInteger(size)) {
|
||||
ip.setAttribute("size", size);
|
||||
} else {
|
||||
ip.setAttribute("style", `width:${size}`);
|
||||
}
|
||||
}
|
||||
ip.setAttribute("type", "text");
|
||||
ip.setAttribute("spellcheck", "false");
|
||||
row.style.display = hide ? 'none' : '';
|
||||
if (opt) {
|
||||
if (opt.disabled) ip.setAttribute("disabled", "true");
|
||||
if (opt.title) row.setAttribute("title", opt.title);
|
||||
if (opt.convert) ip.convert = opt.convert.bind(ip);
|
||||
if (opt.bound) ip.bound = opt.bound;
|
||||
if (opt.action) action = opt.action;
|
||||
}
|
||||
ip.addEventListener('focus', function(event) {
|
||||
hidePop();
|
||||
});
|
||||
if (action) {
|
||||
ip.addEventListener('keydown', function(event) {
|
||||
let key = event.key;
|
||||
if (
|
||||
opt.text ||
|
||||
(key >= '0' && key <= '9') ||
|
||||
key === '.' ||
|
||||
key === '-' ||
|
||||
key === 'Backspace' ||
|
||||
key === 'Delete' ||
|
||||
key === 'ArrowLeft' ||
|
||||
key === 'ArrowRight' ||
|
||||
key === 'Tab' ||
|
||||
event.metaKey ||
|
||||
event.ctrlKey ||
|
||||
(key === ',' && options.comma)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
ip.addEventListener('keyup', function(event) {
|
||||
if (event.keyCode === 13) {
|
||||
// action(event);
|
||||
ip.blur();
|
||||
}
|
||||
});
|
||||
ip.addEventListener('blur', function(event) {
|
||||
action(event);
|
||||
});
|
||||
}
|
||||
if (!ip.convert) ip.convert = raw.bind(ip);
|
||||
ip.setVisible = row.setVisible;
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
function newRange(label, options) {
|
||||
let row = newDiv(options),
|
||||
ip = DOC.createElement('input'),
|
||||
hide = options && options.hide,
|
||||
action = inputAction;
|
||||
|
||||
if (label) row.appendChild(newLabel(label));
|
||||
row.appendChild(ip);
|
||||
row.setAttribute("class", "var-row");
|
||||
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 newSelect(label, options, source) {
|
||||
let row = newDiv(options),
|
||||
ip = DOC.createElement('select'),
|
||||
hide = options && options.hide,
|
||||
action = inputAction;
|
||||
|
||||
row.appendChild(newLabel(label));
|
||||
row.appendChild(ip);
|
||||
row.setAttribute("source", source || "tools");
|
||||
row.setAttribute("class", "var-row");
|
||||
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) {
|
||||
let row = newDiv(options),
|
||||
ip = DOC.createElement('input'),
|
||||
hide = options && options.hide;
|
||||
|
||||
if (label) {
|
||||
row.appendChild(newLabel(label));
|
||||
}
|
||||
row.appendChild(ip);
|
||||
row.setAttribute("class", "var-row");
|
||||
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) {
|
||||
let row = newDiv(options),
|
||||
hide = options && options.hide;
|
||||
|
||||
row.setAttribute("class", "var-row");
|
||||
row.style.display = hide ? 'none' : '';
|
||||
ip.setVisible = row.setVisible;
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
function newButton(label, action, options) {
|
||||
let opt = options || {},
|
||||
b = DOC.createElement('button');
|
||||
|
||||
b.onclick = function() {
|
||||
hidePoppers();
|
||||
if (action) action();
|
||||
};
|
||||
|
||||
if (opt.class) {
|
||||
opt.class.split(' ').forEach(ce => {
|
||||
b.classList.add(ce);
|
||||
});
|
||||
}
|
||||
if (opt.icon) {
|
||||
let d = DOC.createElement('div');
|
||||
d.innerHTML = opt.icon;
|
||||
b.appendChild(d);
|
||||
}
|
||||
if (label) {
|
||||
b.appendChild(DOC.createTextNode(label));
|
||||
}
|
||||
|
||||
addModeControls(b, options);
|
||||
addId(b, options);
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
function newTableRow(arrayOfArrays, options) {
|
||||
return newRow(newTables(arrayOfArrays), options);
|
||||
}
|
||||
|
||||
function newTables(arrayOfArrays) {
|
||||
let array = [];
|
||||
for (let i=0; i<arrayOfArrays.length; i++) {
|
||||
array.push(newRowTable(arrayOfArrays[i]));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
function newRowTable(array) {
|
||||
let div = newDiv();
|
||||
div.setAttribute("class", "table-row");
|
||||
array.forEach(function(c) {
|
||||
div.appendChild(c);
|
||||
});
|
||||
return div;
|
||||
}
|
||||
|
||||
function newRow(children, options) {
|
||||
let row = addCollapsableElement((options && options.noadd) ? null : addTo);
|
||||
if (children) children.forEach(function (c) { row.appendChild(c) });
|
||||
addModeControls(row, options);
|
||||
if (options && options.class) {
|
||||
options.class.split(' ').forEach(ce => {
|
||||
row.classList.add(ce);
|
||||
});
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
function addCollapsableElement(parent) {
|
||||
let row = newDiv();
|
||||
if (parent) parent.appendChild(row);
|
||||
if (lastGroup) lastGroup.push(row);
|
||||
return row;
|
||||
}
|
||||
|
||||
})();
|
||||
747
src/kiri/widget.js
Normal file
747
src/kiri/widget.js
Normal file
|
|
@ -0,0 +1,747 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.kiri.Widget) return;
|
||||
|
||||
const KIRI = self.kiri,
|
||||
DRIVERS = KIRI.driver,
|
||||
BASE = self.base,
|
||||
CONF = BASE.config,
|
||||
DBUG = BASE.debug,
|
||||
UTIL = BASE.util,
|
||||
POLY = BASE.polygons,
|
||||
MATH = Math,
|
||||
SQRT = MATH.sqrt,
|
||||
PRO = Widget.prototype,
|
||||
time = UTIL.time,
|
||||
solid_opacity = 1.0;
|
||||
|
||||
let nextId = 0,
|
||||
groups = [];
|
||||
|
||||
KIRI.Widget = Widget;
|
||||
KIRI.newWidget = newWidget;
|
||||
|
||||
function newWidget(id,group) { return new Widget(id,group) }
|
||||
|
||||
/** ******************************************************************
|
||||
* Group helpers
|
||||
******************************************************************* */
|
||||
|
||||
let Group = Widget.Groups = {
|
||||
|
||||
forid: function(id) {
|
||||
for (let i=0; i<groups.length; i++) {
|
||||
if (groups[i].id === id) return groups[i];
|
||||
}
|
||||
let group = [];
|
||||
group.id = id;
|
||||
groups.push(group);
|
||||
return group;
|
||||
},
|
||||
|
||||
remove: function(widget) {
|
||||
groups.slice().forEach(group => {
|
||||
let pos = group.indexOf(widget);
|
||||
if (pos >= 0) {
|
||||
group.splice(pos,1);
|
||||
}
|
||||
if (group.length === 0) {
|
||||
pos = groups.indexOf(group);
|
||||
groups.splice(pos,1);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
blocks: function() {
|
||||
return groups.map(group => {
|
||||
return {
|
||||
w: group[0].track.box.w,
|
||||
h: group[0].track.box.h,
|
||||
move: (x,y,z,abs) => {
|
||||
group.forEach(widget => {
|
||||
widget.mesh.material.visible = true;
|
||||
widget._move(x, y, z, abs);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
loadDone: function() {
|
||||
groups.forEach(group => {
|
||||
if (!group.centered) {
|
||||
group[0].center();
|
||||
group.centered = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
bounds: function(group) {
|
||||
let bounds = null;
|
||||
group.forEach(widget => {
|
||||
let wb = widget.mesh.getBoundingBox(true);
|
||||
if (bounds) {
|
||||
bounds = bounds.union(wb);
|
||||
} else {
|
||||
bounds = wb;
|
||||
}
|
||||
});
|
||||
return bounds;
|
||||
}
|
||||
};
|
||||
|
||||
/** ******************************************************************
|
||||
* Constructor
|
||||
******************************************************************* */
|
||||
|
||||
/**
|
||||
* @params {String} [id]
|
||||
* @constructor
|
||||
*/
|
||||
function Widget(id,group) {
|
||||
this.id = id || new Date().getTime().toString(36)+(nextId++);
|
||||
this.group = group || [];
|
||||
this.group.push(this);
|
||||
if (!this.group.id) {
|
||||
this.group.id = this.id;
|
||||
}
|
||||
if (groups.indexOf(this.group) < 0) {
|
||||
groups.push(this.group);
|
||||
}
|
||||
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.track = {
|
||||
// box size for packer
|
||||
box: {
|
||||
w: 0,
|
||||
h: 0,
|
||||
d: 0
|
||||
},
|
||||
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;
|
||||
this.support = false; // is synthesized support widget
|
||||
}
|
||||
|
||||
/** ******************************************************************
|
||||
* Widget Class Functions
|
||||
******************************************************************* */
|
||||
|
||||
Widget.loadFromCatalog = function(filename, ondone) {
|
||||
KIRI.catalog.getFile(filename, function(data) {
|
||||
ondone(newWidget().loadVertices(data));
|
||||
});
|
||||
};
|
||||
|
||||
Widget.loadFromState = function(id, ondone, move) {
|
||||
KIRI.odb.get('ws-save-'+id, function(data) {
|
||||
if (data) {
|
||||
let vertices = data.geo || data,
|
||||
track = data.track || undefined,
|
||||
group = data.group || id,
|
||||
widget = newWidget(id, Group.forid(group));
|
||||
widget.saved = time();
|
||||
ondone(widget.loadVertices(vertices));
|
||||
// restore widget position if specified
|
||||
if (move && track && track.pos) {
|
||||
widget.track = track;
|
||||
widget.move(track.pos.x, track.pos.y, track.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) {
|
||||
let 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) {
|
||||
let p = BASE.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) {
|
||||
let lines = [], line, dec = 0;
|
||||
for (i=0; i<oldpoints; ) {
|
||||
let 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; ) {
|
||||
let 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) {
|
||||
let 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
|
||||
******************************************************************* */
|
||||
|
||||
PRO.saveToCatalog = function(filename) {
|
||||
let widget = this;
|
||||
let 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;
|
||||
};
|
||||
|
||||
PRO.saveState = function(ondone) {
|
||||
let widget = this;
|
||||
KIRI.odb.put('ws-save-'+this.id, {
|
||||
geo:widget.getGeoVertices(),
|
||||
track:widget.track,
|
||||
group:this.group.id
|
||||
}, function(result) {
|
||||
widget.saved = time();
|
||||
if (ondone) ondone();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Float32Array} vertices
|
||||
* @returns {Widget}
|
||||
*/
|
||||
PRO.loadVertices = function(vertices) {
|
||||
if (this.mesh) {
|
||||
this.mesh.geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
|
||||
this.mesh.geometry.computeFaceNormals();
|
||||
this.mesh.geometry.computeVertexNormals();
|
||||
this.points = null;
|
||||
return this;
|
||||
} else {
|
||||
let geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
|
||||
return this.loadGeometry(geometry);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {THREE.Geometry} geometry
|
||||
* @returns {Widget}
|
||||
*/
|
||||
PRO.loadGeometry = function(geometry) {
|
||||
let 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(true);
|
||||
return this;
|
||||
};
|
||||
|
||||
PRO.groupBounds = function() {
|
||||
return Group.bounds(this.group);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Point[]} points
|
||||
* @returns {Widget}
|
||||
*/
|
||||
PRO.setPoints = function(points) {
|
||||
this.points = points || null;
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* remove slice data and their views
|
||||
*/
|
||||
PRO.clearSlices = function() {
|
||||
let slices = this.slices,
|
||||
mesh = this.mesh;
|
||||
if (slices) {
|
||||
slices.forEach(function(slice) {
|
||||
mesh.remove(slice.view);
|
||||
});
|
||||
this.slices = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {number} color
|
||||
*/
|
||||
PRO.setColor = function(color,settings) {
|
||||
if (Array.isArray(color)) {
|
||||
color = color[this.getExtruder(settings) % color.length];
|
||||
}
|
||||
let material = this.mesh.material;
|
||||
material.color.set(color);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
*/
|
||||
PRO.setOpacity = function(value) {
|
||||
let 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
|
||||
*/
|
||||
PRO.center = function(init) {
|
||||
let bb = init ? this.mesh.getBoundingBox(true) : this.groupBounds(),
|
||||
bm = bb.min.clone(),
|
||||
bM = bb.max.clone(),
|
||||
bd = bM.sub(bm).multiplyScalar(0.5),
|
||||
dx = bm.x + bd.x,
|
||||
dy = bm.y + bd.y,
|
||||
dz = bm.z;
|
||||
// move mesh for each widget in group
|
||||
if (!init) {
|
||||
this.group.forEach(w => {
|
||||
w.moveMesh(dx,dy,dz);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* called by center() and Group.center()
|
||||
*/
|
||||
PRO.moveMesh = function(x, y, z) {
|
||||
let gap = this.mesh.geometry.attributes.position,
|
||||
pa = gap.array;
|
||||
// center point array on 0,0,0
|
||||
for (let i=0; i < pa.length; i += 3) {
|
||||
pa[i ] -= x;
|
||||
pa[i + 1] -= y;
|
||||
pa[i + 2] -= z;
|
||||
}
|
||||
gap.needsUpdate = true;
|
||||
let bb = this.groupBounds();
|
||||
this.track.box = {
|
||||
w: (bb.max.x - bb.min.x),
|
||||
h: (bb.max.y - bb.min.y),
|
||||
d: (bb.max.z - bb.min.z)
|
||||
};
|
||||
// for use with the packer
|
||||
// invalidate cached points
|
||||
this.points = null;
|
||||
this.modified = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* moves top of widget to given Z
|
||||
* used in CAM mode
|
||||
*
|
||||
* @param {number} z position
|
||||
*/
|
||||
PRO.setTopZ = function(z) {
|
||||
let mesh = this.mesh,
|
||||
pos = this.track.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;
|
||||
}
|
||||
|
||||
PRO.move = function(x, y, z, abs) {
|
||||
this.group.forEach(w => {
|
||||
w._move(x, y, z, abs);
|
||||
});
|
||||
};
|
||||
|
||||
PRO._move = function(x, y, z, abs) {
|
||||
let mesh = this.mesh,
|
||||
pos = this.track.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;
|
||||
KIRI.api.event.emit('widget.move', {widget: this, pos});
|
||||
}
|
||||
};
|
||||
|
||||
PRO.scale = function(x, y, z) {
|
||||
this.group.forEach(w => {
|
||||
w._scale(x, y, z);
|
||||
});
|
||||
this.center();
|
||||
};
|
||||
|
||||
PRO._scale = function(x, y, z) {
|
||||
let mesh = this.mesh,
|
||||
scale = this.track.scale;
|
||||
this.bounds = null;
|
||||
this.setWireframe(false);
|
||||
this.clearSlices();
|
||||
mesh.geometry.applyMatrix4(new THREE.Matrix4().makeScale(x, y, z));
|
||||
scale.x *= (x || 1.0);
|
||||
scale.y *= (y || 1.0);
|
||||
scale.z *= (z || 1.0);
|
||||
};
|
||||
|
||||
PRO.rotate = function(x, y, z) {
|
||||
this.group.forEach(w => {
|
||||
w._rotate(x, y, z, false);
|
||||
});
|
||||
this.center();
|
||||
};
|
||||
|
||||
PRO._rotate = function(x, y, z, temp) {
|
||||
if (!temp) {
|
||||
this.bounds = null;
|
||||
this.setWireframe(false);
|
||||
this.clearSlices();
|
||||
}
|
||||
let m4 = new THREE.Matrix4();
|
||||
let euler = typeof(x) === 'number';
|
||||
if (euler) {
|
||||
m4 = m4.makeRotationFromEuler(new THREE.Euler(x || 0, y || 0, z || 0));
|
||||
} else {
|
||||
m4 = m4.makeRotationFromQuaternion(x);
|
||||
}
|
||||
this.mesh.geometry.applyMatrix4(m4);
|
||||
if (!temp && euler) {
|
||||
let rot = this.track.rot;
|
||||
rot.x += (x || 0);
|
||||
rot.y += (y || 0);
|
||||
rot.z += (z || 0);
|
||||
}
|
||||
};
|
||||
|
||||
PRO.mirror = function() {
|
||||
this.group.forEach(w => {
|
||||
w._mirror();
|
||||
});
|
||||
this.center();
|
||||
};
|
||||
|
||||
PRO._mirror = function() {
|
||||
this.setWireframe(false);
|
||||
this.clearSlices();
|
||||
let i,
|
||||
o = this.track,
|
||||
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();
|
||||
o.mirror = !o.mirror;
|
||||
};
|
||||
|
||||
PRO.getGeoVertices = function() {
|
||||
return this.mesh.geometry.getAttribute('position').array;
|
||||
};
|
||||
|
||||
PRO.getPoints = function() {
|
||||
if (!this.points) {
|
||||
// convert and cache points from geometry vertices
|
||||
this.points = Widget.verticesToPoints(this.getGeoVertices());
|
||||
}
|
||||
return this.points;
|
||||
};
|
||||
|
||||
PRO.getBoundingBox = function(refresh) {
|
||||
if (!this.bounds || refresh) {
|
||||
this.bounds = new THREE.Box3();
|
||||
this.bounds.setFromPoints(this.getPoints());
|
||||
}
|
||||
return this.bounds;
|
||||
};
|
||||
|
||||
PRO.isModified = function() {
|
||||
return this.modified;
|
||||
};
|
||||
|
||||
PRO.getExtruder = function(settings) {
|
||||
if (settings && settings.widget) {
|
||||
let rec = settings.widget[this.id];
|
||||
return rec && rec.extruder >= 0 ? rec.extruder : 0;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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]
|
||||
*/
|
||||
PRO.slice = function(settings, ondone, onupdate, remote) {
|
||||
let widget = this,
|
||||
startTime = UTIL.time();
|
||||
|
||||
widget.settings = settings;
|
||||
widget.clearSlices();
|
||||
onupdate(0.0001, "slicing");
|
||||
|
||||
if (remote) {
|
||||
// in case result of slice is nothing, do not preserve previous
|
||||
widget.slices = []
|
||||
|
||||
// executed from kiri.js
|
||||
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.slice) {
|
||||
widget.slices.push(KIRI.codec.decode(reply.slice, {mesh:widget.mesh}));
|
||||
}
|
||||
if (reply.polish) {
|
||||
widget.polish = KIRI.codec.decode(reply.polish);
|
||||
}
|
||||
if (reply.error) {
|
||||
ondone(false, reply.error);
|
||||
}
|
||||
if (reply.done) {
|
||||
widget.modified = false;
|
||||
ondone(true);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
// executed from kiri-worker.js
|
||||
let catchdone = function(error) {
|
||||
if (error) {
|
||||
return ondone(error);
|
||||
}
|
||||
|
||||
onupdate(1.0, "transferring");
|
||||
|
||||
widget.stats.slice_time = UTIL.time() - startTime;
|
||||
widget.modified = false;
|
||||
|
||||
ondone();
|
||||
};
|
||||
|
||||
let catchupdate = function(progress, message) {
|
||||
onupdate(progress, message);
|
||||
};
|
||||
|
||||
let driver = DRIVERS[settings.mode.toUpperCase()];
|
||||
|
||||
if (driver) {
|
||||
driver.slice(settings, widget, catchupdate, catchdone);
|
||||
} else {
|
||||
DBUG.log('invalid mode: '+settings.mode);
|
||||
ondone('invalid mode: '+settings.mode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PRO.getCamBounds = function(settings) {
|
||||
let bounds = this.getBoundingBox().clone();
|
||||
bounds.max.z += settings.process.camZTopOffset;
|
||||
return bounds;
|
||||
};
|
||||
|
||||
/**
|
||||
* render all slice and processed data
|
||||
*/
|
||||
PRO.render = function() {
|
||||
DRIVERS[this.settings.mode.toUpperCase()].sliceRender(this);
|
||||
};
|
||||
|
||||
PRO.hideSlices = function() {
|
||||
let showing = false;
|
||||
if (this.slices) this.slices.forEach(function(slice) {
|
||||
showing = showing || slice.view.visible;
|
||||
slice.view.visible = false;
|
||||
});
|
||||
return showing;
|
||||
};
|
||||
|
||||
PRO.toggleWireframe = function (color, opacity) {
|
||||
this.setWireframe(!this.wire, color, opacity);
|
||||
};
|
||||
|
||||
PRO.setWireframe = function(set, color, opacity) {
|
||||
let 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);
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
176
src/kiri/work.js
Normal file
176
src/kiri/work.js
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
// this code runs in kiri's main loop
|
||||
if (!self.kiri) self.kiri = {};
|
||||
|
||||
let loc = self.location,
|
||||
host = loc.hostname,
|
||||
port = loc.port,
|
||||
proto = loc.protocol,
|
||||
time = function() { return new Date().getTime() },
|
||||
KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
seqid = 1,
|
||||
running = {},
|
||||
slicing = {},
|
||||
worker = null;
|
||||
|
||||
function send(fn, data, onreply, async, zerocopy) {
|
||||
let seq = seqid++;
|
||||
|
||||
running[seq] = {fn:onreply, async:async||false};
|
||||
// console.log('send', data);
|
||||
worker.postMessage({
|
||||
seq: seq,
|
||||
task: fn,
|
||||
time: time(),
|
||||
data: data
|
||||
}, zerocopy);
|
||||
}
|
||||
|
||||
KIRI.work = {
|
||||
isSlicing : function() {
|
||||
let current = 0;
|
||||
for (let key in slicing) {
|
||||
current++;
|
||||
}
|
||||
return current > 0;
|
||||
},
|
||||
|
||||
restart : function() {
|
||||
if (worker) {
|
||||
worker.terminate();
|
||||
}
|
||||
|
||||
for (let key in slicing) {
|
||||
slicing[key]({error: "cancelled slicing"});
|
||||
}
|
||||
|
||||
slicing = {};
|
||||
running = {};
|
||||
worker = new Worker(`/code/worker.js?${self.kiri.version}`);
|
||||
|
||||
worker.onmessage = function(e) {
|
||||
let now = time(),
|
||||
reply = e.data,
|
||||
record = running[reply.seq],
|
||||
onreply = record.fn;
|
||||
// console.log('recv', reply.data)
|
||||
if (reply.done) {
|
||||
delete running[reply.seq];
|
||||
}
|
||||
|
||||
// calculate and replace recv time
|
||||
reply.time_recv = now - reply.time_recv;
|
||||
|
||||
onreply(reply.data, reply);
|
||||
};
|
||||
},
|
||||
|
||||
decimate : function(vertices, callback) {
|
||||
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});
|
||||
});
|
||||
},
|
||||
|
||||
snap : function(data) {
|
||||
send("snap", data, function(reply) {
|
||||
// console.log({snap:reply})
|
||||
});
|
||||
},
|
||||
|
||||
slice : function(settings, widget, callback) {
|
||||
let rotation = (Math.PI/180) * (settings.process.sliceRotation || 0);
|
||||
let centerz;
|
||||
let movez;
|
||||
if (rotation) {
|
||||
let bbox1 = widget.getBoundingBox(true);
|
||||
widget._rotate(0,rotation,0,true);
|
||||
widget.center();
|
||||
let bbox2 = widget.getBoundingBox(true);
|
||||
centerz = (bbox2.max.z - bbox2.min.z)/2;
|
||||
movez = centerz - (bbox1.max.z - bbox1.min.z)/2;
|
||||
}
|
||||
let vertices = widget.getGeoVertices().buffer.slice(0),
|
||||
snapshot = KIRI.api.view.snapshot;
|
||||
if (rotation) {
|
||||
widget._rotate(0,-rotation,0,true);
|
||||
widget.center();
|
||||
}
|
||||
slicing[widget.id] = callback;
|
||||
send("slice", {
|
||||
id: widget.id,
|
||||
settings: settings,
|
||||
vertices: vertices,
|
||||
position: widget.mesh.position,
|
||||
tracking: widget.track,
|
||||
snapshot: snapshot,
|
||||
// for rotation / unrotation
|
||||
state: {
|
||||
rotation: rotation,
|
||||
centerz: centerz,
|
||||
movez: movez
|
||||
}
|
||||
}, function(reply) {
|
||||
if (reply.done || reply.error) delete slicing[widget.id];
|
||||
callback(reply);
|
||||
}, null, [vertices]);
|
||||
},
|
||||
|
||||
printSetup : function(settings, callback) {
|
||||
send("printSetup", {settings:settings}, function(reply) {
|
||||
callback(reply);
|
||||
}, undefined);
|
||||
},
|
||||
|
||||
printExport : function(settings, online, ondone) {
|
||||
let lines = [];
|
||||
send("printExport", {settings:settings}, function(reply) {
|
||||
if (reply.line) {
|
||||
online(reply.line);
|
||||
}
|
||||
if (reply.done) {
|
||||
ondone(reply.done);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
printGCode : function(callback) {
|
||||
let gcode = [];
|
||||
let 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) {
|
||||
vertices = widget.getGeoVertices().buffer.slice(0);
|
||||
let wid = new Date().toString(36);
|
||||
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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// start worker
|
||||
KIRI.work.restart();
|
||||
208
src/kiri/worker.js
Normal file
208
src/kiri/worker.js
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
const base = self.base,
|
||||
util = base.util,
|
||||
time = util.time,
|
||||
kiri = self.kiri,
|
||||
ver = kiri.version,
|
||||
Widget = kiri.Widget;
|
||||
|
||||
let currentPrint,
|
||||
currentSnap,
|
||||
cache = {};
|
||||
|
||||
console.log(`kiri | init work | ${ver}`);
|
||||
base.debug.disable();
|
||||
|
||||
// catch clipper alerts and convert to console messages
|
||||
self.alert = function(o) {
|
||||
console.log(o);
|
||||
};
|
||||
|
||||
let dispatch = {
|
||||
decimate: function(vertices, send) {
|
||||
vertices = new Float32Array(vertices),
|
||||
vertices = Widget.pointsToVertices(Widget.verticesToPoints(vertices, true));
|
||||
send.done(vertices);
|
||||
},
|
||||
|
||||
snap: function(data, send) {
|
||||
currentSnap = data;
|
||||
send.done();
|
||||
},
|
||||
|
||||
slice: function(data, send) {
|
||||
let settings = data.settings,
|
||||
vertices = new Float32Array(data.vertices),
|
||||
position = data.position,
|
||||
tracking = data.tracking,
|
||||
points = Widget.verticesToPoints(vertices),
|
||||
state = data.state || {},
|
||||
rotation = state.rotation,
|
||||
centerz = state.centerz,
|
||||
movez = state.movez;
|
||||
|
||||
if (rotation) {
|
||||
state.rotate = new THREE.Matrix4().makeRotationY(-rotation);
|
||||
}
|
||||
|
||||
send.data({update:0.05, updateStatus:"slicing"});
|
||||
|
||||
let widget = kiri.newWidget(data.id).setPoints(points),
|
||||
last = util.time(),
|
||||
now;
|
||||
|
||||
// do it here so cancel can work
|
||||
cache[data.id] = widget;
|
||||
|
||||
// fake mesh object to satisfy printing
|
||||
widget.track = tracking;
|
||||
widget.mesh = {
|
||||
widget: widget,
|
||||
position: position
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
widget.slice(settings, function(error) {
|
||||
if (error) {
|
||||
delete cache[data.id];
|
||||
send.data({error: error});
|
||||
} else {
|
||||
let slices = widget.slices || [];
|
||||
send.data({send_start: time()});
|
||||
send.data({
|
||||
topo: settings.synth.sendTopo ? widget.topo : null,
|
||||
stats: widget.stats,
|
||||
slices: slices.length
|
||||
});
|
||||
slices.forEach(function(slice,index) {
|
||||
send.data({index: index, slice: slice.encode(state)});
|
||||
})
|
||||
if (self.debug && widget.polish) {
|
||||
send.data({polish: kiri.codec.encode(widget.polish)});
|
||||
}
|
||||
send.data({send_end: time()});
|
||||
}
|
||||
send.done({done: true});
|
||||
}, 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;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
send.data({error: error.toString()});
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
|
||||
printSetup: function(data, send) {
|
||||
let 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()
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
printExport: function(data, send) {
|
||||
currentPrint.export(false, function(line, direct) {
|
||||
send.data({line}, direct);
|
||||
}, function(done, direct) {
|
||||
send.done({done}, direct);
|
||||
});
|
||||
},
|
||||
|
||||
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) {
|
||||
currentSnap = null;
|
||||
currentPrint = null;
|
||||
if (!data.id) {
|
||||
cache = {};
|
||||
send.done({ clear: true });
|
||||
return;
|
||||
}
|
||||
let 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) {
|
||||
let 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) {
|
||||
let 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});
|
||||
}
|
||||
};
|
||||
19
src/license.js
Normal file
19
src/license.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
(function() {
|
||||
|
||||
let terms = {
|
||||
COPYRIGHT: "Copyright (C) Stewart Allen <sa@grid.space> - All Rights Reserved",
|
||||
LICENSE: "See the license.md file included with the source distribution",
|
||||
VERSION: "3.0.0-dev"
|
||||
};
|
||||
|
||||
if (typeof(module) === 'object') {
|
||||
module.exports = terms;
|
||||
} else if (self.kiri) {
|
||||
// self.exports = terms;
|
||||
self.kiri.license = terms.LICENSE;
|
||||
self.kiri.version = terms.VERSION;
|
||||
self.kiri.copyright = terms.COPYRIGHT;
|
||||
self.module = { exports: terms };
|
||||
}
|
||||
|
||||
})();
|
||||
2411
src/meta.js
Normal file
2411
src/meta.js
Normal file
File diff suppressed because it is too large
Load diff
1949
src/mode/cam/driver.js
Normal file
1949
src/mode/cam/driver.js
Normal file
File diff suppressed because it is too large
Load diff
1351
src/mode/fdm/driver.js
Normal file
1351
src/mode/fdm/driver.js
Normal file
File diff suppressed because it is too large
Load diff
465
src/mode/laser/driver.js
Normal file
465
src/mode/laser/driver.js
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (!self.kiri.driver) self.kiri.driver = { };
|
||||
if (self.kiri.driver.LASER) return;
|
||||
|
||||
const KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
POLY = BASE.polygons,
|
||||
DBUG = BASE.debug,
|
||||
LASER = KIRI.driver.LASER = {
|
||||
slice,
|
||||
sliceRender,
|
||||
printSetup,
|
||||
printRender,
|
||||
exportGCode,
|
||||
exportSVG,
|
||||
exportDXF
|
||||
},
|
||||
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)
|
||||
*/
|
||||
function slice(settings, widget, onupdate, ondone) {
|
||||
let proc = settings.process;
|
||||
|
||||
if (proc.laserSliceHeight < 0) {
|
||||
return ondone("invalid slice height");
|
||||
}
|
||||
|
||||
SLICER.sliceWidget(widget, {
|
||||
height: proc.laserSliceHeight, single: proc.laserSliceSingle
|
||||
}, function(slices) {
|
||||
widget.slices = slices;
|
||||
slices.forEach(function(slice, index) {
|
||||
slice.doShells(1, -proc.laserOffset);
|
||||
onupdate(0.80 + (index/slices.length) * 0.20);
|
||||
});
|
||||
ondone();
|
||||
}, function(update) {
|
||||
onupdate(0.0 + update * 0.80)
|
||||
});
|
||||
};
|
||||
|
||||
function sliceRender(widget) {
|
||||
return KIRI.driver.CAM.sliceRender(widget);
|
||||
}
|
||||
|
||||
function sliceEmitObjects(print, slice, groups) {
|
||||
let start = newPoint(0,0,0);
|
||||
let process = print.settings.process;
|
||||
let grouped = process.outputLaserGroup;
|
||||
let group = [];
|
||||
|
||||
function laserOut(poly, group) {
|
||||
if (!poly) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(poly)) {
|
||||
poly.forEach(function(pi) {
|
||||
laserOut(pi, group);
|
||||
});
|
||||
} else {
|
||||
print.polyPrintPath(poly, start, group, {extrude: 1});
|
||||
}
|
||||
}
|
||||
|
||||
slice.tops.forEach(function(top) {
|
||||
laserOut(top.traces, group);
|
||||
laserOut(top.innerTraces(), group);
|
||||
if (!grouped) {
|
||||
groups.push(group);
|
||||
group = [];
|
||||
}
|
||||
});
|
||||
|
||||
if (grouped) {
|
||||
groups.push(group);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* DRIVER PRINT CONTRACT
|
||||
*
|
||||
* @param {Object} print state object
|
||||
* @param {Function} update incremental callback
|
||||
*/
|
||||
function printSetup(print, update) {
|
||||
let widgets = print.widgets,
|
||||
settings = print.settings,
|
||||
device = settings.device,
|
||||
process = settings.process,
|
||||
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) {
|
||||
// slice stack merging
|
||||
if (process.outputLaserMerged) {
|
||||
let merged = [];
|
||||
widget.slices.forEach(function(slice) {
|
||||
let polys = [];
|
||||
slice.gatherTopPolys([]).forEach(p => p.flattenTo(polys));
|
||||
polys.forEach(p => {
|
||||
let match = false;
|
||||
for (let i=0; i<merged.length; i++) {
|
||||
let mp = merged[i];
|
||||
if (p.isEquivalent(mp)) {
|
||||
// increase weight
|
||||
match = true;
|
||||
mp.depth++;
|
||||
}
|
||||
}
|
||||
if (!match) {
|
||||
p.depth = 1;
|
||||
merged.push(p);
|
||||
}
|
||||
});
|
||||
update(slices++ / totalSlices);
|
||||
});
|
||||
let start = newPoint(0,0,0);
|
||||
let gather = [];
|
||||
merged.forEach(poly => {
|
||||
print.polyPrintPath(poly, start, gather, {
|
||||
extrude: poly.depth,
|
||||
rate: poly.depth * 10
|
||||
});
|
||||
});
|
||||
output.push(gather);
|
||||
} else {
|
||||
widget.slices.forEach(function(slice) {
|
||||
sliceEmitObjects(print, slice, output);
|
||||
update(slices++ / totalSlices);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// compute tile width / height
|
||||
output.forEach(function(layerout) {
|
||||
let min = {w:Infinity, h:Infinity}, max = {w:-Infinity, h:-Infinity}, p;
|
||||
layerout.forEach(function(out) {
|
||||
p = out.point;
|
||||
out.point = p.clone(); // b/c first/last point are often shared
|
||||
min.w = Math.min(min.w, p.x);
|
||||
max.w = Math.max(max.w, p.x);
|
||||
min.h = Math.min(min.h, p.y);
|
||||
max.h = Math.max(max.h, p.y);
|
||||
});
|
||||
layerout.w = max.w - min.w;
|
||||
layerout.h = max.h - min.h;
|
||||
// shift objects to top/left of w/h bounds
|
||||
layerout.forEach(function(out) {
|
||||
p = out.point;
|
||||
p.x -= min.w;
|
||||
p.y -= min.h;
|
||||
});
|
||||
});
|
||||
|
||||
// do object layout packing
|
||||
let i, m, e,
|
||||
MOTO = self.moto,
|
||||
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);
|
||||
|
||||
// test different ratios until packed
|
||||
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 + p.pad;
|
||||
m.fit.y += m.h + p.pad;
|
||||
m.forEach(function(o, i) {
|
||||
// because first point emitted twice (begin/end)
|
||||
e = o.point;
|
||||
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) {
|
||||
|
||||
let 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);
|
||||
});
|
||||
});
|
||||
|
||||
if (!process.outputOriginCenter) {
|
||||
// normalize against origin lower left
|
||||
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;
|
||||
} else {
|
||||
// normalize against center of build area
|
||||
let w = print.settings.device.bedWidth;
|
||||
let h = print.settings.device.bedDepth;
|
||||
output.forEach(function(layer) {
|
||||
layer.forEach(function(out) {
|
||||
point = out.point;
|
||||
point.x += w/2;
|
||||
point.y += h/2;
|
||||
});
|
||||
});
|
||||
max.x = w;
|
||||
max.y = h;
|
||||
min.x = 0;
|
||||
min.y = 0;
|
||||
}
|
||||
|
||||
onpre(min, max, process.outputLaserPower, process.outputLaserSpeed);
|
||||
|
||||
output.forEach(function(layer) {
|
||||
let color = 0;
|
||||
layer.forEach(function(out) {
|
||||
point = out.point;
|
||||
if (out.emit) {
|
||||
color = out.emit;
|
||||
if (last && poly.length === 0) {
|
||||
poly.push(onpoint(last));
|
||||
}
|
||||
poly.push(onpoint(point));
|
||||
} else if (poly.length > 0) {
|
||||
onpoly(poly, color);
|
||||
poly = [];
|
||||
}
|
||||
last = point;
|
||||
});
|
||||
if (poly.length > 0) {
|
||||
onpoly(poly, color);
|
||||
poly = [];
|
||||
}
|
||||
});
|
||||
|
||||
onpost();
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function exportGCode(print) {
|
||||
let lines = [], dx = 0, dy = 0, feedrate;
|
||||
let dev = print.settings.device;
|
||||
let space = dev.gcodeSpace ? ' ' : '';
|
||||
let power = 255;
|
||||
let laser_on = dev.gcodeLaserOn || [];
|
||||
let laser_off = dev.gcodeLaserOff || [];
|
||||
|
||||
exportElements(
|
||||
print,
|
||||
function(min, max, power, speed) {
|
||||
let width = (max.x - min.x),
|
||||
height = (max.y - min.y);
|
||||
|
||||
dx = min.x;
|
||||
dy = min.y;
|
||||
feedrate = `${space}F${speed}`;
|
||||
power = (256 * (power / 100)).toFixed(3);
|
||||
|
||||
(dev.gcodePre || []).forEach(line => {
|
||||
lines.push(line);
|
||||
});
|
||||
},
|
||||
function(poly, color) {
|
||||
poly.forEach(function(point, index) {
|
||||
if (index === 0) {
|
||||
lines.push(`G0${space}${point}`);
|
||||
} else if (index === 1) {
|
||||
laser_on.forEach(line => {
|
||||
lines.push(line.replace('{power}', power));
|
||||
});
|
||||
lines.push(`G1${space}${point}${feedrate}`);
|
||||
} else {
|
||||
lines.push(`G1${space}${point}`);
|
||||
}
|
||||
});
|
||||
laser_off.forEach(line => {
|
||||
lines.push(line);
|
||||
});
|
||||
},
|
||||
function() {
|
||||
(dev.gcodePost || []).forEach(line => {
|
||||
lines.push(line);
|
||||
});
|
||||
},
|
||||
function(point) {
|
||||
return `X${(point.x - dx).toFixed(3)}${space}Y${(point.y - dy).toFixed(3)}`;
|
||||
}
|
||||
);
|
||||
|
||||
return lines.join('\n');
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function exportSVG(print, cut_color) {
|
||||
let lines = [], dx = 0, dy = 0, my;
|
||||
let colors = [
|
||||
"black",
|
||||
"purple",
|
||||
"blue",
|
||||
"red",
|
||||
"orange",
|
||||
"yellow",
|
||||
"green",
|
||||
"brown",
|
||||
"gray"
|
||||
];
|
||||
|
||||
exportElements(
|
||||
print,
|
||||
function(min, max) {
|
||||
let 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, color) {
|
||||
let cout = colors[((color-1) % colors.length)];
|
||||
lines.push(`<polyline points="${poly.join(' ')}" fill="none" stroke="${cout}" stroke-width="0.1mm" />`);
|
||||
},
|
||||
function() {
|
||||
lines.push("</svg>");
|
||||
},
|
||||
function(point) {
|
||||
return UTIL.round(point.x - dx, 3) + "," + UTIL.round(my - point.y - dy, 3);
|
||||
}
|
||||
);
|
||||
|
||||
return lines.join('\n');
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function exportDXF(print) {
|
||||
let 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');
|
||||
};
|
||||
|
||||
function printRender(print) {
|
||||
return KIRI.driver.FDM.printRender(print, {aslines: true, nomoves: true});
|
||||
}
|
||||
|
||||
})();
|
||||
1449
src/mode/sla/driver.js
Normal file
1449
src/mode/sla/driver.js
Normal file
File diff suppressed because it is too large
Load diff
84
src/moto/ajax.js
Normal file
84
src/moto/ajax.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (!self.moto) self.moto = {};
|
||||
if (self.moto.Ajax) return;
|
||||
|
||||
const MOTO = self.moto;
|
||||
|
||||
MOTO.Ajax = Ajax;
|
||||
|
||||
MOTO.callAjax = function(url, handler) {
|
||||
new Ajax(handler).request(url);
|
||||
};
|
||||
|
||||
const AP = Ajax.prototype,
|
||||
KV = moto.KV,
|
||||
KEY = "moto-ajax",
|
||||
TIME = function() { return new Date().getTime() },
|
||||
MOKEY = moto.id = KV.getItem(KEY) || (TIME().toString(36)+rnd()+rnd());
|
||||
|
||||
MOTO.restore = function(id) {
|
||||
MOKEY = moto.id = id;
|
||||
KV.setItem(KEY, MOKEY);
|
||||
}
|
||||
|
||||
KV.setItem(KEY, MOKEY);
|
||||
|
||||
const 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);
|
||||
}
|
||||
|
||||
AP.onStateChange = function() {
|
||||
this.state = STATES[this.ajax.readyState];
|
||||
if (this.ajax.readyState === 4 && this.callback) {
|
||||
let 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 (let k in headers) {
|
||||
this.ajax.setRequestHeader(k, headers[k]);
|
||||
}
|
||||
if (send) {
|
||||
this.ajax.send(send);
|
||||
} else {
|
||||
this.ajax.send();
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
599
src/moto/ctrl.js
Normal file
599
src/moto/ctrl.js
Normal file
|
|
@ -0,0 +1,599 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
let MOTO = self.moto = self.moto || {};
|
||||
|
||||
/**
|
||||
* Adapted from THREE.OrbitControls
|
||||
*/
|
||||
|
||||
MOTO.CTRL = 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 = {
|
||||
UP: 38,
|
||||
LEFT: 37,
|
||||
RIGHT: 39,
|
||||
BOTTOM: 40
|
||||
};
|
||||
|
||||
// Mouse buttons
|
||||
this.mouseButtons = {
|
||||
ORBIT: THREE.MOUSE.LEFT,
|
||||
ZOOM: THREE.MOUSE.MIDDLE,
|
||||
PAN: THREE.MOUSE.RIGHT
|
||||
};
|
||||
|
||||
let 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(),
|
||||
// touch tracking
|
||||
firstTouch = [],
|
||||
touchSlide = false,
|
||||
lastDY = 0,
|
||||
// events
|
||||
changeEvent = { type: 'change'},
|
||||
startEvent = { type: 'start'},
|
||||
endEvent = { type: 'end'};
|
||||
|
||||
let 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) {
|
||||
let 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) {
|
||||
let 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) {
|
||||
let element = scope.domElement === document ? scope.domElement.body : scope.domElement;
|
||||
|
||||
switch (mode) {
|
||||
case MODE.PERSPECTIVE:
|
||||
let position = scope.object.position;
|
||||
let offset = position.clone().sub(scope.target);
|
||||
let 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 (let i=0; i<choices.length; i++) {
|
||||
let 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) {
|
||||
let 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 () {
|
||||
let 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));
|
||||
|
||||
let 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();
|
||||
|
||||
let 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;
|
||||
}
|
||||
|
||||
let delta = -(event.deltaY || event.wheelDelta || event.detail || 0);
|
||||
|
||||
if (delta === 0) return;
|
||||
|
||||
if (scope.reverseZoom) {
|
||||
delta = -delta;
|
||||
}
|
||||
|
||||
let saveZoomSpeed = scope.zoomSpeed;
|
||||
if (event.ctrlKey) scope.zoomSpeed *= 2;
|
||||
if (event.metaKey) scope.zoomSpeed *= 2;
|
||||
|
||||
if (delta > 0) {
|
||||
scope.dollyOut();
|
||||
} else if (delta < 0) {
|
||||
scope.dollyIn();
|
||||
}
|
||||
|
||||
scope.zoomSpeed = saveZoomSpeed;
|
||||
|
||||
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;
|
||||
|
||||
let dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
|
||||
let dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
|
||||
let distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
firstTouch = [...event.touches].map(v => { return {x:v.pageX, y:v.pageY} });
|
||||
touchSlide = false;
|
||||
lastDY = 0;
|
||||
|
||||
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();
|
||||
|
||||
let 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;
|
||||
let dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
|
||||
let dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
|
||||
let distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
let touches = [...event.touches].map(v => { return {x:v.pageX, y:v.pageY} });
|
||||
let ddx = ( (touches[0].x - firstTouch[0].x) + (touches[1].x - firstTouch[1].x) ) / 2;
|
||||
let ddy = ( (touches[0].y - firstTouch[0].y) + (touches[1].y - firstTouch[1].y) ) / 2;
|
||||
|
||||
if (scope.reverseZoom) distance = -distance;
|
||||
|
||||
dollyEnd.set(0, distance);
|
||||
dollyDelta.subVectors(dollyEnd, dollyStart);
|
||||
|
||||
if (touchSlide || (Math.abs(ddy/ddx) > 10 && slider)) {
|
||||
touchSlide = true;
|
||||
slider(lastDY ? lastDY - ddy : ddy);
|
||||
lastDY = ddy;
|
||||
} else {
|
||||
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);
|
||||
};
|
||||
|
||||
MOTO.CTRL.prototype = Object.create(THREE.EventDispatcher.prototype);
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue