merge development branch

This commit is contained in:
Stewart Allen 2020-06-12 11:10:52 -04:00
commit 9b0fa9dedf
128 changed files with 3943 additions and 4261 deletions

139
src/add/array.js Normal file
View 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
View 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
View 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 = [];
};
})();

View file

@ -0,0 +1,22 @@
{
"file-ext": "nc",
"token-space": " ",
"strip-comments": true,
"pre":[
"G21 ; set units to MM (required)",
"G90 ; absolute position mode (required)"
],
"post":[
"M30 ; program end"
],
"tool-change":[
"M6 T{tool} ; change tool to '{tool_name}'"
],
"dwell":[
"G4 P{time} ; dwell for {time}ms"
],
"settings": {
"bed_width": 400,
"bed_depth": 400
}
}

View file

@ -0,0 +1,24 @@
{
"file-ext": "ngc",
"token-space": " ",
"strip-comments": true,
"pre":[
"G21 ; set units to MM (required)",
"G90 ; absolute position mode (required)",
"M03 S20000 ; spindle on"
],
"post":[
"M05 ; spindle off",
"M30 ; program end"
],
"tool-change":[
"M6 T{tool} ; change tool to '{tool_name}'"
],
"dwell":[
"G4 P{time} ; dwell for {time}ms"
],
"settings": {
"bed_width": 400,
"bed_depth": 400
}
}

View file

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

View file

@ -0,0 +1,22 @@
{
"file-ext": "nc",
"token-space": " ",
"strip-comments": true,
"pre":[
"G21 ; set units to MM (required)",
"G90 ; absolute position mode (required)"
],
"post":[
"M30 ; program end"
],
"tool-change":[
"M6 T{tool} ; change tool to '{tool_name}'"
],
"dwell":[
"G4 P{time} ; dwell for {time}ms"
],
"settings": {
"bed_width": 200,
"bed_depth": 200
}
}

View file

@ -0,0 +1,22 @@
{
"file-ext": "nc",
"token-space": " ",
"strip-comments": true,
"pre":[
"G21 ; set units to MM (required)",
"G90 ; absolute position mode (required)"
],
"post":[
"M30 ; program end"
],
"tool-change":[
"M6 T{tool} ; change tool to '{tool_name}'"
],
"dwell":[
"G4 P{time} ; dwell for {time}ms"
],
"settings": {
"bed_width": 400,
"bed_depth": 400
}
}

View file

@ -0,0 +1,22 @@
{
"file-ext": "nc",
"token-space": " ",
"strip-comments": true,
"pre":[
"G21 ; set units to MM (required)",
"G90 ; absolute position mode (required)"
],
"post":[
"M30 ; program end"
],
"tool-change":[
"M6 T{tool} ; change tool to '{tool_name}'"
],
"dwell":[
"G4 P{time} ; dwell for {time}ms"
],
"settings": {
"bed_width": 830,
"bed_depth": 400
}
}

View file

@ -0,0 +1,22 @@
{
"file-ext": "nc",
"token-space": " ",
"strip-comments": true,
"pre":[
"G21 ; set units to MM (required)",
"G90 ; absolute position mode (required)"
],
"post":[
"M30 ; program end"
],
"tool-change":[
"M6 T{tool} ; change tool to '{tool_name}'"
],
"dwell":[
"G4 P{time} ; dwell for {time}ms"
],
"settings": {
"bed_width": 830,
"bed_depth": 830
}
}

View file

@ -0,0 +1,24 @@
{
"file-ext": "nc",
"token-space": " ",
"strip-comments": true,
"pre":[
"G21 ; set units to MM (required)",
"G90 ; absolute position mode (required)"
],
"post":[
"M30 ; program end"
],
"tool-change":[
"M6 T{tool} ; change tool to '{tool_name}'"
],
"dwell":[
"G4 P{time} ; dwell for {time}ms"
],
"settings": {
"origin_center": false,
"spindle_max": 12000,
"bed_width": 290,
"bed_depth": 200
}
}

View file

@ -0,0 +1,22 @@
{
"file-ext": "nc",
"token-space": " ",
"strip-comments": true,
"pre":[
"G21 ; set units to MM (required)",
"G90 ; absolute position mode (required)"
],
"post":[
"M30 ; program end"
],
"tool-change":[
"M6 T{tool} ; change tool to '{tool_name}'"
],
"dwell":[
"G4 P{time} ; dwell for {time}ms"
],
"settings": {
"bed_width": 1000,
"bed_depth": 1000
}
}

View file

@ -0,0 +1,22 @@
{
"file-ext": "nc",
"token-space": " ",
"strip-comments": true,
"pre":[
"G21 ; set units to MM (required)",
"G90 ; absolute position mode (required)"
],
"post":[
"M30 ; program end"
],
"tool-change":[
"M6 T{tool} ; change tool to '{tool_name}'"
],
"dwell":[
"G4 P{time} ; dwell for {time}ms"
],
"settings": {
"bed_width": 500,
"bed_depth": 500
}
}

View file

@ -0,0 +1,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
}
}

View file

@ -0,0 +1,22 @@
{
"file-ext": "nc",
"token-space": " ",
"strip-comments": true,
"pre":[
"G21 ; set units to MM (required)",
"G90 ; absolute position mode (required)"
],
"post":[
"M30 ; program end"
],
"tool-change":[
"M6 T{tool} ; change tool to '{tool_name}'"
],
"dwell":[
"G4 P{time} ; dwell for {time}ms"
],
"settings": {
"bed_width": 235,
"bed_depth": 185
}
}

View file

@ -0,0 +1,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
}
}

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

View file

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

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

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

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

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

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

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

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

View 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
View 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
View file

@ -0,0 +1 @@
GridBot.One

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

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

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

View file

@ -0,0 +1,44 @@
{
"pre":[
"M73 P0 ; set progress to 0%",
"G90 ; set absolute positioning mode",
"M83 ; set relative positioning for extruder",
"M104 S{temp} 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
View 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
View 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
View file

@ -0,0 +1,37 @@
{
"pre":[
"G1 Z15.0 F12000 ; move the platform down 15mm",
"G1 X150 Y5 F12000 ; center extruder on X",
"M107 ; turn off filament cooling fan",
"M83 ; use relative positioning for extruder",
"M104 S{temp} ; set extruder temperature",
"G21 ; use metric values",
"G90 ; use absolute positioning mode",
"G28 ; move to endstops",
"G29 ; perform auto-levelling",
"M109 S{temp} ; wait for extruder to reach target temp",
"G1 X150 Y5 Z0.3 ; move the platform to purge extrusion",
"G92 E0 ; zero the extruded length",
"G1 F200 X250 E30 ; extrude 30mm of feed stock",
"G92 E0 ; zero the extruded length again",
"G1 X150 Y150 Z25 F12000 ; center head for print",
"G1 F12000 ; set head seek rate"
],
"post":[
"G1 E-1 F300 ; retract the filament to release pressure",
"G1 Z{z_max} E-5 F1200 ; drop bed, retract more filament",
"G28 X0 Y0 ; move X/Y to min endstops",
"M107 ; turn off filament cooling fan",
"M104 S0 ; turn off extruder",
"M84 ; disable stepper motors"
],
"cmd":{
"fan_power": "M106 S{fan_speed}"
},
"settings":{
"origin_center": false,
"bed_width": 305,
"bed_depth": 305,
"build_height": 305
}
}

View file

@ -0,0 +1,40 @@
{
"pre":[
"G1 Z15.0 F12000 ; move the platform down 15mm",
"G1 X150 Y5 F12000 ; center extruder on X",
"M107 ; turn off filament cooling fan",
"M83 ; use relative positioning for extruder",
"M104 S{temp} ; set extruder temperature",
"M140 S{bed_temp} ; set bed temperature",
"G21 ; use metric values",
"G90 ; use absolute positioning mode",
"G28 ; move to endstops",
"G29 ; perform auto-levelling",
"M109 S{temp} ; wait for extruder to reach target temp",
"M190 S{bed_temp} ; wait for bed to reach target temp",
"G1 X150 Y5 Z0.3 ; move the platform to purge extrusion",
"G92 E0 ; zero the extruded length",
"G1 F200 X250 E30 ; extrude 30mm of feed stock",
"G92 E0 ; zero the extruded length again",
"G1 X150 Y150 Z25 F12000 ; center head for print",
"G1 F12000 ; set head seek rate"
],
"post":[
"G1 E-1 F300 ; retract the filament to release pressure",
"G1 Z{z_max} E-5 F1200 ; drop bed, retract more filament",
"G28 X0 Y0 ; move X/Y to min endstops",
"M107 ; turn off filament cooling fan",
"M104 S0 ; turn off extruder",
"M140 S0 ; turn off bed",
"M84 ; disable stepper motors"
],
"cmd":{
"fan_power": "M106 S{fan_speed}"
},
"settings":{
"origin_center": false,
"bed_width": 305,
"bed_depth": 305,
"build_height": 305
}
}

45
src/dev/fdm/Ulio3D Normal file
View 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
}
}

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

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

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

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

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

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

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

View 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
View file

@ -0,0 +1,12 @@
{
"file-ext": "gcode",
"token-space": " ",
"pre":[],
"post":[],
"laser-on":[],
"laser-off":[],
"settings": {
"bed_width": 500,
"bed_depth": 300
}
}

View file

@ -0,0 +1,10 @@
{
"no_clone": true,
"cmd":{},
"settings":{
"bed_width": 120.96,
"bed_depth": 68.04,
"bed_height": 1,
"max_height": 150
}
}

View 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

File diff suppressed because it is too large Load diff

7629
src/ext/clip2.js Normal file

File diff suppressed because it is too large Load diff

1
src/ext/earcut.js Symbolic link
View file

@ -0,0 +1 @@
../../node_modules/earcut/src/earcut.js

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

@ -0,0 +1,270 @@
/* FileSaver.js
* A saveAs() FileSaver implementation.
* 1.1.20151003
*
* By Eli Grey, http://eligrey.com
* License: MIT
* See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
*/
/*global self */
/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
var saveAs = saveAs || (function(view) {
"use strict";
// IE <10 is explicitly unsupported
if (typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) {
return;
}
var
doc = view.document
// only get URL when necessary in case Blob.js hasn't overridden it yet
, get_URL = function() {
return view.URL || view.webkitURL || view;
}
, save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
, can_use_save_link = "download" in save_link
, click = function(node) {
var event = new MouseEvent("click");
node.dispatchEvent(event);
}
, is_safari = /Version\/[\d\.]+.*Safari/.test(navigator.userAgent)
, webkit_req_fs = view.webkitRequestFileSystem
, req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem
, throw_outside = function(ex) {
(view.setImmediate || view.setTimeout)(function() {
throw ex;
}, 0);
}
, force_saveable_type = "application/octet-stream"
, fs_min_size = 0
// See https://code.google.com/p/chromium/issues/detail?id=375297#c7 and
// https://github.com/eligrey/FileSaver.js/commit/485930a#commitcomment-8768047
// for the reasoning behind the timeout and revocation flow
, arbitrary_revoke_timeout = 500 // in ms
, revoke = function(file) {
var revoker = function() {
if (typeof file === "string") { // file is an object URL
get_URL().revokeObjectURL(file);
} else { // file is a File
file.remove();
}
};
if (view.chrome) {
revoker();
} else {
setTimeout(revoker, arbitrary_revoke_timeout);
}
}
, dispatch = function(filesaver, event_types, event) {
event_types = [].concat(event_types);
var i = event_types.length;
while (i--) {
var listener = filesaver["on" + event_types[i]];
if (typeof listener === "function") {
try {
listener.call(filesaver, event || filesaver);
} catch (ex) {
throw_outside(ex);
}
}
}
}
, auto_bom = function(blob) {
// prepend BOM for UTF-8 XML and text/* types (including HTML)
if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
return new Blob(["\ufeff", blob], {type: blob.type});
}
return blob;
}
, FileSaver = function(blob, name, no_auto_bom) {
if (!no_auto_bom) {
blob = auto_bom(blob);
}
// First try a.download, then web filesystem, then object URLs
var
filesaver = this
, type = blob.type
, blob_changed = false
, object_url
, target_view
, dispatch_all = function() {
dispatch(filesaver, "writestart progress write writeend".split(" "));
}
// on any filesys errors revert to saving with object URLs
, fs_error = function() {
if (target_view && is_safari && typeof FileReader !== "undefined") {
// Safari doesn't allow downloading of blob urls
var reader = new FileReader();
reader.onloadend = function() {
var base64Data = reader.result;
target_view.location.href = "data:attachment/file" + base64Data.slice(base64Data.search(/[,;]/));
filesaver.readyState = filesaver.DONE;
dispatch_all();
};
reader.readAsDataURL(blob);
filesaver.readyState = filesaver.INIT;
return;
}
// don't create more object URLs than needed
if (blob_changed || !object_url) {
object_url = get_URL().createObjectURL(blob);
}
if (target_view) {
target_view.location.href = object_url;
} else {
var new_tab = view.open(object_url, "_blank");
if (new_tab == undefined && is_safari) {
//Apple do not allow window.open, see http://bit.ly/1kZffRI
view.location.href = object_url
}
}
filesaver.readyState = filesaver.DONE;
dispatch_all();
revoke(object_url);
}
, abortable = function(func) {
return function() {
if (filesaver.readyState !== filesaver.DONE) {
return func.apply(this, arguments);
}
};
}
, create_if_not_found = {create: true, exclusive: false}
, slice
;
filesaver.readyState = filesaver.INIT;
if (!name) {
name = "download";
}
if (can_use_save_link) {
object_url = get_URL().createObjectURL(blob);
setTimeout(function() {
save_link.href = object_url;
save_link.download = name;
click(save_link);
dispatch_all();
revoke(object_url);
filesaver.readyState = filesaver.DONE;
});
return;
}
// Object and web filesystem URLs have a problem saving in Google Chrome when
// viewed in a tab, so I force save with application/octet-stream
// http://code.google.com/p/chromium/issues/detail?id=91158
// Update: Google errantly closed 91158, I submitted it again:
// https://code.google.com/p/chromium/issues/detail?id=389642
if (view.chrome && type && type !== force_saveable_type) {
slice = blob.slice || blob.webkitSlice;
blob = slice.call(blob, 0, blob.size, force_saveable_type);
blob_changed = true;
}
// Since I can't be sure that the guessed media type will trigger a download
// in WebKit, I append .download to the filename.
// https://bugs.webkit.org/show_bug.cgi?id=65440
if (webkit_req_fs && name !== "download") {
name += ".download";
}
if (type === force_saveable_type || webkit_req_fs) {
target_view = view;
}
if (!req_fs) {
fs_error();
return;
}
fs_min_size += blob.size;
req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) {
fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
var save = function() {
dir.getFile(name, create_if_not_found, abortable(function(file) {
file.createWriter(abortable(function(writer) {
writer.onwriteend = function(event) {
target_view.location.href = file.toURL();
filesaver.readyState = filesaver.DONE;
dispatch(filesaver, "writeend", event);
revoke(file);
};
writer.onerror = function() {
var error = writer.error;
if (error.code !== error.ABORT_ERR) {
fs_error();
}
};
"writestart progress write abort".split(" ").forEach(function(event) {
writer["on" + event] = filesaver["on" + event];
});
writer.write(blob);
filesaver.abort = function() {
writer.abort();
filesaver.readyState = filesaver.DONE;
};
filesaver.readyState = filesaver.WRITING;
}), fs_error);
}), fs_error);
};
dir.getFile(name, {create: false}, abortable(function(file) {
// delete file if it already exists
file.remove();
save();
}), abortable(function(ex) {
if (ex.code === ex.NOT_FOUND_ERR) {
save();
} else {
fs_error();
}
}));
}), fs_error);
}), fs_error);
}
, FS_proto = FileSaver.prototype
, saveAs = function(blob, name, no_auto_bom) {
return new FileSaver(blob, name, no_auto_bom);
}
;
// IE 10+ (native saveAs)
if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
return function(blob, name, no_auto_bom) {
if (!no_auto_bom) {
blob = auto_bom(blob);
}
return navigator.msSaveOrOpenBlob(blob, name || "download");
};
}
FS_proto.abort = function() {
var filesaver = this;
filesaver.readyState = filesaver.DONE;
dispatch(filesaver, "abort");
};
FS_proto.readyState = FS_proto.INIT = 0;
FS_proto.WRITING = 1;
FS_proto.DONE = 2;
FS_proto.error =
FS_proto.onwritestart =
FS_proto.onprogress =
FS_proto.onwrite =
FS_proto.onabort =
FS_proto.onerror =
FS_proto.onwriteend =
null;
return saveAs;
}(
typeof self !== "undefined" && self
|| typeof window !== "undefined" && window
|| this.content
));
// `self` is undefined in Firefox for Android content script context
// while `this` is nsIContentFrameMessageManager
// with an attribute `content` that corresponds to the window
if (typeof module !== "undefined" && module.exports) {
module.exports.saveAs = saveAs;
} else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) {
define([], function() {
return saveAs;
});
}

16433
src/ext/pngjs.js Normal file

File diff suppressed because it is too large Load diff

1
src/ext/three.js Symbolic link
View file

@ -0,0 +1 @@
../../node_modules/three/build/three.min.js

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

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

397
src/geo/base.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

890
src/geo/polygons.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

1
src/kiri/lang-en.js Symbolic link
View file

@ -0,0 +1 @@
../../web/kiri/lang/en.js

58
src/kiri/lang.js Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

1157
src/kiri/print.js Normal file

File diff suppressed because it is too large Load diff

1402
src/kiri/slice.js Normal file

File diff suppressed because it is too large Load diff

899
src/kiri/slicer.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

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

File diff suppressed because it is too large Load diff

465
src/mode/laser/driver.js Normal file
View 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

File diff suppressed because it is too large Load diff

84
src/moto/ajax.js Normal file
View 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
View 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