merge development branch
This commit is contained in:
commit
9b0fa9dedf
128 changed files with 3943 additions and 4261 deletions
236
src/kiri/codec.js
Normal file
236
src/kiri/codec.js
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.kiri.codec) return;
|
||||
|
||||
const BASE = self.base, KIRI = self.kiri, handlers = {};
|
||||
|
||||
KIRI.codec = {
|
||||
encode: encode,
|
||||
decode: decode,
|
||||
registerDecoder: registerDecoder
|
||||
};
|
||||
|
||||
function encode(o, state) {
|
||||
state = state || {};
|
||||
if (o === null) return null;
|
||||
if (o === undefined) return undefined;
|
||||
if (Array.isArray(o)) {
|
||||
let arr = new Array(o.length), i=0;
|
||||
while (i < o.length) arr[i] = encode(o[i++], state);
|
||||
return arr;
|
||||
}
|
||||
switch (typeof(o)) {
|
||||
case 'string':
|
||||
case 'number':
|
||||
return o;
|
||||
case 'object':
|
||||
if (o.encode) return o.encode(state);
|
||||
return genOEncode(o, state);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function decode(o, state) {
|
||||
state = state || {};
|
||||
if (o === null) return null;
|
||||
if (o === undefined) return undefined;
|
||||
if (Array.isArray(o)) {
|
||||
for (let i=0; i < o.length; i++) o[i] = decode(o[i],state);
|
||||
return o;
|
||||
}
|
||||
switch (typeof(o)) {
|
||||
case 'string':
|
||||
case 'number':
|
||||
return o;
|
||||
case 'object':
|
||||
if (o.type && handlers[o.type]) return handlers[o.type](o,state);
|
||||
return genODecode(o, state);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function registerDecoder(type, handler) {
|
||||
handlers[type] = handler;
|
||||
}
|
||||
|
||||
function genOEncode(o, state) {
|
||||
if (o instanceof Float32Array) return o;
|
||||
let out = {};
|
||||
for (let k in o) {
|
||||
if (o.hasOwnProperty(k)) out[k] = encode(o[k], state);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function genODecode(o, state) {
|
||||
if (o instanceof Float32Array) return o;
|
||||
let out = {};
|
||||
for (let k in o) {
|
||||
if (o.hasOwnProperty(k)) out[k] = decode(o[k], state);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** ******************************************************************
|
||||
* Object CODEC Functions
|
||||
******************************************************************* */
|
||||
|
||||
KIRI.Slice.prototype.encode = function(state) {
|
||||
return {
|
||||
type: 'slice',
|
||||
z: this.z,
|
||||
index: this.index,
|
||||
camMode: this.camMode,
|
||||
tops: encode(this.tops, state),
|
||||
bridges: encode(this.bridges, state),
|
||||
flats: encode(this.flats, state),
|
||||
solids: encode(this.solids, state),
|
||||
supports: encode(this.supports, state)
|
||||
//groups: encode(this.groups, state)
|
||||
};
|
||||
};
|
||||
|
||||
registerDecoder('slice', function(v, state) {
|
||||
let slice = KIRI.newSlice(v.z, state.mesh ? state.mesh.newGroup() : null);
|
||||
|
||||
slice.index = v.index;
|
||||
slice.camMode = v.camMode;
|
||||
slice.tops = decode(v.tops, state);
|
||||
slice.bridges = decode(v.bridges, state);
|
||||
slice.flats = decode(v.flats, state);
|
||||
slice.solids = decode(v.solids, state);
|
||||
slice.supports = decode(v.supports, state);
|
||||
//slice.groups = decode(v.groups, state);
|
||||
|
||||
return slice;
|
||||
});
|
||||
|
||||
KIRI.Top.prototype.encode = function(state) {
|
||||
return {
|
||||
type: 'top',
|
||||
poly: encode(this.poly, state),
|
||||
traces: encode(this.traces, state),
|
||||
inner: encode(this.inner, state),
|
||||
// thinner: encode(this.thinner, state),
|
||||
solids: encode(this.solids, state),
|
||||
fill_lines: encodePointArray(this.fill_lines, state),
|
||||
fill_sparse: encode(this.fill_sparse, state),
|
||||
polish: encode(this.polish, state)
|
||||
};
|
||||
};
|
||||
|
||||
registerDecoder('top', function(v, state) {
|
||||
let top = KIRI.newTop(decode(v.poly, state));
|
||||
|
||||
top.traces = decode(v.traces, state);
|
||||
top.inner = decode(v.inner, state);
|
||||
// top.thinner = decode(v.thinner, state);
|
||||
top.solids = decode(v.solids, state);
|
||||
top.fill_lines = decodePointArray(v.fill_lines, state);
|
||||
top.fill_sparse = decode(v.fill_sparse, state);
|
||||
top.polish = decode(v.polish, state);
|
||||
|
||||
return top;
|
||||
});
|
||||
|
||||
function encodePointArray(points, state) {
|
||||
if (!points) return null;
|
||||
|
||||
let array = new Float32Array(points.length * 3),
|
||||
pos = 0;
|
||||
|
||||
points.forEach(function(point) {
|
||||
if (state.rotate) {
|
||||
if (state.centerz) {
|
||||
point.z -= state.centerz;
|
||||
}
|
||||
let v = new THREE.Vector3(point.x, point.y, point.z);
|
||||
v.applyMatrix4(state.rotate);
|
||||
point = {x: v.x, y: v.y, z: v.z};
|
||||
if (state.centerz) {
|
||||
point.z += state.centerz;
|
||||
}
|
||||
if (state.movez) {
|
||||
point.z -= state.movez;
|
||||
}
|
||||
}
|
||||
array[pos++] = point.x;
|
||||
array[pos++] = point.y;
|
||||
array[pos++] = point.z;
|
||||
});
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
function decodePointArray(array) {
|
||||
if (!array) return null;
|
||||
|
||||
let vid = 0,
|
||||
pid = 0,
|
||||
points = new Array(array.length/3);
|
||||
|
||||
while (vid < array.length) {
|
||||
points[pid++] = BASE.newPoint(array[vid++], array[vid++], array[vid++]);
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
BASE.Polygon.prototype.encode = function(state) {
|
||||
if (!state.poly) state.poly = {};
|
||||
|
||||
let cached = state.poly[this.id];
|
||||
|
||||
if (cached) {
|
||||
return {
|
||||
type: 'poly',
|
||||
ref: this.id
|
||||
};
|
||||
}
|
||||
|
||||
state.poly[this.id] = this;
|
||||
|
||||
return {
|
||||
type: 'poly',
|
||||
id: this.id,
|
||||
array: encodePointArray(this.points, state),
|
||||
open: this.isOpen(),
|
||||
inner: encode(this.inner, state),
|
||||
parent: encode(this.parent, state),
|
||||
fills: encodePointArray(this.fills, state),
|
||||
depth: this.depth
|
||||
};
|
||||
};
|
||||
|
||||
registerDecoder('poly', function(v, state) {
|
||||
if (!state.poly) state.poly = {};
|
||||
|
||||
if (v.ref) return state.poly[v.ref];
|
||||
|
||||
let poly = BASE.newPolygon(),
|
||||
vid = 0;
|
||||
|
||||
while (vid < v.array.length) {
|
||||
poly.push(BASE.newPoint(v.array[vid++], v.array[vid++], v.array[vid++]));
|
||||
}
|
||||
|
||||
poly.id = v.id;
|
||||
poly.open = v.open;
|
||||
// if (v.open) poly.setOpen(); else poly.setClosed();
|
||||
|
||||
state.poly[v.id] = poly;
|
||||
|
||||
poly.inner = decode(v.inner, state);
|
||||
poly.parent = decode(v.parent, state);
|
||||
poly.fills = decodePointArray(v.fills);
|
||||
poly.depth = v.depth;
|
||||
|
||||
return poly;
|
||||
});
|
||||
|
||||
|
||||
})();
|
||||
575
src/kiri/conf.js
Normal file
575
src/kiri/conf.js
Normal file
|
|
@ -0,0 +1,575 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.kiri.conf) return;
|
||||
|
||||
const KIRI = self.kiri, CVER = 185, clone = Object.clone;
|
||||
|
||||
function genID() {
|
||||
while (true) {
|
||||
let k = Math.round(Math.random() * 9999999999).toString(36);
|
||||
if (k.length >= 4 && k.length <= 8) return k;
|
||||
}
|
||||
}
|
||||
|
||||
// add fields to o(bject) from d(efault) that are missing
|
||||
// remove fields from o(bject) that don't exist in d(efault)
|
||||
function fill_cull_once(obj, def) {
|
||||
if (!obj) return;
|
||||
// add missing
|
||||
for (let k in def) {
|
||||
if (def.hasOwnProperty(k)) {
|
||||
let okv = obj[k];
|
||||
if ((okv === undefined || okv === null)) {
|
||||
// console.log({fill: k});
|
||||
obj[k] = def[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
// remove invalid
|
||||
for (let k in obj) {
|
||||
if (!def.hasOwnProperty(k)) {
|
||||
// console.log({cull: k});
|
||||
delete obj[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fill_cull_many(map, def) {
|
||||
forValues(map, (obj) => { fill_cull_once(obj, def) });
|
||||
}
|
||||
|
||||
function objectMap(o, fn) {
|
||||
for (let [key, val] of Object.entries(o)) {
|
||||
o[key] = fn(val) || val;
|
||||
}
|
||||
}
|
||||
|
||||
function valueOf(val, dv) {
|
||||
return typeof(val) !== 'undefined' ? val : dv;
|
||||
}
|
||||
|
||||
function forValues(o, fn) {
|
||||
Object.values(o).forEach(v => fn(v));
|
||||
}
|
||||
|
||||
function device_v1_to_v2(device) {
|
||||
if (device && device.filamentSize) {
|
||||
device.extruders = [{
|
||||
extFilament: device.filamentSize,
|
||||
extNozzle: device.nozzleSize,
|
||||
extSelect: ["T0"],
|
||||
extOffsetX: 0,
|
||||
extOffsetY: 0
|
||||
}];
|
||||
delete device.filamentSize;
|
||||
delete device.nozzleSize;
|
||||
}
|
||||
}
|
||||
|
||||
// convert default filter (from server) into device structure
|
||||
function device_from_code(code,mode) {
|
||||
// presence of internal field indicates already converted
|
||||
if (code.internal >= 0) return code;
|
||||
|
||||
let API = KIRI.api,
|
||||
cmd = code.cmd || {},
|
||||
set = code.settings || {},
|
||||
ext = code.extruders;
|
||||
|
||||
// currently causes unecessary fills and culls because
|
||||
// it's not mode and device type sensitive
|
||||
let device = {
|
||||
noclone: valueOf(code.no_clone, false),
|
||||
mode: mode || code.mode || '',
|
||||
internal: 0,
|
||||
bedHeight: valueOf(set.bed_height, 2.5),
|
||||
bedWidth: valueOf(set.bed_width, 300),
|
||||
bedDepth: valueOf(set.bed_depth, 175),
|
||||
bedRound: valueOf(set.bed_circle, false),
|
||||
maxHeight: valueOf(set.build_height, 150),
|
||||
originCenter: valueOf(set.origin_center, false),
|
||||
extrudeAbs: valueOf(set.extrude_abs, false),
|
||||
spindleMax: valueOf(set.spindle_max, 0),
|
||||
gcodeFan: valueOf(cmd.fan_power, ''),
|
||||
gcodeTrack: valueOf(cmd.progress, ''),
|
||||
gcodeLayer: valueOf(cmd.layer, []),
|
||||
gcodePre: valueOf(code.pre, []),
|
||||
gcodePost: valueOf(code.post, []),
|
||||
gcodeProc: valueOf(code.proc, ''),
|
||||
gcodePause: valueOf(code.pause, []),
|
||||
gcodeDwell: valueOf(code.dwell, []),
|
||||
gcodeSpindle: valueOf(cmd.spindle, []),
|
||||
gcodeChange: valueOf(code['tool-change'], []),
|
||||
gcodeFExt: valueOf(code['file-ext'], 'gcode'),
|
||||
gcodeSpace: valueOf(code['token-space'], true),
|
||||
gcodeStrip: valueOf(code['strip-comments'], false),
|
||||
gcodeLaserOn: valueOf(code['laser-on'], []),
|
||||
gcodeLaserOff: valueOf(code['laser-off'], []),
|
||||
extruders: []
|
||||
};
|
||||
|
||||
if (ext) {
|
||||
// synthesize extruders from new style settings
|
||||
ext.forEach(rec => {
|
||||
let e = API.clone(CONF.defaults.fdm.d.extruders[0]);
|
||||
if (rec.nozzle) e.extNozzle = rec.nozzle;
|
||||
if (rec.filament) e.extFilament = rec.filament;
|
||||
if (rec.offset_x) e.extOffsetX = rec.offset_x;
|
||||
if (rec.offset_y) e.extOffsetY = rec.offset_y;
|
||||
if (rec.select) e.extSelect = rec.select;
|
||||
device.extruders.push(e);
|
||||
});
|
||||
} else {
|
||||
// synthesize extruders from old style settings
|
||||
device.extruders = [API.clone(CONF.defaults.fdm.d.extruders[0])];
|
||||
device.extruders[0].extNozzle = valueOf(set.nozzle_size, 0.4);
|
||||
device.extruders[0].extFilament = valueOf(set.filament_diameter, 1.75);
|
||||
}
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
// ensure settings structure is up-to-date
|
||||
function normalize(settings) {
|
||||
let API = KIRI.api,
|
||||
defaults = CONF.defaults,
|
||||
template = CONF.template,
|
||||
mode = settings.mode.toLowerCase(),
|
||||
default_dev = defaults[mode].d,
|
||||
default_pro = defaults[mode].p;
|
||||
|
||||
// v1 to v2 changed FDM extruder / nozzle / filament structure
|
||||
if (settings.ver != CVER) {
|
||||
// backup settings before upgrade
|
||||
API.sdb.setItem(`ws-settings-${Date.now()}`, JSON.stringify(settings));
|
||||
device_v1_to_v2(settings.device);
|
||||
device_v1_to_v2(settings.cdev.FDM);
|
||||
objectMap(settings.devices, dev => {
|
||||
return dev ? device_from_code(dev) : dev;
|
||||
});
|
||||
settings.ver = CVER;
|
||||
}
|
||||
|
||||
fill_cull_once(settings, template);
|
||||
fill_cull_once(settings.device, default_dev);
|
||||
fill_cull_once(settings.process, default_pro);
|
||||
fill_cull_once(settings.cdev, template.cdev);
|
||||
fill_cull_once(settings.cproc, template.cproc);
|
||||
fill_cull_once(settings.sproc, template.sproc);
|
||||
fill_cull_once(settings.defaults, template.defaults);
|
||||
fill_cull_once(settings.cdev.FDM, defaults.fdm.d);
|
||||
fill_cull_once(settings.cdev.SLA, defaults.sla.d);
|
||||
fill_cull_once(settings.cdev.CAM, defaults.cam.d);
|
||||
fill_cull_once(settings.cdev.LASER, defaults.laser.d);
|
||||
fill_cull_many(settings.sproc.FDM, defaults.fdm.p);
|
||||
fill_cull_many(settings.sproc.SLA, defaults.sla.p);
|
||||
fill_cull_many(settings.sproc.CAM, defaults.cam.p);
|
||||
fill_cull_many(settings.sproc.LASER, defaults.laser.p);
|
||||
fill_cull_once(settings.controller, template.controller);
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
let CONF = KIRI.conf = {
|
||||
// --------------- helper functions
|
||||
normalize: normalize,
|
||||
device_from_code: device_from_code,
|
||||
// ---------------
|
||||
MODES: {
|
||||
FDM: 1, // fused deposition modeling (also FFF)
|
||||
LASER: 2, // laser cutters
|
||||
CAM: 3, // 3 axis milling/machining
|
||||
SLA: 4 // cured resin printers
|
||||
},
|
||||
VIEWS: {
|
||||
ARRANGE: 1,
|
||||
SLICE: 2,
|
||||
PREVIEW: 3
|
||||
},
|
||||
// --------------- device and process defaults
|
||||
defaults: {
|
||||
fdm:{
|
||||
// device defaults FDM:Device
|
||||
d:{
|
||||
new: true,
|
||||
mode: "",
|
||||
internal: 0,
|
||||
bedWidth: 300,
|
||||
bedDepth: 175,
|
||||
bedHeight: 2.5,
|
||||
bedRound: false,
|
||||
originCenter: false,
|
||||
maxHeight: 150,
|
||||
gcodePre: [],
|
||||
gcodePost: [],
|
||||
gcodePause: [],
|
||||
gcodeProc: "",
|
||||
gcodeFan: "",
|
||||
gcodeTrack: "",
|
||||
gcodeLayer: [],
|
||||
gcodeFExt: "",
|
||||
extruders:[{
|
||||
extFilament: 1.75,
|
||||
extNozzle: 0.4,
|
||||
extSelect: ["T0"],
|
||||
extOffsetX: 0,
|
||||
extOffsetY: 0
|
||||
}]
|
||||
},
|
||||
// process defaults FDM:Process
|
||||
p:{
|
||||
processName: "default",
|
||||
sliceHeight: 0.25,
|
||||
sliceShells: 3,
|
||||
sliceFillAngle: 45,
|
||||
sliceFillOverlap: 0.3,
|
||||
sliceFillSparse: 0.5,
|
||||
sliceFillType: "hex",
|
||||
sliceSupportEnable: false,
|
||||
sliceSupportDensity: 0.25,
|
||||
sliceSupportOffset: 1.0,
|
||||
sliceSupportGap: 1,
|
||||
sliceSupportSize: 10,
|
||||
sliceSupportArea: 1,
|
||||
sliceSupportExtra: 0,
|
||||
sliceSupportSpan: 6,
|
||||
sliceSupportNozzle: 0,
|
||||
sliceSolidMinArea: 1,
|
||||
sliceSolidLayers: 3,
|
||||
sliceBottomLayers: 3,
|
||||
sliceTopLayers: 3,
|
||||
firstSliceHeight: 0.25,
|
||||
firstLayerRate: 30,
|
||||
firstLayerFillRate: 35,
|
||||
firstLayerPrintMult: 1.0,
|
||||
firstLayerNozzleTemp: 200,
|
||||
firstLayerBedTemp: 60,
|
||||
outputRaft: false,
|
||||
outputRaftSpacing: 0.2,
|
||||
outputTemp: 200,
|
||||
outputFanMax: 255,
|
||||
outputBedTemp: 60,
|
||||
outputFeedrate: 50,
|
||||
outputFinishrate: 50,
|
||||
outputSeekrate: 80,
|
||||
outputShellMult: 1.25,
|
||||
outputFillMult: 1.25,
|
||||
outputSparseMult: 1.25,
|
||||
outputFanLayer: 1,
|
||||
outputRetractDist: 1.5,
|
||||
outputRetractSpeed: 40,
|
||||
outputRetractDwell: 20,
|
||||
outputBrimCount: 2,
|
||||
outputBrimOffset: 2,
|
||||
outputShortPoly: 100.0,
|
||||
outputMinSpeed: 10.0,
|
||||
outputCoastDist: 0,
|
||||
outputWipeDistance: 0,
|
||||
outputLayerRetract: false,
|
||||
outputOriginCenter: true,
|
||||
outputInvertX: false,
|
||||
outputInvertY: false,
|
||||
detectThinWalls: false,
|
||||
sliceMinHeight: 0,
|
||||
zHopDistance: 0.2,
|
||||
antiBacklash: 1,
|
||||
gcodePause: "",
|
||||
sliceRotation: 0
|
||||
}
|
||||
},
|
||||
sla:{
|
||||
// device defaults SLA:Device
|
||||
d:{
|
||||
new: true,
|
||||
deviceName: "",
|
||||
noclone: false,
|
||||
mode: "",
|
||||
internal: 0,
|
||||
bedWidth: 150,
|
||||
bedDepth: 150,
|
||||
bedHeight: 1.5,
|
||||
maxHeight: 150
|
||||
},
|
||||
// process defaults SLA:Process
|
||||
p:{
|
||||
processName: "default",
|
||||
slaSlice: 0.05,
|
||||
slaShell: 0.00,
|
||||
slaOpenTop: false,
|
||||
slaOpenBase: false,
|
||||
slaAntiAlias: 1,
|
||||
slaLayerOff: 0.1,
|
||||
slaLayerOn: 7,
|
||||
slaPeelDist: 6,
|
||||
slaPeelLiftRate: 1.5,
|
||||
slaPeelDropRate: 3,
|
||||
slaBaseLayers: 5,
|
||||
slaBaseOff: 0.1,
|
||||
slaBaseOn: 30,
|
||||
slaBasePeelDist: 6,
|
||||
slaBasePeelLiftRate: 1.5,
|
||||
slaFillDensity: 0,
|
||||
slaFillLine: 0.5,
|
||||
slaFirstOffset: 0,
|
||||
slaSupportLayers: 10,
|
||||
slaSupportDensity: 0.5,
|
||||
slaSupportSize: 0.6,
|
||||
slaSupportPoints: 4,
|
||||
slaSupportGap: 10,
|
||||
slaSupportEnable: false
|
||||
}
|
||||
},
|
||||
cam:{
|
||||
// device defaults CAM:Device
|
||||
d:{
|
||||
new: true,
|
||||
mode: "",
|
||||
internal: 0,
|
||||
bedWidth: 300,
|
||||
bedDepth: 175,
|
||||
bedHeight: 2.5,
|
||||
originCenter: false,
|
||||
spindleMax: 0,
|
||||
gcodePre: [],
|
||||
gcodePost: [],
|
||||
gcodeFExt: "",
|
||||
gcodeSpace: true,
|
||||
gcodeStrip: true,
|
||||
gcodeDwell: ["G4 P{time}"],
|
||||
gcodeChange: ["M6 T{tool}"],
|
||||
gcodeSpindle: ["M3 S{speed}"]
|
||||
},
|
||||
// process defaults CAM:Process
|
||||
p:{
|
||||
processName: "default",
|
||||
camFastFeed: 6000,
|
||||
camFastFeedZ: 300,
|
||||
roughingTool: 1000,
|
||||
roughingSpindle: 1000,
|
||||
roughingDown: 2,
|
||||
roughingOver: 0.5,
|
||||
roughingSpeed: 1000,
|
||||
roughingPlunge: 250,
|
||||
roughingStock: 0,
|
||||
roughingPocket: true,
|
||||
roughingOn: true,
|
||||
finishingTool: 1000,
|
||||
finishingSpindle: 1000,
|
||||
finishingDown: 3,
|
||||
finishingOver: 0.5,
|
||||
finishingAngle: 85,
|
||||
finishingSpeed: 800,
|
||||
finishingPlunge: 250,
|
||||
finishingOn: true,
|
||||
finishingXOn: true,
|
||||
finishingYOn: true,
|
||||
finishCurvesOnly: false,
|
||||
drillTool: 1000,
|
||||
drillSpindle: 1000,
|
||||
drillDownSpeed: 250,
|
||||
drillDown: 5,
|
||||
drillDwell: 250,
|
||||
drillLift: 2,
|
||||
drillingOn: false,
|
||||
camTabsAngle: 0,
|
||||
camTabsCount: 4,
|
||||
camTabsWidth: 5,
|
||||
camTabsHeight: 5,
|
||||
camTabsOn: false,
|
||||
camPocketOnlyRough: false,
|
||||
camPocketOnlyFinish: false,
|
||||
camDepthFirst: true,
|
||||
camEaseDown: false,
|
||||
camOriginTop: true,
|
||||
camTolerance: 0.15,
|
||||
camZTopOffset: 0,
|
||||
camZBottom: 0,
|
||||
camZClearance: 1,
|
||||
camStockX: 0,
|
||||
camStockY: 0,
|
||||
camStockZ: 0,
|
||||
camStockOffset: true,
|
||||
outputClockwise: false,
|
||||
outputOriginCenter: true,
|
||||
outputInvertX: false,
|
||||
outputInvertY: false
|
||||
}
|
||||
},
|
||||
laser: {
|
||||
// device defaults Laser:Device
|
||||
d:{
|
||||
new: true,
|
||||
mode: "",
|
||||
internal: 0,
|
||||
bedWidth: 300,
|
||||
bedDepth: 175,
|
||||
bedHeight: 2.5,
|
||||
gcodePre: [],
|
||||
gcodePost: [],
|
||||
gcodeFExt: "",
|
||||
gcodeSpace: true,
|
||||
gcodeLaserOn: ["M106 S{power}"],
|
||||
gcodeLaserOff: ["M107"]
|
||||
},
|
||||
// process defaults Laser:Process
|
||||
p:{
|
||||
processName: "default",
|
||||
laserOffset: 0.25,
|
||||
laserSliceHeight: 1,
|
||||
laserSliceSingle: false,
|
||||
outputTileSpacing: 1,
|
||||
outputTileScaling: 1,
|
||||
outputLaserPower: 100,
|
||||
outputLaserSpeed: 1000,
|
||||
outputLaserGroup: true,
|
||||
outputLaserMerged: false,
|
||||
outputOriginCenter: true,
|
||||
outputInvertX: false,
|
||||
outputInvertY: false
|
||||
}
|
||||
}
|
||||
},
|
||||
// --------------- settings template
|
||||
template: {
|
||||
// CAM only
|
||||
bounds: {},
|
||||
origin: {},
|
||||
stock: {},
|
||||
tools:[
|
||||
{
|
||||
id: 1000,
|
||||
number: 1,
|
||||
type: "endmill",
|
||||
name: "end 1/4",
|
||||
metric: false,
|
||||
shaft_diam: 0.25,
|
||||
shaft_len: 1,
|
||||
flute_diam: 0.25,
|
||||
flute_len: 2,
|
||||
// taper_angle: 70,
|
||||
taper_tip: 0,
|
||||
},
|
||||
{
|
||||
id: 1001,
|
||||
number: 2,
|
||||
type: "endmill",
|
||||
name: "end 1/8",
|
||||
metric: false,
|
||||
shaft_diam: 0.125,
|
||||
shaft_len: 1,
|
||||
flute_diam: 0.125,
|
||||
flute_len: 1.5,
|
||||
// taper_angle: 70,
|
||||
taper_tip: 0,
|
||||
},
|
||||
{
|
||||
id: 1002,
|
||||
number: 3,
|
||||
type: "endmill",
|
||||
name: "end 1/16",
|
||||
metric: false,
|
||||
shaft_diam: 0.0625,
|
||||
shaft_len: 1,
|
||||
flute_diam: 0.0625,
|
||||
flute_len: 1.5,
|
||||
// taper_angle: 70,
|
||||
taper_tip: 0,
|
||||
}
|
||||
],
|
||||
// currently selected device
|
||||
device:{},
|
||||
// currently selected process
|
||||
process:{},
|
||||
// current process name by mode
|
||||
cproc:{
|
||||
FDM: "default",
|
||||
SLA: "default",
|
||||
CAM: "default",
|
||||
LASER: "default"
|
||||
},
|
||||
// stored processes by mode
|
||||
sproc:{
|
||||
FDM: {},
|
||||
SLA: {},
|
||||
CAM: {},
|
||||
LASER: {}
|
||||
},
|
||||
// current device name by mode
|
||||
filter:{
|
||||
FDM: "Any.Generic.Marlin",
|
||||
SLA: "Anycubic.Photon",
|
||||
CAM: "Any.Generic.Grbl",
|
||||
LASER: "Any.Generic.Laser"
|
||||
},
|
||||
// stored device by mode
|
||||
cdev: {
|
||||
FDM: null,
|
||||
SLA: null,
|
||||
CAM: null,
|
||||
LASER: null
|
||||
},
|
||||
// custom devices by name (all modes)
|
||||
devices:{},
|
||||
// favorited devices (all modes)
|
||||
favorites:{},
|
||||
// map of device to last process setting (name)
|
||||
devproc: {},
|
||||
layers:{
|
||||
layerOutline: true,
|
||||
layerTrace: true,
|
||||
layerFacing: true,
|
||||
layerRough: true,
|
||||
layerFinish: true,
|
||||
layerFinishX: true,
|
||||
layerFinishY: true,
|
||||
layerDelta: false,
|
||||
layerSolid: false,
|
||||
layerSparse: true,
|
||||
layerFill: true,
|
||||
layerSupport: true,
|
||||
layerPrint: false,
|
||||
layerMoves: false
|
||||
},
|
||||
controller:{
|
||||
view: null,
|
||||
dark: false,
|
||||
expert: false,
|
||||
zoomSpeed: 1.0,
|
||||
thinRender: false,
|
||||
reverseZoom: true,
|
||||
showOrigin: false,
|
||||
freeLayout: true,
|
||||
autoLayout: true,
|
||||
alignTop: true,
|
||||
units: "mm",
|
||||
exportOcto: true,
|
||||
exportGhost: false,
|
||||
exportLocal: false
|
||||
},
|
||||
// for passing temporary slice hints (topo currently)
|
||||
synth: {},
|
||||
// widget extra info for slicing (extruder mapping)
|
||||
widget: {},
|
||||
mode: 'FDM',
|
||||
id: genID(),
|
||||
ver: CVER
|
||||
}
|
||||
};
|
||||
|
||||
let settings = CONF.template;
|
||||
|
||||
// seed defaults. will get culled on save
|
||||
settings.sproc.FDM.default = clone(settings.process);
|
||||
settings.sproc.SLA.default = clone(settings.process);
|
||||
settings.sproc.CAM.default = clone(settings.process);
|
||||
settings.sproc.LASER.default = clone(settings.process);
|
||||
settings.cdev.FDM = clone(settings.device);
|
||||
settings.cdev.SLA = clone(settings.device);
|
||||
settings.cdev.CAM = clone(settings.device);
|
||||
settings.cdev.LASER = clone(settings.device);
|
||||
|
||||
})();
|
||||
190
src/kiri/db.js
Normal file
190
src/kiri/db.js
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
const KIRI = self.kiri, DP = Catalog.prototype;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
function Catalog(motodb, decimate) {
|
||||
let store = this;
|
||||
store.db = motodb;
|
||||
store.files = {};
|
||||
store.listeners = [];
|
||||
store.autodec = decimate;
|
||||
store.deferredHandler = null;
|
||||
store.refresh();
|
||||
}
|
||||
|
||||
KIRI.openCatalog = function(motodb,decimate) {
|
||||
return new Catalog(motodb,decimate);
|
||||
};
|
||||
|
||||
DP.refresh = function() {
|
||||
let store = this;
|
||||
store.db.get('files', function(files) {
|
||||
if (files) {
|
||||
store.files = files;
|
||||
notifyFileListeners(store);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
DP.wipe = function() {
|
||||
let key, files = this.files;
|
||||
for (key in files) {
|
||||
if (files.hasOwnProperty(key)) this.deleteFile(key);
|
||||
}
|
||||
};
|
||||
|
||||
DP.fileList = function() {
|
||||
return this.files;
|
||||
};
|
||||
|
||||
DP.addFileListener = function(listener) {
|
||||
if (!this.listeners.contains(listener)) {
|
||||
this.listeners.push(listener);
|
||||
listener(this.files);
|
||||
}
|
||||
};
|
||||
|
||||
DP.removeFileListener = function(listener) {
|
||||
this.listeners.remove(listener);
|
||||
};
|
||||
|
||||
function saveFileList(store) {
|
||||
store.db.put('files', store.files);
|
||||
notifyFileListeners(store);
|
||||
}
|
||||
|
||||
function notifyFileListeners(store) {
|
||||
for (let i=0; i<store.listeners.length; i++) {
|
||||
store.listeners[i](store.files);
|
||||
}
|
||||
}
|
||||
|
||||
DP.decimate = function(vertices, callback) {
|
||||
if (vertices.length < 500000) return callback(vertices);
|
||||
KIRI.work.decimate(vertices, function(reply) {
|
||||
callback(reply);
|
||||
});
|
||||
};
|
||||
|
||||
DP.setDeferredHandler = function(handler) {
|
||||
this.deferredHandler = handler;
|
||||
};
|
||||
|
||||
DP.putDeferred = function(name, mark) {
|
||||
// triggers refresh callback
|
||||
this.files[name] = {
|
||||
deferred: mark
|
||||
};
|
||||
saveFileList(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {String} name
|
||||
* @param {Float32Array} vertices
|
||||
* @param {Function} [callback]
|
||||
*/
|
||||
DP.putFile = function(name, vertices, callback) {
|
||||
let store = this;
|
||||
store.db.put('file-'+name, vertices, function(ok) {
|
||||
if (ok) {
|
||||
store.files[name] = {
|
||||
vertices: vertices.length/3,
|
||||
updated: new Date().getTime()
|
||||
};
|
||||
saveFileList(store);
|
||||
if (store.autodec) {
|
||||
store.decimate(vertices, function(decimated) {
|
||||
store.db.put('fdec-'+name, decimated);
|
||||
if (callback) callback(decimated);
|
||||
});
|
||||
} else if (callback) callback(ok);
|
||||
} else if (callback) callback(ok);
|
||||
});
|
||||
};
|
||||
|
||||
DP.rename = function(name, newname, callback) {
|
||||
if (!this.files[name]) return callback({error: 'no such file'});
|
||||
if (!newname || newname == name) return callback({error: 'invalid new name'});
|
||||
let done = 0;
|
||||
let error = [];
|
||||
let store = this;
|
||||
function complete(ok, err) {
|
||||
if (err) error.push(err);
|
||||
if (++done === 2) {
|
||||
store.files[newname] = store.files[name];
|
||||
delete store.files[name];
|
||||
saveFileList(store);
|
||||
store.db.remove(`fdec-${name}`);
|
||||
store.db.remove(`file-${name}`);
|
||||
callback(error.length ? {error} : {});
|
||||
}
|
||||
}
|
||||
store.db.get(`fdec-${name}`, (vertices) => {
|
||||
if (!vertices) return complete(false, 'no decimation');
|
||||
store.db.put(`fdec-${newname}`, vertices, complete);
|
||||
});
|
||||
store.db.get(`file-${name}`, (vertices) => {
|
||||
if (!vertices) return complete(false, 'no raw file');
|
||||
store.db.put(`file-${newname}`, vertices, complete);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {String} name
|
||||
* @param {Function} callback
|
||||
*/
|
||||
DP.getFile = function(name, callback) {
|
||||
let store = this,
|
||||
rec = store.files[name];
|
||||
if (rec && rec.deferred) {
|
||||
if (store.deferredHandler) return store.deferredHandler(rec.deferred, name, callback);
|
||||
return callback();
|
||||
}
|
||||
if (!this.autodec) {
|
||||
store.db.get('file-'+name, callback);
|
||||
return;
|
||||
}
|
||||
this.db.get('fdec-'+name, function(vertices) {
|
||||
if (vertices) {
|
||||
callback(vertices);
|
||||
} else {
|
||||
store.db.get('file-'+name, function(vertices) {
|
||||
if (vertices) {
|
||||
store.decimate(vertices, function(decimated) {
|
||||
store.db.put('fdec-'+name, decimated);
|
||||
callback(vertices);
|
||||
});
|
||||
} else {
|
||||
return callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {String} name
|
||||
* @param {Function} callback
|
||||
*/
|
||||
DP.deleteFile = function(name, callback) {
|
||||
let store = this;
|
||||
if (store.files[name]) {
|
||||
delete store.files[name];
|
||||
store.db.remove('fdec-'+name);
|
||||
store.db.remove('file-'+name, function(ok) {
|
||||
saveFileList(store);
|
||||
if (callback) callback(ok);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (callback) callback(false);
|
||||
};
|
||||
|
||||
})();
|
||||
504
src/kiri/export.js
Normal file
504
src/kiri/export.js
Normal file
|
|
@ -0,0 +1,504 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.kiri.export) return;
|
||||
|
||||
const KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
API = KIRI.api,
|
||||
SDB = API.sdb,
|
||||
UI = API.ui,
|
||||
STATS = API.stats,
|
||||
MODES = API.const.MODES;
|
||||
|
||||
KIRI.export = exportPrint;
|
||||
|
||||
let printSeq = parseInt(SDB['kiri-print-seq'] || SDB['print-seq'] || "0") + 1;
|
||||
|
||||
function exportPrint() {
|
||||
let currentPrint = API.print.get();
|
||||
if (currentPrint) {
|
||||
API.event.emit('export', API.mode.get());
|
||||
switch (API.mode.get()) {
|
||||
case 'LASER': return exportPrintLaser(currentPrint);
|
||||
case 'FDM': return exportPrintGCODE(currentPrint);
|
||||
case 'CAM': return exportPrintGCODE(currentPrint);
|
||||
case 'SLA': return exportPrintSLA(currentPrint);
|
||||
}
|
||||
} else {
|
||||
API.function.print(exportPrint);
|
||||
}
|
||||
}
|
||||
|
||||
function exportPrintSLA(currentPrint) {
|
||||
if (currentPrint) {
|
||||
if (currentPrint.exported) {
|
||||
KIRI.driver.SLA.printDownload(currentPrint);
|
||||
return;
|
||||
}
|
||||
let lines = [];
|
||||
let times = {};
|
||||
let mark = Date.now();
|
||||
let seq = 1;
|
||||
let segment;
|
||||
currentPrint.export(true, function(line) {
|
||||
if (line.message) {
|
||||
if (segment && segment !== line.message) {
|
||||
let now = Date.now();
|
||||
times[`${seq++}_${segment}`] = now - mark;
|
||||
mark = now;
|
||||
}
|
||||
segment = line.message;
|
||||
}
|
||||
if (line.progress) {
|
||||
API.show.progress(line.progress, "exporting");
|
||||
}
|
||||
if (line.data) {
|
||||
lines.push(line.data);
|
||||
}
|
||||
}, function(done) {
|
||||
currentPrint.exported = true;
|
||||
times[`${seq++}_${segment}`] = Date.now() - mark;
|
||||
console.log(times);
|
||||
API.show.progress(0);
|
||||
currentPrint.sla = { lines, done, API };
|
||||
KIRI.driver.SLA.printDownload(currentPrint);
|
||||
});
|
||||
} else {
|
||||
API.function.print(exportPrintSLA);
|
||||
}
|
||||
}
|
||||
|
||||
function exportPrintGCODE(currentPrint) {
|
||||
if (currentPrint) {
|
||||
currentPrint.exportGCode(true, function(gcode) {
|
||||
exportGCode(gcode,currentPrint);
|
||||
});
|
||||
} else {
|
||||
API.function.print(exportPrint);
|
||||
}
|
||||
}
|
||||
|
||||
function exportPrintLaser(currentPrint) {
|
||||
if (!currentPrint) {
|
||||
return API.function.print(exportPrintLaser);
|
||||
}
|
||||
|
||||
let filename = "laser-"+(new Date().getTime().toString(36));
|
||||
|
||||
function download_svg() {
|
||||
saveAs(new Blob(
|
||||
[currentPrint.exportSVG()],
|
||||
{type:"application/octet-stream"}),
|
||||
$('print-filename').value + ".svg");
|
||||
}
|
||||
|
||||
function download_dxf() {
|
||||
saveAs(new Blob(
|
||||
[currentPrint.exportDXF()],
|
||||
{type:"application/octet-stream"}),
|
||||
$('print-filename').value + ".dxf");
|
||||
}
|
||||
|
||||
function download_gcode() {
|
||||
saveAs(new Blob(
|
||||
[currentPrint.exportLaserGCode()],
|
||||
{type:"application/octet-stream"}),
|
||||
$('print-filename').value + ".gcode");
|
||||
}
|
||||
|
||||
API.ajax("/kiri/output-laser.html", function(html) {
|
||||
let segments = 0;
|
||||
currentPrint.output.forEach(layer => { segments += layer.length });
|
||||
UI.print.innerHTML = html;
|
||||
$('print-filename').value = filename;
|
||||
$('print-lines').value = UTIL.comma(segments);
|
||||
$('print-svg').onclick = download_svg;
|
||||
$('print-dxf').onclick = download_dxf;
|
||||
$('print-lg').onclick = download_gcode;
|
||||
API.modal.show('print');
|
||||
});
|
||||
}
|
||||
|
||||
function exportGCode(gcode, currentPrint) {
|
||||
SDB['kiri-print-seq'] = printSeq++;
|
||||
|
||||
let settings = API.conf.get(),
|
||||
MODE = API.mode.get_id(),
|
||||
pre = (MODE === MODES.CAM ? "cnc-" : "print-") + (printSeq.toString().padStart(3,"0")),
|
||||
filename = pre,// + (new Date().getTime().toString(36)),
|
||||
fileext = settings.device.gcodeFExt || "gcode",
|
||||
codeproc = settings.device.gcodeProc,
|
||||
octo_host,
|
||||
octo_apik,
|
||||
grid_host,
|
||||
grid_apik,
|
||||
grid_target,
|
||||
grid_targets = {},
|
||||
grid_local,
|
||||
grid_uuid;
|
||||
|
||||
// run gcode post processor function (when supplied and valid)
|
||||
if (codeproc && self[codeproc]) {
|
||||
gcode = self[codeproc](gcode);
|
||||
}
|
||||
|
||||
function getBlob() {
|
||||
return new Blob(
|
||||
[gcode],
|
||||
{type:"application/octet-stream"});
|
||||
}
|
||||
|
||||
function sendto_octoprint() {
|
||||
if (!(octo_host && octo_apik)) return;
|
||||
|
||||
let form = new FormData(),
|
||||
ajax = new XMLHttpRequest(),
|
||||
host = octo_host.value.toLowerCase(),
|
||||
apik = octo_apik.value;
|
||||
|
||||
if (host.indexOf("http") !== 0) {
|
||||
API.show.alert("host missing protocol (http:// or https://)");
|
||||
return;
|
||||
}
|
||||
if (API.const.SECURE && !API.util.isSecure(host)) {
|
||||
API.show.alert("host must begin with 'https' on a secure site");
|
||||
return;
|
||||
}
|
||||
|
||||
SDB['octo-host'] = host.trim();
|
||||
SDB['octo-apik'] = apik.trim();
|
||||
|
||||
filename = $('print-filename').value;
|
||||
form.append("file", getBlob(), filename+"."+fileext);
|
||||
ajax.onreadystatechange = function() {
|
||||
if (ajax.readyState === 4) {
|
||||
let status = ajax.status;
|
||||
STATS.add(`ua_${API.mode.get_lower()}_print_octo_${status}`);
|
||||
if (status >= 200 && status < 300) {
|
||||
API.modal.hide();
|
||||
} else {
|
||||
API.show.alert("octoprint error\nstatus: "+status+"\nmessage: "+ajax.responseText);
|
||||
}
|
||||
API.show.progress(0);
|
||||
}
|
||||
API.show.progress(0);
|
||||
};
|
||||
ajax.upload.addEventListener('progress', function(evt) {
|
||||
API.show.progress(evt.loaded/evt.total, "sending");
|
||||
});
|
||||
ajax.open("POST", host+"/api/files/local");
|
||||
ajax.setRequestHeader("X-Api-Key", apik);
|
||||
ajax.send(form);
|
||||
}
|
||||
|
||||
function gridhost_tracker(host,key) {
|
||||
ajax(host+"/api/check?key="+key, function(data) {
|
||||
data = js2o(data);
|
||||
DBUG.log(data);
|
||||
if (!(data.done || data.error)) {
|
||||
setTimeout(function() { gridhost_tracker(host,key) }, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function gridlocal_probe(ev, devs) {
|
||||
if (ev && ev.code !== 'Enter') return;
|
||||
|
||||
if (!devs && API.probe.local(gridlocal_probe)) return;
|
||||
|
||||
grid_local = devs;
|
||||
|
||||
let html = [];
|
||||
for (let uuid in devs) {
|
||||
let dev = devs[uuid];
|
||||
html.push(`<option id="gl-${uuid}" value="${uuid}">${dev.stat.device.name}</option>`);
|
||||
}
|
||||
|
||||
if (html.length) {
|
||||
$('grid-local').innerHTML = html.join('\n');
|
||||
$('send-to-gridhead').style.display = 'flex';
|
||||
$('send-to-gridspool').style.display = 'flex';
|
||||
} else {
|
||||
$('send-to-gridhead').style.display = '';
|
||||
$('send-to-gridspool').style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
function sendto_gridlocal() {
|
||||
let uuid = $('grid-local').value;
|
||||
let dev = grid_local[uuid];
|
||||
if (dev) {
|
||||
let file = $('print-filename').value;
|
||||
fetch(
|
||||
`/api/grid_send?uuid=${uuid}&file=${encodeURIComponent(file + "." + fileext)}`,
|
||||
{method: "POST", body: gcode}
|
||||
)
|
||||
.then(t => t.text())
|
||||
.then(t => {
|
||||
STATS.add(`ua_${API.mode.get_lower()}_print_local_ok`);
|
||||
console.log({grid_spool_said: t});
|
||||
})
|
||||
.catch(e => {
|
||||
STATS.add(`ua_${API.mode.get_lower()}_print_local_err`);
|
||||
console.log({grid_local_spool_error: e});
|
||||
})
|
||||
.finally(() => {
|
||||
API.modal.hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function admin_gridlocal() {
|
||||
let dev = grid_local[$('grid-local').value];
|
||||
if (dev && dev.stat && dev.stat.device) {
|
||||
let dsd = dev.stat.device;
|
||||
window.open(`http://${dsd.addr[0]}:${dsd.port || 4080}`, "_grid_admin");
|
||||
}
|
||||
}
|
||||
|
||||
function gridhost_probe(ev, set_host) {
|
||||
if (ev && ev.code !== 'Enter') return;
|
||||
if (!(grid_host && grid_apik)) return;
|
||||
|
||||
if (set_host) grid_host.value = set_host;
|
||||
|
||||
let xhtr = new XMLHttpRequest(),
|
||||
host = grid_host.value,
|
||||
apik = grid_apik.value,
|
||||
target = grid_target.value;
|
||||
|
||||
if (!apik) $('gpapik').style.display = 'none';
|
||||
|
||||
if (!host && API.probe.grid(gridhost_probe)) return;
|
||||
|
||||
if (!host) return;
|
||||
|
||||
xhtr.onreadystatechange = function() {
|
||||
if (xhtr.readyState === 4) {
|
||||
if (xhtr.status >= 200 && xhtr.status < 300) {
|
||||
SDB['grid-host'] = host;
|
||||
SDB['grid-apik'] = apik;
|
||||
let res = JSON.parse(xhtr.responseText);
|
||||
let sel = false;
|
||||
let match = false;
|
||||
let first = null;
|
||||
let html = [];
|
||||
grid_targets = {};
|
||||
for (let key in res) {
|
||||
first = first || key;
|
||||
if (!SDB['grid-target']) {
|
||||
SDB['grid-target'] = key;
|
||||
sel = true;
|
||||
} else {
|
||||
sel = SDB['grid-target'] === key;
|
||||
}
|
||||
match = match || sel;
|
||||
grid_targets[html.length] = key;
|
||||
html.push(
|
||||
"<option id='gpo-'" + key + " value='" +key + "'" +
|
||||
(sel ? " selected" : "") +
|
||||
">" +
|
||||
(res[key].comment || key) +
|
||||
"</option>"
|
||||
);
|
||||
}
|
||||
if (!match) {
|
||||
SDB['grid-target'] = first;
|
||||
}
|
||||
grid_target.innerHTML = html.join('\n');
|
||||
} else if (xhtr.status === 401) {
|
||||
$('gpapik').style.display = '';
|
||||
} else {
|
||||
SDB.removeItem('grid-host');
|
||||
SDB.removeItem('grid-apik');
|
||||
console.log("invalid grid:host url");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhtr.open("GET", host + "/api/active?key=" + apik);
|
||||
xhtr.send();
|
||||
}
|
||||
|
||||
function sendto_gridhost() {
|
||||
if (!(grid_host && grid_apik)) return;
|
||||
|
||||
let xhtr = new XMLHttpRequest(),
|
||||
host = grid_host.value,
|
||||
apik = grid_apik.value,
|
||||
target = SDB['grid-target'] || '';
|
||||
|
||||
if (target === '') {
|
||||
API.show.alert('invalid or missing target');
|
||||
return;
|
||||
}
|
||||
if (host.indexOf("http") !== 0) {
|
||||
API.show.alert("host missing protocol (http:// or https://)");
|
||||
return;
|
||||
}
|
||||
if (host.indexOf("://") < 0) {
|
||||
API.show.alert("host:port malformed");
|
||||
return;
|
||||
}
|
||||
if (API.const.SECURE && !API.util.isSecure(host)) {
|
||||
API.show.alert("host must begin with 'https' on a secure site");
|
||||
return;
|
||||
}
|
||||
|
||||
SDB['grid-host'] = host.trim();
|
||||
SDB['grid-apik'] = apik.trim();
|
||||
|
||||
xhtr.onreadystatechange = function() {
|
||||
if (xhtr.readyState === 4) {
|
||||
let status = xhtr.status;
|
||||
STATS.add(`ua_${API.mode.get_lower()}_print_grid_${status}`);
|
||||
if (status >= 200 && status < 300) {
|
||||
let json = js2o(xhtr.responseText);
|
||||
gridhost_tracker(host,json.key);
|
||||
API.ajax(host+"/api/wait?key="+json.key, function(data) {
|
||||
data = js2o(data);
|
||||
DBUG.log(data);
|
||||
API.show.alert("print to "+target+": "+data.status, 600);
|
||||
});
|
||||
} else {
|
||||
API.show.alert("grid:host error\nstatus: "+status+"\nmessage: "+xhtr.responseText, 10000);
|
||||
}
|
||||
API.show.progress(0);
|
||||
}
|
||||
};
|
||||
xhtr.upload.addEventListener('progress', function(evt) {
|
||||
API.show.progress(evt.loaded/evt.total, "sending");
|
||||
});
|
||||
filename = $('print-filename').value;
|
||||
xhtr.open("POST",
|
||||
host + "/api/print?" +
|
||||
"filename=" + filename +
|
||||
"&target=" + target +
|
||||
"&key=" + apik +
|
||||
"&time=" + Math.round(currentPrint.time) +
|
||||
"&length=" + Math.round(currentPrint.distance) +
|
||||
"&image=" + filename
|
||||
);
|
||||
xhtr.setRequestHeader("Content-Type", "text/plain");
|
||||
let snapshot = API.view.snapshot;
|
||||
xhtr.send(snapshot ? [gcode,snapshot].join("\0") : gcode);
|
||||
API.modal.hide();
|
||||
}
|
||||
|
||||
function download() {
|
||||
filename = $('print-filename').value;
|
||||
saveAs(getBlob(), filename + "." + fileext);
|
||||
}
|
||||
|
||||
function pad(v) {
|
||||
v = v.toString();
|
||||
return v.length < 2 ? '0' + v : v;
|
||||
}
|
||||
|
||||
function calcWeight() {
|
||||
try {
|
||||
let density = $('print-density');
|
||||
$('print-weight').value = (
|
||||
(Math.PI * UTIL.sqr(
|
||||
currentPrint.settings.device.extruders[0].extFilament / 2
|
||||
)) *
|
||||
currentPrint.distance *
|
||||
(parseFloat(density.value) || 1.25) /
|
||||
1000
|
||||
).toFixed(2);
|
||||
density.onkeyup = (ev) => {
|
||||
if (ev.key === 'Enter') calcWeight();
|
||||
};
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
function calcTime() {
|
||||
let floor = Math.floor,
|
||||
time = floor(currentPrint.time),
|
||||
hours = floor(time / 3600),
|
||||
newtime = time - hours * 3600,
|
||||
mins = floor(newtime / 60),
|
||||
secs = newtime - mins * 60;
|
||||
|
||||
$('output-time').value = [pad(hours),pad(mins),pad(secs)].join(':');
|
||||
}
|
||||
|
||||
fetch("/kiri/output-gcode.html").then(r => r.text()).then(html => {
|
||||
UI.print.innerHTML = html;
|
||||
let set = API.conf.get();
|
||||
let fdm = MODE === MODES.FDM;
|
||||
let octo = set.controller.exportOcto && MODE !== MODES.CAM;
|
||||
let ghost = set.controller.exportGhost;
|
||||
let local = set.controller.exportLocal;
|
||||
$('print-download').onclick = download;
|
||||
$('print-filament-head').style.display = fdm ? '' : 'none';
|
||||
$('print-filament-info').style.display = fdm ? '' : 'none';
|
||||
$('print-filename').value = filename;
|
||||
$('print-filesize').value = UTIL.comma(currentPrint.bytes);
|
||||
$('print-filament').value = Math.round(currentPrint.distance);
|
||||
calcTime();
|
||||
if (fdm) {
|
||||
calcWeight();
|
||||
}
|
||||
|
||||
// octoprint setup
|
||||
$('send-to-octohead').style.display = octo ? '' : 'none';
|
||||
$('send-to-octoprint').style.display = octo ? '' : 'none';
|
||||
if (octo) try {
|
||||
$('print-octoprint').onclick = sendto_octoprint;
|
||||
octo_host = $('octo-host');
|
||||
octo_apik = $('octo-apik');
|
||||
if (MODE === MODES.CAM) {
|
||||
$('send-to-octohead').style.display = 'none';
|
||||
$('send-to-octoprint').style.display = 'none';
|
||||
} else {
|
||||
$('send-to-octohead').style.display = '';
|
||||
$('send-to-octoprint').style.display = '';
|
||||
}
|
||||
// hide octoprint when hard-coded in the url
|
||||
if (API.const.OCTO) {
|
||||
$('ophost').style.display = 'none';
|
||||
$('opapik').style.display = 'none';
|
||||
$('ophint').style.display = 'none';
|
||||
$('send-to-gridhost').style.display = 'none';
|
||||
}
|
||||
octo_host.value = SDB['octo-host'] || '';
|
||||
octo_apik.value = SDB['octo-apik'] || '';
|
||||
} catch (e) { console.log(e) }
|
||||
|
||||
// grid:host setup
|
||||
$('send-to-hosthead').style.display = ghost ? '' : 'none';
|
||||
$('send-to-gridhost').style.display = ghost ? '' : 'none';
|
||||
if (ghost) try {
|
||||
$('print-gridhost').onclick = sendto_gridhost;
|
||||
$('grid-host').onkeyup = gridhost_probe;
|
||||
$('grid-apik').onkeyup = gridhost_probe;
|
||||
grid_host = $('grid-host');
|
||||
grid_apik = $('grid-apik');
|
||||
grid_target = $('grid-target');
|
||||
grid_target.onchange = function(ev) {
|
||||
SDB['grid-target'] = grid_targets[grid_target.selectedIndex];
|
||||
};
|
||||
grid_host.value = SDB['grid-host'] || '';
|
||||
grid_apik.value = SDB['grid-apik'] || '';
|
||||
gridhost_probe();
|
||||
} catch (e) { console.log(e) }
|
||||
|
||||
// grid:local setup
|
||||
if (local) try {
|
||||
gridlocal_probe();
|
||||
$('print-gridlocal').onclick = sendto_gridlocal;
|
||||
$('admin-gridlocal').onclick = admin_gridlocal;
|
||||
} catch (e) { console.log(e) }
|
||||
|
||||
// show dialog
|
||||
API.modal.show('print');
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
256
src/kiri/fill.js
Normal file
256
src/kiri/fill.js
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.kiri.fill) return;
|
||||
|
||||
const KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
ROUND = UTIL.round,
|
||||
DEG2RAD = Math.PI / 180,
|
||||
FILL = self.kiri.fill = {
|
||||
hex: fillHexFull,
|
||||
grid: fillGrid,
|
||||
gyroid: fillGyroid,
|
||||
triangle: fillTriangle,
|
||||
linear: fillLinear,
|
||||
bubbles: fillBubbles
|
||||
},
|
||||
CACHE = self.kiri.fill_fixed = {
|
||||
hex: fillHexFull,
|
||||
grid: fillGrid,
|
||||
triangle: fillTriangle
|
||||
};
|
||||
|
||||
function fillHexFull(target) {
|
||||
fillHex(target, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* emitter creates a hex infill pattern and sends to target
|
||||
*
|
||||
* @param {Object} target
|
||||
* @param {boolean} full continuous walls
|
||||
*/
|
||||
function fillHex(target, full) {
|
||||
// compute segment lengths (vert/horiz and 45)
|
||||
let spacing = target.offset();
|
||||
let vhlen = (1 - target.density()) * 4 + spacing;
|
||||
let anxlen = ROUND(Math.cos(30 * DEG2RAD) * vhlen, 7);
|
||||
let anylen = ROUND(Math.sin(30 * DEG2RAD) * vhlen, 7);
|
||||
let bounds = target.bounds();
|
||||
let even = true;
|
||||
let evenZ = target.zIndex() % 2 === 0;
|
||||
let maxy = bounds.max.y + (vhlen + anylen * 2);
|
||||
let x, y;
|
||||
|
||||
if (full || evenZ) {
|
||||
x = bounds.min.x;
|
||||
for (;;) {
|
||||
if (even && x > bounds.max.x) break;
|
||||
if (!even && x > bounds.max.x + anxlen + spacing) break;
|
||||
y = bounds.min.y;
|
||||
target.newline();
|
||||
while (y <= maxy) {
|
||||
target.emit(x,y);
|
||||
y += vhlen;
|
||||
target.emit(x,y);
|
||||
if (even) x += anxlen; else x -= anxlen;
|
||||
y += anylen;
|
||||
target.emit(x,y);
|
||||
y += vhlen;
|
||||
target.emit(x,y);
|
||||
if (even) x -= anxlen; else x += anxlen;
|
||||
y += anylen;
|
||||
}
|
||||
x += spacing;
|
||||
if (even) x += (anxlen * 2);
|
||||
even = !even;
|
||||
target.newline();
|
||||
}
|
||||
} else {
|
||||
y = bounds.min.y + vhlen;
|
||||
for (;;) {
|
||||
if (even && y > bounds.max.y) break;
|
||||
if (!even && y > bounds.max.y + anylen) break;
|
||||
x = bounds.min.x;
|
||||
target.newline();
|
||||
while (x < bounds.max.x) {
|
||||
target.emit(x,y);
|
||||
if (even) y += anylen; else y -= anylen;
|
||||
x += anxlen;
|
||||
target.emit(x,y);
|
||||
x += spacing;
|
||||
target.emit(x,y);
|
||||
if (even) y -= anylen; else y += anylen;
|
||||
x += anxlen;
|
||||
target.emit(x,y);
|
||||
x += spacing;
|
||||
}
|
||||
y += vhlen;
|
||||
if (even) y += (anylen * 2);
|
||||
even = !even;
|
||||
target.newline();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fillGyroid(target) {
|
||||
let bounds = target.bounds();
|
||||
let height = target.zHeight();
|
||||
let span_x = bounds.max.x - bounds.min.x;
|
||||
let span_y = bounds.max.y - bounds.min.y;
|
||||
let density = target.density();
|
||||
let tile = 1 + (1 - density) * 15;
|
||||
let tile_x = span_x / tile;
|
||||
let tile_y = span_y / tile;
|
||||
let tile_z = 1 / tile;
|
||||
let gyroid = BASE.gyroid.slice(target.zValue() * tile_z, (1 - density) * 500);
|
||||
|
||||
gyroid.polys.forEach(poly => {
|
||||
for (let tx=0; tx<=tile_x; tx++) {
|
||||
for (let ty=0; ty<=tile_y; ty++) {
|
||||
target.newline();
|
||||
let bx = tx * tile + bounds.min.x;
|
||||
let by = ty * tile + bounds.min.y;
|
||||
poly.forEach(point => {
|
||||
target.emit(bx + point.x * tile, by + point.y * tile);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fillGrid(target) {
|
||||
let bounds = target.bounds();
|
||||
let height = target.zHeight();
|
||||
let span_x = bounds.max.x - bounds.min.x;
|
||||
let span_y = bounds.max.y - bounds.min.y;
|
||||
let density = target.density();
|
||||
let offset = target.offset() / 2;
|
||||
let tile = 1 + (1 - density) * 3;
|
||||
let tile_x = tile + offset;
|
||||
let tile_xc = span_x / tile_x;
|
||||
let tile_yc = span_y / tile;
|
||||
|
||||
for (let tx=0; tx<=tile_xc; tx++) {
|
||||
target.newline();
|
||||
for (let ty=0; ty<=tile_yc; ty++) {
|
||||
let bx = tx * tile_x + bounds.min.x;
|
||||
let by = ty * tile + bounds.min.y;
|
||||
if ((tx + ty) % 2) {
|
||||
target.emit(bx, by);
|
||||
target.emit(bx + tile_x - offset, by + tile);
|
||||
} else {
|
||||
target.emit(bx + tile_x - offset, by);
|
||||
target.emit(bx, by + tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fillLinear(target) {
|
||||
let bounds = target.bounds();
|
||||
let height = target.zHeight();
|
||||
let span_x = bounds.max.x - bounds.min.x;
|
||||
let span_y = bounds.max.y - bounds.min.y;
|
||||
let density = target.density();
|
||||
let offset = target.offset() / 2;
|
||||
let tile = 1 + (1 - density);
|
||||
let tile_xc = Math.ceil(span_x / tile);
|
||||
let tile_yc = Math.ceil(span_y / tile);
|
||||
|
||||
if (target.zIndex() % 2 === 1) {
|
||||
for (let tx=0; tx<=tile_xc; tx++) {
|
||||
target.newline();
|
||||
target.emit(tx * tile + bounds.min.x, bounds.min.y);
|
||||
target.emit(tx * tile + bounds.min.x, bounds.max.y);
|
||||
}
|
||||
} else {
|
||||
for (let ty=0; ty<=tile_yc; ty++) {
|
||||
target.newline();
|
||||
target.emit(bounds.min.x, ty * tile + bounds.min.y);
|
||||
target.emit(bounds.max.x, ty * tile + bounds.min.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fillTriangle(target) {
|
||||
let bounds = target.bounds();
|
||||
let height = target.zHeight();
|
||||
let span_x = bounds.max.x - bounds.min.x;
|
||||
let span_y = bounds.max.y - bounds.min.y;
|
||||
let density = target.density();
|
||||
let offset = target.offset();
|
||||
let line_w = target.lineWidth() / 2;
|
||||
let tile = 1 + (1 - density) * 5;
|
||||
let tile_x = tile + offset*2 + line_w;
|
||||
let tile_xc = span_x / tile_x;
|
||||
let tile_yc = span_y / tile;
|
||||
|
||||
for (let tx=0; tx<=tile_xc; tx++) {
|
||||
target.newline();
|
||||
for (let ty=0; ty<=tile_yc; ty++) {
|
||||
let bx = tx * tile_x + bounds.min.x;
|
||||
let by = ty * tile + bounds.min.y;
|
||||
if ((tx + ty) % 2) {
|
||||
target.emit(bx, by);
|
||||
target.emit(bx + tile_x - offset - line_w, by + tile);
|
||||
} else {
|
||||
target.emit(bx + tile_x - offset - line_w, by);
|
||||
target.emit(bx, by + tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let tx=0; tx<=tile_xc; tx++) {
|
||||
let bx = tx * tile_x + bounds.min.x;
|
||||
let xp = bx + tile_x - line_w/2 - offset/2;
|
||||
target.newline();
|
||||
target.emit(xp, bounds.min.y);
|
||||
target.emit(xp, bounds.max.y);
|
||||
}
|
||||
}
|
||||
|
||||
function fillBubbles(api) {
|
||||
let {min, max} = api.bounds();
|
||||
let slice = api.slice(); // slice object (for adding solids)
|
||||
let height = api.zHeight(); // layer height
|
||||
let offset = api.lineWidth() / 2; // offset size by nozzle width
|
||||
let size = 3 / api.density(); // circle diameter from density
|
||||
let rad = size / 2 - offset; // max circle radius
|
||||
let minr = offset + (2 - api.density() * 2) * offset;
|
||||
let zrep = size / height; // # layers to repeat pattern
|
||||
let zpad = zrep * 0.3; // # pad layers between pattern
|
||||
let bind = api.zIndex() % (zrep + zpad); // index into pattern
|
||||
let brad = bind < zrep ? Math.max(minr,(rad * Math.sin(((bind + 1) / zrep) * Math.PI))) : minr;
|
||||
let sind = (api.zIndex() + (zrep + zpad)/2) % (zrep + zpad); // index into pattern
|
||||
let srad = sind < zrep ? Math.max(minr,(rad * Math.sin(((sind + 1) / (zrep + 0)) * Math.PI))) : minr;
|
||||
for (let x=min.x-size; x<max.x+size; x += size) {
|
||||
let eoy = 0;
|
||||
for (let y=min.y-size; y<max.y+size; y += size) {
|
||||
let xo = (eoy++ % 2 === 0) ? (size / 2) : 0;
|
||||
// primary pattern
|
||||
self.base
|
||||
.newPolygon()
|
||||
.centerCircle({x:x+xo, y:y*0.85}, brad, 20, true)
|
||||
.forEachPoint(p => {
|
||||
api.emit(p.x, p.y);
|
||||
}, true);
|
||||
api.newline();
|
||||
// alternate pattern
|
||||
self.base
|
||||
.newPolygon()
|
||||
.centerCircle({x:x+xo, y:y*0.85+(size/1.75)}, srad, 20, true)
|
||||
.forEachPoint(p => {
|
||||
api.emit(p.x, p.y);
|
||||
}, true);
|
||||
api.newline();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
1932
src/kiri/init.js
Normal file
1932
src/kiri/init.js
Normal file
File diff suppressed because it is too large
Load diff
1
src/kiri/lang-en.js
Symbolic link
1
src/kiri/lang-en.js
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../web/kiri/lang/en.js
|
||||
58
src/kiri/lang.js
Normal file
58
src/kiri/lang.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.kiri.lang) return;
|
||||
|
||||
const KIRI = self.kiri, LANG = self.kiri.lang = { current: {} };
|
||||
|
||||
let lset = navigator.language.toLocaleLowerCase();
|
||||
|
||||
LANG.get = function() {
|
||||
return lset;
|
||||
};
|
||||
|
||||
LANG.set = function() {
|
||||
let map, key, keys = [...arguments];
|
||||
// provide default if none given
|
||||
if (keys.length === 0) {
|
||||
keys = [lset, lset.split('-')[0], 'en'];
|
||||
}
|
||||
for (let i=0; i<keys.length; i++)
|
||||
{
|
||||
key = keys[i]
|
||||
if (!(map = LANG[key])) {
|
||||
continue;
|
||||
}
|
||||
lset = key;
|
||||
let missing = [];
|
||||
let invalid = [];
|
||||
// default to EN values when missing
|
||||
Object.keys(LANG.en).forEach(key => {
|
||||
if (!map[key]) {
|
||||
map[key] = LANG.en[key];
|
||||
missing.push(key);
|
||||
}
|
||||
});
|
||||
// update current map from chosen map
|
||||
Object.keys(map).forEach(key => {
|
||||
if (LANG.en[key]) {
|
||||
LANG.current[key] = map[key];
|
||||
} else {
|
||||
invalid.push(key);
|
||||
}
|
||||
});
|
||||
if (missing.length) {
|
||||
console.log(`language map "${key}" missing keys [${missing.length}]: ${missing.join(', ')}`);
|
||||
}
|
||||
if (invalid.length) {
|
||||
console.log(`language map "${key}" invalid keys [${invalid.length}]: ${invalid.join(', ')}`);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
})();
|
||||
305
src/kiri/layer.js
Normal file
305
src/kiri/layer.js
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.kiri.Layer) return;
|
||||
|
||||
const KIRI = self.kiri,
|
||||
POLY = self.base.polygons,
|
||||
LP = Layer.prototype,
|
||||
mcache = {};
|
||||
|
||||
KIRI.Layer = Layer;
|
||||
KIRI.newLayer = function(view) { return new Layer(view) };
|
||||
|
||||
function materialFor(layer, color, mesh, linewidth) {
|
||||
let m = mcache[color];
|
||||
if (!m) {
|
||||
m = mcache[color] = mesh ? new THREE.MeshPhongMaterial({
|
||||
transparent: layer.transparent,
|
||||
shininess: layer.shininess,
|
||||
specular: layer.specular,
|
||||
opacity: layer.opacity,
|
||||
color: color,
|
||||
side: THREE.DoubleSide
|
||||
}) : new THREE.LineBasicMaterial({
|
||||
fog: false,
|
||||
color: color,
|
||||
linewidth: 1
|
||||
});
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
function toVector(p) {
|
||||
return new THREE.Vector3(p.x, p.y, p.z);
|
||||
}
|
||||
|
||||
function addPoly(arr, poly, deep, open) {
|
||||
let points = poly.points,
|
||||
len = points.length;
|
||||
|
||||
if (len < 2) return;
|
||||
|
||||
let doOpen = (open === undefined || open === null) ? poly.isOpen() : open,
|
||||
end = doOpen ? len - 1 : len,
|
||||
last = toVector(points[0]),
|
||||
i = 0;
|
||||
|
||||
while (i < end) {
|
||||
arr.push(last);
|
||||
arr.push(last = toVector(points[(++i) % len]));
|
||||
}
|
||||
|
||||
if (deep && poly.inner) {
|
||||
poly.inner.forEach(function(p) {
|
||||
addPoly(arr, p, false, open);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function addSolid(arr, poly) {
|
||||
let faces = poly.earcut();
|
||||
faces.forEach(p => {
|
||||
p.forEachPoint(p => {
|
||||
arr.push(p.x, p.y, p.z);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {THREE.Group} view
|
||||
*/
|
||||
function Layer(view) {
|
||||
this.changed = false;
|
||||
this.group = null;
|
||||
this.solids = null;
|
||||
this.view = view;
|
||||
this.bycolor = {};
|
||||
this.opacity = 0.15,
|
||||
this.specular = 0x181818;
|
||||
this.shininess = 100;
|
||||
this.transparent = true;
|
||||
};
|
||||
|
||||
LP.setOpacity = function(v) {
|
||||
this.opacity = v;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.setSpecular = function(v) {
|
||||
this.specular = v;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.setShininess = function(v) {
|
||||
this.shininess = v;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.setTransparent = function(b) {
|
||||
this.transparent = b;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.setVisible = function(vis) {
|
||||
if (this.group) this.group.visible = vis;
|
||||
if (this.solids) this.solids.visible = vis;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.clear = function() {
|
||||
this.changed = true;
|
||||
this.bycolor = {};
|
||||
};
|
||||
|
||||
LP.add = function(color, obj) {
|
||||
let ca = this.bycolor[color];
|
||||
if (!ca) ca = this.bycolor[color] = [];
|
||||
ca.push(obj);
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.poly = function(poly, color, deep, open) {
|
||||
let layer = this;
|
||||
if (Array.isArray(poly)) {
|
||||
poly.forEach(function(p) { layer.poly(p, color, deep, open) });
|
||||
} else {
|
||||
this.add(color, {poly:poly, deep:deep, open:open});
|
||||
this.changed = true;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.solid = function(poly, color) {
|
||||
this.add(color, {solid: poly});
|
||||
this.changed = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.noodle = function(poly, offset, color, traceColor, open) {
|
||||
if (!poly) return;
|
||||
if (Array.isArray(poly)) {
|
||||
poly = POLY.flatten(poly, [], true);
|
||||
} else {
|
||||
poly = POLY.flatten([poly], [], true);
|
||||
}
|
||||
if (!poly.length) return;
|
||||
let z = poly[0].getZ();
|
||||
poly.forEach(p => {
|
||||
let exp = [];
|
||||
POLY.expand([p], offset, z-0.01, exp, 1);
|
||||
POLY.expand([p], -offset, z-0.01, exp, 1);
|
||||
POLY.nest(exp).forEach((p,i) => {
|
||||
this.add(color, {solid: p});
|
||||
this.add(traceColor, {poly: p.clone(true).setZ(z + 0.01), deep: true, open});
|
||||
});
|
||||
})
|
||||
this.changed = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.noodle_open = function(poly, offset, color, traceColor, z) {
|
||||
poly = POLY.expand_lines(poly, offset, z);
|
||||
poly.forEach(p => {
|
||||
this.add(color, {solid: p});
|
||||
this.add(traceColor, {poly: p.clone().setZ(z + 0.01), deep: false, open: false});
|
||||
})
|
||||
this.changed = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.noodle_lines = function(points, offset, color, traceColor, z) {
|
||||
if (!(points && points.length)) return;
|
||||
for (let i=0; i<points.length; i += 2) {
|
||||
let p1 = points[i];
|
||||
let p2 = points[i+1];
|
||||
let l1 = p1.offsetLineTo(p2, offset);
|
||||
let l2 = p2.offsetLineTo(p1, offset);
|
||||
let poly = base.newPolygon().addPoints([
|
||||
l1.p1, l1.p2, l2.p1, l2.p2
|
||||
]);
|
||||
this.add(color, {solid: poly});
|
||||
this.add(traceColor, {poly: poly.clone().setZ(z + 0.01), deep: false, open: false});
|
||||
}
|
||||
this.changed = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.points = function(points, color, size, opacity) {
|
||||
let layer = this,
|
||||
sz = size/2;
|
||||
points.forEach(function(p) {
|
||||
layer.lines([
|
||||
toVector({x:p.x+sz, y:p.y+sz, z:p.z-sz}),
|
||||
toVector({x:p.x+sz, y:p.y+sz, z:p.z+sz}),
|
||||
toVector({x:p.x-sz, y:p.y+sz, z:p.z-sz}),
|
||||
toVector({x:p.x-sz, y:p.y+sz, z:p.z+sz}),
|
||||
toVector({x:p.x+sz, y:p.y-sz, z:p.z-sz}),
|
||||
toVector({x:p.x+sz, y:p.y-sz, z:p.z+sz}),
|
||||
toVector({x:p.x-sz, y:p.y-sz, z:p.z-sz}),
|
||||
toVector({x:p.x-sz, y:p.y-sz, z:p.z+sz}),
|
||||
|
||||
toVector({x:p.x+sz, y:p.y+sz, z:p.z+sz}),
|
||||
toVector({x:p.x-sz, y:p.y+sz, z:p.z+sz}),
|
||||
toVector({x:p.x+sz, y:p.y+sz, z:p.z-sz}),
|
||||
toVector({x:p.x-sz, y:p.y+sz, z:p.z-sz}),
|
||||
toVector({x:p.x+sz, y:p.y-sz, z:p.z+sz}),
|
||||
toVector({x:p.x-sz, y:p.y-sz, z:p.z+sz}),
|
||||
toVector({x:p.x+sz, y:p.y-sz, z:p.z-sz}),
|
||||
toVector({x:p.x-sz, y:p.y-sz, z:p.z-sz}),
|
||||
|
||||
toVector({x:p.x+sz, y:p.y+sz, z:p.z+sz}),
|
||||
toVector({x:p.x+sz, y:p.y-sz, z:p.z+sz}),
|
||||
toVector({x:p.x+sz, y:p.y+sz, z:p.z-sz}),
|
||||
toVector({x:p.x+sz, y:p.y-sz, z:p.z-sz}),
|
||||
toVector({x:p.x-sz, y:p.y+sz, z:p.z+sz}),
|
||||
toVector({x:p.x-sz, y:p.y-sz, z:p.z+sz}),
|
||||
toVector({x:p.x-sz, y:p.y+sz, z:p.z-sz}),
|
||||
toVector({x:p.x-sz, y:p.y-sz, z:p.z-sz}),
|
||||
], color);
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.lines = function(points, color) {
|
||||
this.add(color, {lines:points});
|
||||
this.changed = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
LP.renderAll = function() {
|
||||
this.renderSolid();
|
||||
this.render();
|
||||
};
|
||||
|
||||
LP.renderSolid = function() {
|
||||
if (!(this.view)) return;
|
||||
if (this.solids) this.view.remove(this.solids);
|
||||
this.solids = this.view.newGroup();
|
||||
|
||||
let bycolor = this.bycolor;
|
||||
|
||||
for (let key in bycolor) {
|
||||
if (!bycolor.hasOwnProperty(key)) continue;
|
||||
|
||||
let arr = [], mat = materialFor(this, parseInt(key), true);
|
||||
|
||||
bycolor[key].forEach(function(obj) {
|
||||
if (obj.solid) {
|
||||
addSolid(arr, obj.solid);
|
||||
}
|
||||
});
|
||||
|
||||
if (arr.length > 0) {
|
||||
let geo = new THREE.BufferGeometry();
|
||||
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(arr.toFloat32(), 3));
|
||||
geo.computeFaceNormals();
|
||||
geo.computeVertexNormals();
|
||||
|
||||
let mesh = new THREE.Mesh(geo, mat);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
|
||||
this.solids.add(mesh);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
LP.render = function() {
|
||||
if (!(this.view && this.changed)) return;
|
||||
if (this.group) this.view.remove(this.group);
|
||||
this.group = this.view.newGroup();
|
||||
|
||||
let bycolor = this.bycolor,
|
||||
key, added;
|
||||
|
||||
for (key in bycolor) {
|
||||
if (!bycolor.hasOwnProperty(key)) continue;
|
||||
|
||||
let geo = new THREE.Geometry(),
|
||||
arr = geo.vertices,
|
||||
mat = materialFor(this, parseInt(key));
|
||||
|
||||
added = bycolor[key];
|
||||
|
||||
added.forEach(function(obj) {
|
||||
if (obj.poly) {
|
||||
addPoly(arr, obj.poly, obj.deep, obj.open);
|
||||
} else if (obj.lines) {
|
||||
obj.lines.forEach(function(p) {
|
||||
arr.push(toVector(p));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (arr.length > 0) this.group.add(new THREE.LineSegments(geo, mat));
|
||||
}
|
||||
|
||||
this.changed = false;
|
||||
}
|
||||
|
||||
})();
|
||||
1989
src/kiri/main.js
Normal file
1989
src/kiri/main.js
Normal file
File diff suppressed because it is too large
Load diff
1157
src/kiri/print.js
Normal file
1157
src/kiri/print.js
Normal file
File diff suppressed because it is too large
Load diff
1402
src/kiri/slice.js
Normal file
1402
src/kiri/slice.js
Normal file
File diff suppressed because it is too large
Load diff
899
src/kiri/slicer.js
Normal file
899
src/kiri/slicer.js
Normal file
|
|
@ -0,0 +1,899 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.kiri.slicer) return;
|
||||
|
||||
self.kiri.slicer = {
|
||||
slice: slice,
|
||||
sliceWidget: sliceWidget,
|
||||
connectLines: connectLines
|
||||
};
|
||||
|
||||
let KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
CONF = BASE.config,
|
||||
UTIL = BASE.util,
|
||||
POLY = BASE.polygons,
|
||||
time = UTIL.time,
|
||||
newSlice = KIRI.newSlice,
|
||||
newOrderedLine = BASE.newOrderedLine;
|
||||
|
||||
/**
|
||||
* Convenience method. Gets a Widget's points and calls slice()
|
||||
*
|
||||
* @param {Widget} widget
|
||||
* @param {Object} options
|
||||
* @param {Function} ondone callback when slicing complete
|
||||
* @param {Function} onupdate callback on incremental updates
|
||||
*/
|
||||
function sliceWidget(widget, options, ondone, onupdate) {
|
||||
slice(widget.getPoints(), widget.getBoundingBox(), options, ondone, onupdate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of points as triples, a bounding box and a set of
|
||||
* slicing controls, emit an array of Slice objects to the ondone()
|
||||
* function. onupdate() will be called with two parameters (% completion
|
||||
* and an optional message) so that the UI can report progress to the user.
|
||||
*
|
||||
* @param {Array} points vertex array
|
||||
* @param {Bounds} bounds bounding box for points
|
||||
* @param {Object} options slicing parameters
|
||||
* @param {Function} ondone callback when slicing done
|
||||
* @param {Function} onupdate callback to report slicing progress
|
||||
*/
|
||||
function slice(points, bounds, options, ondone, onupdate) {
|
||||
let topoMode = options.topo,
|
||||
simpleMode = options.simple,
|
||||
swap = options.swapX || options.swapY,
|
||||
isCam = options.cam,
|
||||
ox = 0,
|
||||
oy = 0;
|
||||
|
||||
// handle rotating meshes for CAM finishing.
|
||||
// slicer expects things just so, so we alter
|
||||
// geometry to satisfy
|
||||
if (swap) {
|
||||
points = points.slice();
|
||||
|
||||
let btmp = new THREE.Box3(),
|
||||
pref = {},
|
||||
cached;
|
||||
|
||||
btmp.setFromPoints(points);
|
||||
if (options.swapX) ox = -btmp.max.x;
|
||||
if (options.swapY) oy = -btmp.max.y;
|
||||
|
||||
// array re-uses points so we need
|
||||
// to be careful not to alter a point
|
||||
// more than once
|
||||
for (let p, index=0; index<points.length; index++) {
|
||||
p = points[index];
|
||||
cached = pref[p.key];
|
||||
// skip points already altered
|
||||
if (cached) {
|
||||
points[index] = cached;
|
||||
continue;
|
||||
}
|
||||
cached = p.clone();
|
||||
if (options.swapX) cached.swapXZ();
|
||||
else if (options.swapY) cached.swapYZ();
|
||||
cached.rekey();
|
||||
pref[p.key] = cached;
|
||||
points[index] = cached;
|
||||
}
|
||||
|
||||
// update temp bounds from new points
|
||||
btmp.setFromPoints(points);
|
||||
for (let p, index=0; index<points.length; index++) {
|
||||
p = points[index];
|
||||
if (p.mod === 1) continue;
|
||||
p.mod = 1;
|
||||
p.z -= btmp.min.z;
|
||||
}
|
||||
|
||||
// update temp bounds from points with altered Z
|
||||
btmp.setFromPoints(points);
|
||||
bounds = btmp;
|
||||
}
|
||||
|
||||
let zMin = options.zmin || options.firstHeight || Math.floor(bounds.min.z),
|
||||
zMax = options.zmax || Math.ceil(bounds.max.z),
|
||||
zInc = options.height,
|
||||
zIncMin = options.minHeight,
|
||||
zIncFirst = options.firstHeight || zInc,
|
||||
zOff = true ? zInc / 2 : 0,
|
||||
zHeights = [], // heights for zIndexes in adaptive mode
|
||||
zIndexes = [], // auto-detected z slicing offsets (laser/cam)
|
||||
zOrdered = [], // ordered list of Z indexes
|
||||
zList = {}, // map count of z index points for adaptive slicing
|
||||
zFlat = {}, // map area of z index flat areas (cam)
|
||||
zLines = {}, // map count of z index lines
|
||||
zScale, // bucket span in z units
|
||||
timeStart = time(),
|
||||
slices = [],
|
||||
zSum = 0.0,
|
||||
buckets = [],
|
||||
i, j = 0, k, p1, p2, p3, px,
|
||||
CPRO = KIRI.driver.CAM.process;
|
||||
|
||||
if (options.add) zMax += zInc;
|
||||
|
||||
function countZ(z) {
|
||||
z = UTIL.round(z,5);
|
||||
zList[z] = (zList[z] || 0) + 1;
|
||||
}
|
||||
|
||||
// gather z-index stats
|
||||
// these are used for auto-slicing in laser
|
||||
// and to flats detection in CAM mode
|
||||
for (i = 0; i < points.length;) {
|
||||
p1 = points[i++];
|
||||
p2 = points[i++];
|
||||
p3 = points[i++];
|
||||
zSum += (Math.abs(p1.z - p2.z) + Math.abs(p2.z - p3.z) + Math.abs(p3.z - p1.z));
|
||||
// laser auto-detect z slice points
|
||||
if (zInc === 0 || zIncMin) {
|
||||
countZ(p1.z);
|
||||
countZ(p2.z);
|
||||
countZ(p3.z);
|
||||
}
|
||||
// use co-flat and co-line detection to adjust slice Z
|
||||
if (p1.z === p2.z && p2.z === p3.z && p1.z > bounds.min.z) {
|
||||
// detect zFlat faces to avoid slicing directly on them
|
||||
let zkey = p1.z.toFixed(5),
|
||||
area = Math.abs(UTIL.area2(p1,p2,p3)) / 2;
|
||||
if (!zFlat[zkey]) {
|
||||
zFlat[zkey] = area;
|
||||
} else {
|
||||
zFlat[zkey] += area;
|
||||
}
|
||||
} else if (true || options.trace) {
|
||||
// detect zLines (curved region tops/bottoms)
|
||||
// mark these layers for ball and v mill tracing
|
||||
if (p1.z === p2.z && p1.z > bounds.min.z) {
|
||||
let zkey = p1.z.toFixed(5);
|
||||
let zval = zLines[zkey];
|
||||
zLines[zkey] = (zval || 0) + 1;
|
||||
}
|
||||
if (p2.z === p3.z && p2.z > bounds.min.z) {
|
||||
let zkey = p2.z.toFixed(5);
|
||||
let zval = zLines[zkey];
|
||||
zLines[zkey] = (zval || 0) + 1;
|
||||
}
|
||||
if (p3.z === p1.z && p3.z > bounds.min.z) {
|
||||
let zkey = p3.z.toFixed(5);
|
||||
let zval = zLines[zkey];
|
||||
zLines[zkey] = (zval || 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CAM: find slice candidates to trace for ballmills and tapermills
|
||||
if (options.trace) {
|
||||
let zl = {};
|
||||
let le;
|
||||
let zs = Object.entries(zLines).map(oe => {
|
||||
return oe.map(v => parseFloat(v));
|
||||
}).sort((a,b) => {
|
||||
return b[0] - a[0];
|
||||
}).forEach((e,i) => {
|
||||
if (i > 0) {
|
||||
let zd = le[0]-e[0];
|
||||
if (zd > 0.1 && e[1] > 100) {
|
||||
// zl.push(e)
|
||||
zl[e[0].toFixed(5)] = e[1];
|
||||
zFlat[e[0].toFixed(5)] = e[1];
|
||||
}
|
||||
}
|
||||
if (e[1] > 10) {
|
||||
le = e;
|
||||
}
|
||||
});
|
||||
zLines = zl;
|
||||
}
|
||||
|
||||
/**
|
||||
* bucket polygons into z-bounded groups (inside or crossing)
|
||||
* to reduce the search space in complex models
|
||||
*/
|
||||
let bucketCount = Math.max(1, Math.ceil(zMax / (zSum / points.length)) - 1);
|
||||
|
||||
zScale = 1 / (zMax / bucketCount);
|
||||
|
||||
if (bucketCount > 1) {
|
||||
// create empty buckets
|
||||
for (i = 0; i < bucketCount + 1; i++) buckets.push([]);
|
||||
|
||||
// copy triples into all matching z-buckets
|
||||
for (i = 0; i < points.length;) {
|
||||
p1 = points[i++];
|
||||
p2 = points[i++];
|
||||
p3 = points[i++];
|
||||
let zm = Math.min(p1.z, p2.z, p3.z),
|
||||
zM = Math.max(p1.z, p2.z, p3.z),
|
||||
bm = Math.floor(zm * zScale),
|
||||
bM = Math.ceil(zM * zScale);
|
||||
for (j = bm; j < bM; j++) {
|
||||
buckets[j].push(p1);
|
||||
buckets[j].push(p2);
|
||||
buckets[j].push(p3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we need Z ordered list for laser auto or adaptive fdm slicing
|
||||
if (zInc === 0 || zIncMin) {
|
||||
for (let key in zList) {
|
||||
if (!zList.hasOwnProperty(key)) continue;
|
||||
zOrdered.push(parseFloat(key));
|
||||
}
|
||||
zOrdered.sort(function(a,b) { return a - b});
|
||||
}
|
||||
|
||||
if (options.single) {
|
||||
// usually for laser single slice
|
||||
zIndexes.push(zMin + zInc);
|
||||
} else if (zInc === 0) {
|
||||
// use Z indices in auto slice mode for laser
|
||||
// find unique z-index offsets for slicing
|
||||
let zl = zOrdered
|
||||
for (i = 0; i < zl.length - 1; i++) {
|
||||
zIndexes.push((zl[i] + zl[i+1]) / 2);
|
||||
}
|
||||
} else if (isCam) {
|
||||
// re-divide slice height so that top and
|
||||
// bottom slices fall exactly on those faces
|
||||
zInc = (zMax - zMin) / (Math.floor(zMax / zInc) + 1);
|
||||
for (i = zMin; i < zMax; i += zInc) {
|
||||
zIndexes.push(i);
|
||||
}
|
||||
for (let key in zFlat) {
|
||||
// todo make threshold for flat detection configurable
|
||||
if (!zFlat.hasOwnProperty(key) || zFlat[key] < 10){
|
||||
continue;
|
||||
}
|
||||
key = parseFloat(key);
|
||||
if (!zIndexes.contains(key) && key >= zMin) {
|
||||
zIndexes.push(key);
|
||||
}
|
||||
}
|
||||
// sort top down
|
||||
zIndexes.sort(function(a,b) {
|
||||
return b-a;
|
||||
});
|
||||
// copy to store original z
|
||||
zHeights = zIndexes.slice();
|
||||
} else if (zIncMin) {
|
||||
// console.log('adaptive slicing', zIncMin, ':', zInc, 'from', zMin, 'to', zMax);
|
||||
// FDM adaptive slicing
|
||||
let zPos = zMin + zIncFirst,
|
||||
zOI = 0,
|
||||
zDelta,
|
||||
zDivMin,
|
||||
zDivMax,
|
||||
zStep,
|
||||
nextZ,
|
||||
lzp = zPos;
|
||||
|
||||
// first slice is fixed
|
||||
zHeights.push(zIncFirst);
|
||||
zIndexes.push(zIncFirst / 2);
|
||||
|
||||
while (zPos < zMax && zOI < zOrdered.length) {
|
||||
nextZ = zOrdered[zOI++];
|
||||
if (zPos >= nextZ) continue;
|
||||
zDelta = nextZ - zPos;
|
||||
if (zDelta < zIncMin) continue;
|
||||
|
||||
zDivMin = Math.floor(zDelta / zIncMin);
|
||||
zDivMax = Math.floor(zDelta / zInc);
|
||||
|
||||
if (zDivMax && zDivMax <= zDivMin) {
|
||||
if (zDelta % zInc > 0.01) zDivMax++;
|
||||
zStep = zDelta / zDivMax;
|
||||
// console.log(`--- zDivMax <= zDivMin ---`, zStep, zDelta % zInc)
|
||||
} else {
|
||||
zStep = zDelta;
|
||||
}
|
||||
// console.log({nextZ, zPos, zDelta, zStep, zDivMin, zDivMax})
|
||||
while (zPos < nextZ) {
|
||||
zHeights.push(zStep);
|
||||
zIndexes.push(zPos + zStep / 2);
|
||||
zPos += zStep;
|
||||
// console.log({D: zPos - lzp, zPos})
|
||||
// lzp = zPos;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// console.log('fixed slicing', zInc, 'from', zMin, 'to', zMax);
|
||||
// FDM fixed slicing
|
||||
if (options.firstHeight) {
|
||||
zIndexes.push(options.firstHeight / 2);
|
||||
zHeights.push(options.firstHeight);
|
||||
zMin = options.firstHeight;
|
||||
}
|
||||
for (i = zMin + zOff; i < zMax; i += zInc) {
|
||||
zIndexes.push(i);
|
||||
zHeights.push(zInc);
|
||||
}
|
||||
}
|
||||
|
||||
// create a Slice for each z offset in the zIndexes array
|
||||
for (let i = 0; i < zIndexes.length; i++) {
|
||||
let ik = zIndexes[i].toFixed(5),
|
||||
onFlat = false,
|
||||
onLine = false;
|
||||
// ensure no slice through horizontal lines or planes
|
||||
if (zFlat[ik]) onFlat = true;
|
||||
if (zLines[ik]) onLine = true;
|
||||
if (onFlat || onLine) {
|
||||
zIndexes[i] += isCam ? 0.001 : -0.001;
|
||||
}
|
||||
// slice next layer and add to slices[] array
|
||||
let slice = sliceZ(zIndexes[i], zHeights[i], onFlat, onLine);
|
||||
// override z in cam in case it was moved for a flat or line
|
||||
if (isCam && slice) {
|
||||
slice.z = zHeights[i];
|
||||
}
|
||||
onupdate(i / zIndexes.length);
|
||||
}
|
||||
|
||||
// for cam, bottom as mandatory (hasFlats)
|
||||
if (isCam && slices.length > 0) {
|
||||
// slices[0].hasFlats = true;
|
||||
slices[slices.length-1].hasFlats = true;
|
||||
}
|
||||
|
||||
// connect slices into linked list for island/bridge projections
|
||||
for (i=1; i<slices.length; i++) {
|
||||
slices[i-1].up = slices[i];
|
||||
slices[i].down = slices[i-1];
|
||||
}
|
||||
|
||||
slices.slice_time = time() - timeStart;
|
||||
|
||||
// pass Slices array back to ondone function
|
||||
ondone(slices);
|
||||
|
||||
/** ***** SLICING FUNCTIONS ***** */
|
||||
|
||||
/**
|
||||
* given a point, append to the correct
|
||||
* 'where' objec tarray (on, over or under)
|
||||
*
|
||||
* @param {Point} p
|
||||
* @param {number} z offset
|
||||
* @param {Obejct} where
|
||||
*/
|
||||
function checkUnderOverOn(p, z, where) {
|
||||
let delta = p.z - z;
|
||||
if (Math.abs(delta) < CONF.precision_slice_z) { // on
|
||||
where.on.push(p);
|
||||
} else if (delta < 0) { // under
|
||||
where.under.push(p);
|
||||
} else { // over
|
||||
where.over.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a point over and under a z offset, calculate
|
||||
* and return the intersection point on that z plane
|
||||
*
|
||||
* @param {Point} over
|
||||
* @param {Point} under
|
||||
* @param {number} z offset
|
||||
* @returns {Point} intersection point
|
||||
*/
|
||||
function intersectPoints(over, under, z) {
|
||||
let ip = [];
|
||||
for (let i = 0; i < over.length; i++) {
|
||||
for (let j = 0; j < under.length; j++) {
|
||||
ip.push(over[i].intersectZ(under[j], z));
|
||||
}
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure points are unique with a cache/key algorithm
|
||||
*/
|
||||
function getCachedPoint(phash, p) {
|
||||
let cached = phash[p.key];
|
||||
if (!cached) {
|
||||
phash[p.key] = p;
|
||||
return p;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
/**
|
||||
* Given two points and hints about their edges,
|
||||
* return a new Line object with points sorted
|
||||
* lexicographically by key. This allows for future
|
||||
* line de-duplication and joins.
|
||||
*
|
||||
* @param {Object} phash
|
||||
* @param {Point} p1
|
||||
* @param {Point} p2
|
||||
* @param {boolean} [coplanar]
|
||||
* @param {boolean} [edge]
|
||||
* @returns {Line}
|
||||
*/
|
||||
function makeZLine(phash, p1, p2, coplanar, edge) {
|
||||
p1 = getCachedPoint(phash, p1);
|
||||
p2 = getCachedPoint(phash, p2);
|
||||
let line = newOrderedLine(p1,p2);
|
||||
line.coplanar = coplanar || false;
|
||||
line.edge = edge || false;
|
||||
return line;
|
||||
}
|
||||
|
||||
/**
|
||||
* process a single z-slice on a single mesh and
|
||||
* add to slices array
|
||||
*
|
||||
* @param {number} z
|
||||
* @param {number} [height] optional real height (fdm)
|
||||
*/
|
||||
function sliceZ(z, height, onflat, online) {
|
||||
let phash = {},
|
||||
lines = [],
|
||||
slice = newSlice(z, options.view ? options.view.newGroup() : null),
|
||||
bucket = bucketCount == 1 ? points : buckets[Math.floor(z * zScale)];
|
||||
|
||||
if (!bucket) {
|
||||
return;
|
||||
}
|
||||
|
||||
// annotate slices with cam flats for finishing waterlines
|
||||
if (onflat && isCam) {
|
||||
slice.hasFlats = true;
|
||||
}
|
||||
|
||||
// iterate over matching buckets for this z offset
|
||||
for (let i = 0; i < bucket.length;) {
|
||||
p1 = bucket[i++];
|
||||
p2 = bucket[i++];
|
||||
p3 = bucket[i++];
|
||||
let where = {under: [], over: [], on: []};
|
||||
checkUnderOverOn(p1, z, where);
|
||||
checkUnderOverOn(p2, z, where);
|
||||
checkUnderOverOn(p3, z, where);
|
||||
if (where.under.length === 3 || where.over.length === 3) {
|
||||
// does not intersect (all 3 above or below)
|
||||
} else if (where.on.length === 2) {
|
||||
// one side of triangle is on the Z plane
|
||||
lines.push(makeZLine(phash, where.on[0], where.on[1], false, true));
|
||||
} else if (where.on.length === 3) {
|
||||
// triangle is coplanar with Z
|
||||
// we drop these because this face is attached to 3 others
|
||||
// that will satisfy the if above (line) with 2 points
|
||||
} else if (where.under.length === 0 || where.over.length === 0) {
|
||||
// does not intersect but one point is on the slice Z plane
|
||||
} else {
|
||||
// compute two point intersections and construct line
|
||||
let line = intersectPoints(where.over, where.under, z);
|
||||
if (line.length < 2 && where.on.length === 1) {
|
||||
line.push(where.on[0]);
|
||||
}
|
||||
if (line.length === 2) {
|
||||
lines.push(makeZLine(phash, line[0], line[1]));
|
||||
} else {
|
||||
console.log({msg: "invalid ips", line: line, where: where});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// allow empty slices in CAM swap mode (for topos w/ gaps)
|
||||
// if (lines.length == 0 && !(options.swapX || options.swapY)) return;
|
||||
if (lines.length == 0 && options.noEmpty) return;
|
||||
|
||||
slice.height = height;
|
||||
slice.index = slices.length;
|
||||
slice.lines = removeDuplicateLines(lines);
|
||||
|
||||
// for topo slices, we just need the raw lines
|
||||
if (!topoMode && !simpleMode) {
|
||||
slice.groups = connectLines(slice.lines, slices.length);
|
||||
POLY.nest(slice.groups).forEach(function(top) { slice.addTop(top) });
|
||||
}
|
||||
|
||||
// fixup un-rotates polygons for CAM
|
||||
if (options.swapX || options.swapY) {
|
||||
let move = {x:ox, y:oy, z:0};
|
||||
slice.camMode = options.swapX ? CPRO.FINISH_X : CPRO.FINISH_Y;
|
||||
if (topoMode) {
|
||||
let lines = slice.lines, llen = lines.length, idx, line;
|
||||
// shared points causing problems
|
||||
for (idx=0; idx<llen; idx++) {
|
||||
line = lines[idx];
|
||||
line.p1 = line.p1.clone();
|
||||
line.p2 = line.p2.clone();
|
||||
}
|
||||
for (idx=0; idx<llen; idx++) {
|
||||
line = lines[idx];
|
||||
if (options.swapX) {
|
||||
line.p1.swapXZ();
|
||||
line.p2.swapXZ();
|
||||
} else {
|
||||
line.p1.swapYZ();
|
||||
line.p2.swapYZ();
|
||||
}
|
||||
line.p1.move(move);
|
||||
line.p2.move(move);
|
||||
}
|
||||
} else if (simpleMode) {
|
||||
// fdm polishing mode
|
||||
slice.groups = connectLines(slice.lines, slices.length);
|
||||
slice.groups.forEach(poly => {
|
||||
poly.swap(options.swapX, options.swapY);
|
||||
poly.move(move);
|
||||
poly.inner = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
slices.push(slice);
|
||||
return slice;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of input lines (line soup), find the path through
|
||||
* joining line ends that encompasses the greatest area without self
|
||||
* interesection. Eliminate used points and repeat. Unjoined lines
|
||||
* are permitted and handled after all other cases are handled.
|
||||
*
|
||||
* @param {Line[]} input
|
||||
* @param {number} [index]
|
||||
* @returns {Array}
|
||||
*/
|
||||
function connectLines(input) {
|
||||
// map points to all other points they're connected to
|
||||
let DBUG = BASE.debug,
|
||||
CONF = BASE.config,
|
||||
pmap = {},
|
||||
points = [],
|
||||
output = [],
|
||||
connect = [],
|
||||
search = 1,
|
||||
nextMod = 1,
|
||||
bridge = CONF.bridgeLineGapDistance,
|
||||
p1, p2;
|
||||
|
||||
function cachedPoint(p) {
|
||||
let cp = pmap[p.key];
|
||||
if (cp) return cp;
|
||||
points.push(p);
|
||||
pmap[p.key] = p;
|
||||
p.mod = nextMod++; // unique seq ID for points
|
||||
p.toString = function() { return this.mod }; // point array concat
|
||||
return p;
|
||||
}
|
||||
|
||||
function addConnected(p1, p2) {
|
||||
if (!p1.group) p1.group = [ p2 ];
|
||||
else p1.group.push(p2);
|
||||
}
|
||||
|
||||
function sliceAtTerm(path, term) {
|
||||
let idx, len = path.length;
|
||||
for (idx = 0; idx < len-1; idx++) {
|
||||
if (path[idx] === term) {
|
||||
return path.slice(idx);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* using minimal recursion, follow points through connected lines
|
||||
* to form candidate output paths.
|
||||
*/
|
||||
function findPathsMinRecurse(point, path, paths, from) {
|
||||
let stack = [ ];
|
||||
if (paths.length > 10000) {
|
||||
DBUG.log("excessive path options @ "+paths.length+" #"+input.length);
|
||||
return;
|
||||
}
|
||||
for (;;) {
|
||||
stack.push(point);
|
||||
|
||||
let last = point,
|
||||
links = point.group;
|
||||
|
||||
path.push(point);
|
||||
// use del to mark traversed path
|
||||
point.del = true;
|
||||
// set so point isn't used in another polygon search
|
||||
point.pos = search++;
|
||||
// seed path with two points to prevent redundant opposing seeks
|
||||
if (path.length === 1) {
|
||||
from = point;
|
||||
point = links[0];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (links.length > 2) {
|
||||
// TODO optimize when > 2 and limit to left-most and right-most branches
|
||||
// for now, pursue all possible branches
|
||||
links.forEach(function(nextp) {
|
||||
// do not backtrack
|
||||
if (nextp === from) {
|
||||
return;
|
||||
}
|
||||
if (nextp.del) {
|
||||
paths.push(sliceAtTerm(path,nextp));
|
||||
} else {
|
||||
findPathsMinRecurse(nextp, path.slice(), paths, point);
|
||||
}
|
||||
});
|
||||
break;
|
||||
} else {
|
||||
point = links[0] === from ? links[1] : links[0];
|
||||
from = last;
|
||||
// hit an open end
|
||||
if (!point) {
|
||||
path.open = true;
|
||||
paths.push(path);
|
||||
break;
|
||||
}
|
||||
// hit a point previously in the path (or start)
|
||||
if (point.del) {
|
||||
paths.push(sliceAtTerm(path,point));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i=0; i<stack.length; i++) stack[i].del = false;
|
||||
// stack.forEach(function(p) { p.del = false });
|
||||
}
|
||||
|
||||
// emit a polygon if it can be cleaned and still have 2 or more points
|
||||
function emit(poly) {
|
||||
poly = poly.clean();
|
||||
if (poly.length > 2) output.push(poly);
|
||||
}
|
||||
|
||||
// given an array of paths, emit longest to shortest
|
||||
// eliminating points from the paths as they are emitted
|
||||
// shorter paths any point eliminated are eliminated as candidates.
|
||||
function emitLongestAsPolygon(paths) {
|
||||
let longest = null,
|
||||
emitted = 0,
|
||||
closed = 0,
|
||||
open = 0;
|
||||
|
||||
paths.forEach(function(path) {
|
||||
// use longest perimeter vs longest path?
|
||||
if (!longest || path.length > longest.length) longest = path;
|
||||
if (!path.open) closed++; else open++;
|
||||
});
|
||||
|
||||
// it gets more complicated with multiple possible output paths
|
||||
if (closed > 1 && open === 0) {
|
||||
// add polygon to path (for area sorting)
|
||||
paths.forEach(function(path) { path.poly = BASE.newPolygon().addPoints(path) });
|
||||
|
||||
// sort descending by area VS (length below -- better in most cases)
|
||||
// paths.sort(function(a,b) { return b.poly.area() - a.poly.area() });
|
||||
|
||||
// sort descending by length
|
||||
paths.sort(function(a,b) { return b.poly.length - a.poly.length });
|
||||
|
||||
// emit polygons largest to smallest
|
||||
// omit polygon if it intersects previously emitted (has del points)
|
||||
paths.forEach(function(path) {
|
||||
if (path.length < 3) return;
|
||||
let len = path.length, i;
|
||||
for (i = 0; i < len; i++) if (path[i].del) return;
|
||||
for (i = 0; i < len; i++) path[i].del = true;
|
||||
emit(path.poly);
|
||||
emitted++;
|
||||
});
|
||||
} else {
|
||||
if (longest.open) {
|
||||
connect.push(longest);
|
||||
} else {
|
||||
emit(BASE.newPolygon().addPoints(longest));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create point map, unique point list and point group arrays
|
||||
input.forEach(function(line) {
|
||||
p1 = cachedPoint(line.p1.round(7));
|
||||
p2 = cachedPoint(line.p2.round(7));
|
||||
addConnected(p1,p2);
|
||||
addConnected(p2,p1);
|
||||
});
|
||||
|
||||
// first trace paths starting at dangling endpoinds (bad polygon soup)
|
||||
points.forEach(function(point) {
|
||||
// must not have been used and be a dangling end
|
||||
if (point.pos === 0 && point.group.length === 1) {
|
||||
let path = [],
|
||||
paths = [];
|
||||
findPathsMinRecurse(point, path, paths);
|
||||
if (paths.length > 0) emitLongestAsPolygon(paths);
|
||||
}
|
||||
});
|
||||
|
||||
// for each point, find longest path back to self
|
||||
points.forEach(function(point) {
|
||||
// must not have been used or be at a split
|
||||
if (point.pos === 0 && point.group.length === 2) {
|
||||
let path = [],
|
||||
paths = [];
|
||||
findPathsMinRecurse(point, path, paths);
|
||||
if (paths.length > 0) emitLongestAsPolygon(paths);
|
||||
}
|
||||
});
|
||||
|
||||
// return true if points are deemed "close enough" close a polygon
|
||||
function close(p1,p2) {
|
||||
return p1.distToSq2D(p2) <= 0.01;
|
||||
}
|
||||
|
||||
// reconnect dangling/open polygons to closest endpoint
|
||||
for (let i=0; i<connect.length; i++) {
|
||||
|
||||
let array = connect[i],
|
||||
last = array[array.length-1],
|
||||
tmp, dist, j;
|
||||
|
||||
if (!bridge) {
|
||||
emit(BASE.newPolygon().addPoints(array).setOpen());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (array.delete) continue;
|
||||
|
||||
loop: for (let merged=0;;) {
|
||||
let closest = { dist:Infinity };
|
||||
for (j=i+1; j<connect.length; j++) {
|
||||
tmp = connect[j];
|
||||
if (tmp.delete) continue;
|
||||
dist = last.distToSq2D(tmp[0]);
|
||||
if (dist < closest.dist && dist <= bridge) {
|
||||
closest = {
|
||||
dist: dist,
|
||||
array: tmp
|
||||
}
|
||||
}
|
||||
dist = last.distToSq2D(tmp[tmp.length-1]);
|
||||
if (dist < closest.dist && dist <= bridge) {
|
||||
closest = {
|
||||
dist: dist,
|
||||
array: tmp,
|
||||
reverse: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tmp = closest.array) {
|
||||
if (closest.reverse) tmp.reverse();
|
||||
tmp.delete = true;
|
||||
array.appendAll(tmp);
|
||||
last = array[array.length-1];
|
||||
merged++;
|
||||
// tail meets head (closed)
|
||||
if (close(array[0], last)) {
|
||||
emit(BASE.newPolygon().addPoints(array));
|
||||
break loop;
|
||||
}
|
||||
} else {
|
||||
// no more closest polys (open set)
|
||||
emit(BASE.newPolygon().addPoints(array));
|
||||
break loop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* eliminate duplicate lines and interior-only lines (coplanar)
|
||||
*
|
||||
* lines are sorted using lexicographic point keys such that
|
||||
* they are comparable even if their points are reversed. hinting
|
||||
* for deletion, co-planar and suspect shared edge is detectable at
|
||||
* this time.
|
||||
*
|
||||
* @param {Line[]} lines
|
||||
* @returns {Line[]}
|
||||
*/
|
||||
function removeDuplicateLines(lines) {
|
||||
let output = [],
|
||||
tmplines = [],
|
||||
points = [],
|
||||
pmap = {};
|
||||
|
||||
function cachePoint(p) {
|
||||
let cp = pmap[p.key];
|
||||
if (cp) return cp;
|
||||
points.push(p);
|
||||
pmap[p.key] = p;
|
||||
return p;
|
||||
}
|
||||
|
||||
function addLinesToPoint(point, line) {
|
||||
cachePoint(point);
|
||||
if (!point.group) point.group = [ line ];
|
||||
else point.group.push(line);
|
||||
}
|
||||
|
||||
// mark duplicates for deletion preserving edges
|
||||
lines.sort(function (l1, l2) {
|
||||
if (l1.key === l2.key) {
|
||||
l1.del = !l1.edge;
|
||||
l2.del = !l2.edge;
|
||||
return 0;
|
||||
}
|
||||
return l1.key < l2.key ? -1 : 1;
|
||||
});
|
||||
|
||||
// associate points with their lines, cull deleted
|
||||
lines.forEach(function(line) {
|
||||
if (!line.del) {
|
||||
tmplines.push(line);
|
||||
addLinesToPoint(line.p1, line);
|
||||
addLinesToPoint(line.p2, line);
|
||||
}
|
||||
});
|
||||
|
||||
// merge collinear lines
|
||||
points.forEach(function(point) {
|
||||
if (point.group.length != 2) return;
|
||||
let l1 = point.group[0],
|
||||
l2 = point.group[1];
|
||||
if (l1.isCollinear(l2)) {
|
||||
l1.del = true;
|
||||
l2.del = true;
|
||||
// find new endpoints that are not shared point
|
||||
let p1 = l1.p1 != point ? l1.p1 : l1.p2,
|
||||
p2 = l2.p1 != point ? l2.p1 : l2.p2,
|
||||
newline = base.newOrderedLine(p1,p2);
|
||||
// remove deleted lines from associated points
|
||||
p1.group.remove(l1);
|
||||
p1.group.remove(l2);
|
||||
p2.group.remove(l1);
|
||||
p2.group.remove(l2);
|
||||
// associate new line with points
|
||||
p1.group.push(newline);
|
||||
p2.group.push(newline);
|
||||
// add new line to lines array
|
||||
newline.edge = l1.edge || l2.edge;
|
||||
tmplines.push(newline);
|
||||
}
|
||||
});
|
||||
|
||||
// mark duplicates for deletion
|
||||
// but preserve one if it's an edge
|
||||
tmplines.sort(function (l1, l2) {
|
||||
if (l1.key === l2.key) {
|
||||
l1.del = true;
|
||||
l2.del = !l2.edge;
|
||||
return 0;
|
||||
}
|
||||
return l1.key < l2.key ? -1 : 1;
|
||||
});
|
||||
|
||||
// create new line array culling deleted
|
||||
tmplines.forEach(function(line) {
|
||||
if (!line.del) {
|
||||
output.push(line);
|
||||
line.p1.group = null;
|
||||
line.p2.group = null;
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
})();
|
||||
586
src/kiri/ui.js
Normal file
586
src/kiri/ui.js
Normal file
|
|
@ -0,0 +1,586 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (kiri.ui) return;
|
||||
|
||||
let SELF = self,
|
||||
KIRI = self.kiri,
|
||||
lastGroup = null,
|
||||
lastDiv = null,
|
||||
addTo = null,
|
||||
hideAction = null,
|
||||
inputAction = null,
|
||||
groups = {},
|
||||
groupShow = {},
|
||||
groupName = undefined,
|
||||
poppable = {},
|
||||
hasModes = [],
|
||||
isExpert = [],
|
||||
letMode = null,
|
||||
letExpert = false,
|
||||
DOC = SELF.document,
|
||||
state = {},
|
||||
prefix = "tab",
|
||||
NOMODE = "nomode",
|
||||
exitimer = undefined;
|
||||
|
||||
SELF.$ = (SELF.$ || function (id) { return DOC.getElementById(id) } );
|
||||
|
||||
KIRI.ui = {
|
||||
prefix: function(pre) { prefix = pre; return kiri.ui },
|
||||
hideAction: function(fn) { hideAction = fn; return kiri.ui },
|
||||
inputAction: function(fn) { inputAction = fn; return kiri.ui },
|
||||
setMode: setMode,
|
||||
setExpert: setExpert,
|
||||
bound: bound,
|
||||
toInt: toInt,
|
||||
toFloat: toFloat,
|
||||
hidePop: hidePop,
|
||||
isPopped: isPopped,
|
||||
newText: newTextArea,
|
||||
newLabel: newLabel,
|
||||
newRange: newRange,
|
||||
newInput: newInputField,
|
||||
newButton: newButton,
|
||||
newBoolean: newBooleanField,
|
||||
newSelect: newSelect,
|
||||
newTable: newTables,
|
||||
newTableRow: newTableRow,
|
||||
newRow: newRow,
|
||||
newBlank: newBlank,
|
||||
newGroup: newGroup,
|
||||
setGroup: setGroup,
|
||||
hidePoppers: hidePoppers,
|
||||
checkpoint,
|
||||
restore
|
||||
};
|
||||
|
||||
function setMode(mode) {
|
||||
Object.keys(groupShow).forEach(group => {
|
||||
updateGroupShow(group);
|
||||
});
|
||||
letMode = mode;
|
||||
hasModes.forEach(function(div) {
|
||||
div.setMode(div._group && !groupShow[div._group] ? NOMODE : mode);
|
||||
});
|
||||
hidePoppers();
|
||||
}
|
||||
|
||||
function setExpert(bool) {
|
||||
letExpert = bool;
|
||||
setMode(letMode);
|
||||
}
|
||||
|
||||
function checkpoint() {
|
||||
return { addTo, lastDiv, lastGroup, groupName };
|
||||
}
|
||||
|
||||
function restore(opt) {
|
||||
if (opt) {
|
||||
addTo = opt.addTo;
|
||||
lastDiv = opt.lastDiv;
|
||||
lastGroup = opt.lastGroup;
|
||||
groupName = opt.groupName;
|
||||
}
|
||||
}
|
||||
|
||||
// at present only used by the layers popup menu
|
||||
function setGroup(div) {
|
||||
addTo = lastDiv = div;
|
||||
groupName = undefined;
|
||||
lastGroup = [];
|
||||
return div;
|
||||
}
|
||||
|
||||
function newGroup(label, div, options) {
|
||||
lastDiv = div || lastDiv;
|
||||
return addCollapsableGroup(label, lastDiv, options);
|
||||
}
|
||||
|
||||
function addCollapsableGroup(label, div, options) {
|
||||
let opt = options || {};
|
||||
let group = opt.group || label;
|
||||
poppable[group] = opt.inline === true ? false : true;
|
||||
|
||||
let row = DOC.createElement('div'),
|
||||
a = DOC.createElement('a'),
|
||||
dbkey = `beta-${prefix}-show-${group}`,
|
||||
popper = !opt.inline;
|
||||
|
||||
if (popper) {
|
||||
let pop = DOC.createElement('div');
|
||||
pop.classList.add('set-pop');
|
||||
row.appendChild(pop);
|
||||
row.setAttribute("class", "set-group noselect");
|
||||
addTo = pop;
|
||||
} else {
|
||||
row.setAttribute("class", "set-header col");
|
||||
addTo = lastDiv;
|
||||
}
|
||||
|
||||
div.appendChild(row);
|
||||
row.appendChild(a);
|
||||
a.appendChild(DOC.createTextNode(label));
|
||||
addModeControls(row, opt);
|
||||
lastGroup = groups[group] = [];
|
||||
lastGroup.key = dbkey;
|
||||
groupName = group;
|
||||
groupShow[group] = popper ? state[dbkey] === 'true' : state[dbkey] !== 'false';
|
||||
row.onclick = function(ev) {
|
||||
if (ev.target !== a && ev.target !== row) {
|
||||
return;
|
||||
}
|
||||
toggleGroup(group, dbkey);
|
||||
};
|
||||
if (popper) {
|
||||
row.onmouseenter = function(ev) {
|
||||
if (ev.target !== a && ev.target !== row) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(exitimer);
|
||||
showGroup(group);
|
||||
};
|
||||
row.onmouseleave = function(ev) {
|
||||
if (ev.target !== a && ev.target !== row) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(exitimer);
|
||||
exitimer = setTimeout(showGroup, 1000);
|
||||
};
|
||||
addTo._group = group;
|
||||
addModeControls(addTo, opt);
|
||||
lastGroup.push(addTo);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
function hidePoppers() {
|
||||
showGroup(undefined);
|
||||
}
|
||||
|
||||
function showGroup(groupname) {
|
||||
Object.keys(groups).forEach(name => {
|
||||
let group = groups[name];
|
||||
groupShow[name] = state[group.key] = (groupname === name);
|
||||
updateGroupShow(name);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleGroup(groupname, dbkey) {
|
||||
let show = state[dbkey] === 'false';
|
||||
groupShow[groupname] = poppable[groupname] ? state[dbkey] = show : true;
|
||||
updateGroupShow(groupname);
|
||||
}
|
||||
|
||||
function updateGroupShow(groupname) {
|
||||
let show = groupShow[groupname] || !poppable[groupname];
|
||||
let group = groups[groupname];
|
||||
group.forEach(div => {
|
||||
if (show) div.setMode(letMode);
|
||||
else div.setMode(NOMODE);
|
||||
});
|
||||
}
|
||||
|
||||
function toInt() {
|
||||
let nv = this.value !== '' ? parseInt(this.value) : null;
|
||||
if (isNaN(nv)) nv = 0;
|
||||
if (nv !== null && this.bound) nv = this.bound(nv);
|
||||
this.value = nv;
|
||||
return nv;
|
||||
}
|
||||
|
||||
function toFloat() {
|
||||
let nv = this.value !== '' ? parseFloat(this.value) : null;
|
||||
if (nv !== null && this.bound) nv = this.bound(nv);
|
||||
this.value = nv;
|
||||
return nv;
|
||||
}
|
||||
|
||||
function bound(low,high) {
|
||||
return function(v) {
|
||||
if (isNaN(v)) return low;
|
||||
return v < low ? low : v > high ? high : v;
|
||||
};
|
||||
}
|
||||
|
||||
function raw() {
|
||||
return this.value !== '' ? this.value : null;
|
||||
}
|
||||
|
||||
function newLabel(text) {
|
||||
let label = DOC.createElement('label');
|
||||
label.appendChild(DOC.createTextNode(text));
|
||||
label.setAttribute("class", "noselect");
|
||||
return label;
|
||||
}
|
||||
|
||||
function addId(el, options) {
|
||||
if (options && options.id) {
|
||||
el.setAttribute("id", options.id);
|
||||
}
|
||||
}
|
||||
|
||||
function addModeControls(el, options) {
|
||||
options = options || {};
|
||||
el.__show = true;
|
||||
el.__modeSave = null;
|
||||
el.showMe = function() {
|
||||
if (el.__show) return;
|
||||
el.style.display = el.__modeSave;
|
||||
el.__show = true;
|
||||
el.__modeSave = null;
|
||||
};
|
||||
el.hideMe = function() {
|
||||
if (!el.__show) return;
|
||||
el.__show = false;
|
||||
el.__modeSave = el.style.display;
|
||||
el.style.display = 'none';
|
||||
};
|
||||
el.setVisible = function(show) {
|
||||
if (show) el.showMe();
|
||||
else el.hideMe();
|
||||
};
|
||||
el.setMode = function(mode) {
|
||||
let show = options.expert === undefined || (options.expert === letExpert);
|
||||
el.setVisible(el.hasMode(mode) && show);
|
||||
}
|
||||
el.hasMode = function(mode) {
|
||||
if (mode === NOMODE) return false;
|
||||
if (!el.modes) return true;
|
||||
return el.modes.contains(mode);
|
||||
}
|
||||
if (options.modes) {
|
||||
el.modes = options.modes;
|
||||
hasModes.push(el);
|
||||
}
|
||||
}
|
||||
|
||||
function newDiv(options) {
|
||||
let div = DOC.createElement('div');
|
||||
addModeControls(div, options);
|
||||
addTo.appendChild(div);
|
||||
lastGroup.push(div);
|
||||
div._group = groupName;
|
||||
return div;
|
||||
}
|
||||
|
||||
let lastTxt = null;
|
||||
let lastPop = null;
|
||||
let savePop = null;
|
||||
|
||||
function hidePop() {
|
||||
if (lastPop) {
|
||||
lastPop.style.display = "none";
|
||||
}
|
||||
if (savePop) {
|
||||
savePop();
|
||||
savePop = null;
|
||||
}
|
||||
lastPop = null;
|
||||
}
|
||||
|
||||
function isPopped() {
|
||||
return lastPop !== null;
|
||||
}
|
||||
|
||||
function newTextArea(label, options) {
|
||||
let opt = options || {},
|
||||
row = newDiv(options),
|
||||
btn = DOC.createElement("button"),
|
||||
pop = DOC.createElement("div"),
|
||||
txt = DOC.createElement("textarea"),
|
||||
area = opt.area;
|
||||
|
||||
txt.setAttribute("wrap", "off");
|
||||
txt.setAttribute("spellcheck", "false");
|
||||
txt.setAttribute("style", "resize: none");
|
||||
txt.onblur = inputAction;
|
||||
|
||||
btn.appendChild(DOC.createTextNode("edit"));
|
||||
|
||||
btn.onclick = function(ev) {
|
||||
ev.stopPropagation();
|
||||
if (ev.target === txt) {
|
||||
// drop clicks on TextArea
|
||||
ev.target.focus();
|
||||
} else {
|
||||
let fc = area.firstChild;
|
||||
if (fc) area.removeChild(fc);
|
||||
area.appendChild(txt);
|
||||
// first time, button click / show
|
||||
btn.parentNode.onclick = btn.onclick;
|
||||
txt.scrollTop = 0;
|
||||
txt.scrollLeft = 0;
|
||||
txt.selectionEnd = 0;
|
||||
let rows = txt.value.split('\n');
|
||||
let cols = 0;
|
||||
rows.forEach(row => {
|
||||
cols = Math.max(cols, row.length);
|
||||
});
|
||||
txt.setAttribute("cols", Math.max(30, cols + 1));
|
||||
txt.setAttribute("rows", Math.max(10, rows.length + 1));
|
||||
|
||||
let showing = pop === lastPop;
|
||||
hidePop();
|
||||
if (!showing) {
|
||||
if (lastTxt) {
|
||||
lastTxt.classList.remove('txt-sel');
|
||||
}
|
||||
row.classList.add('txt-sel');
|
||||
savePop = inputAction;
|
||||
pop.style.display = "flex";
|
||||
lastPop = pop;
|
||||
lastTxt = row;
|
||||
txt.focus();
|
||||
} else {
|
||||
inputAction();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addModeControls(btn, opt);
|
||||
addId(btn, opt);
|
||||
|
||||
row.appendChild(newLabel(label));
|
||||
row.appendChild(btn);
|
||||
row.setAttribute("class", "var-row");
|
||||
if (opt.title) row.setAttribute("title", options.title);
|
||||
btn.setVisible = row.setVisible;
|
||||
|
||||
return txt;
|
||||
}
|
||||
|
||||
function newInputField(label, options) {
|
||||
let opt = options || {},
|
||||
row = newDiv(opt),
|
||||
hide = opt && opt.hide,
|
||||
size = opt ? opt.size || 5 : 5,
|
||||
height = opt ? opt.height : 0,
|
||||
ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input'),
|
||||
action = opt.action || inputAction;
|
||||
|
||||
row.appendChild(newLabel(label));
|
||||
row.appendChild(ip);
|
||||
row.setAttribute("class", "var-row");
|
||||
if (height > 1) {
|
||||
ip.setAttribute("cols", size);
|
||||
ip.setAttribute("rows", height);
|
||||
ip.setAttribute("wrap", "off");
|
||||
} else {
|
||||
if (Number.isInteger(size)) {
|
||||
ip.setAttribute("size", size);
|
||||
} else {
|
||||
ip.setAttribute("style", `width:${size}`);
|
||||
}
|
||||
}
|
||||
ip.setAttribute("type", "text");
|
||||
ip.setAttribute("spellcheck", "false");
|
||||
row.style.display = hide ? 'none' : '';
|
||||
if (opt) {
|
||||
if (opt.disabled) ip.setAttribute("disabled", "true");
|
||||
if (opt.title) row.setAttribute("title", opt.title);
|
||||
if (opt.convert) ip.convert = opt.convert.bind(ip);
|
||||
if (opt.bound) ip.bound = opt.bound;
|
||||
if (opt.action) action = opt.action;
|
||||
}
|
||||
ip.addEventListener('focus', function(event) {
|
||||
hidePop();
|
||||
});
|
||||
if (action) {
|
||||
ip.addEventListener('keydown', function(event) {
|
||||
let key = event.key;
|
||||
if (
|
||||
opt.text ||
|
||||
(key >= '0' && key <= '9') ||
|
||||
key === '.' ||
|
||||
key === '-' ||
|
||||
key === 'Backspace' ||
|
||||
key === 'Delete' ||
|
||||
key === 'ArrowLeft' ||
|
||||
key === 'ArrowRight' ||
|
||||
key === 'Tab' ||
|
||||
event.metaKey ||
|
||||
event.ctrlKey ||
|
||||
(key === ',' && options.comma)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
ip.addEventListener('keyup', function(event) {
|
||||
if (event.keyCode === 13) {
|
||||
// action(event);
|
||||
ip.blur();
|
||||
}
|
||||
});
|
||||
ip.addEventListener('blur', function(event) {
|
||||
action(event);
|
||||
});
|
||||
}
|
||||
if (!ip.convert) ip.convert = raw.bind(ip);
|
||||
ip.setVisible = row.setVisible;
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
function newRange(label, options) {
|
||||
let row = newDiv(options),
|
||||
ip = DOC.createElement('input'),
|
||||
hide = options && options.hide,
|
||||
action = inputAction;
|
||||
|
||||
if (label) row.appendChild(newLabel(label));
|
||||
row.appendChild(ip);
|
||||
row.setAttribute("class", "var-row");
|
||||
ip.setAttribute("type", "range");
|
||||
ip.setAttribute("min", (options && options.min ? options.min : 0));
|
||||
ip.setAttribute("max", (options && options.max ? options.max : 100));
|
||||
ip.setAttribute("value", 0);
|
||||
row.style.display = hide ? 'none' : '';
|
||||
if (options) {
|
||||
if (options.title) {
|
||||
ip.setAttribute("title", options.title);
|
||||
row.setAttribute("title", options.title);
|
||||
}
|
||||
if (options.action) action = options.action;
|
||||
}
|
||||
ip.setVisible = row.setVisible;
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
function newSelect(label, options, source) {
|
||||
let row = newDiv(options),
|
||||
ip = DOC.createElement('select'),
|
||||
hide = options && options.hide,
|
||||
action = inputAction;
|
||||
|
||||
row.appendChild(newLabel(label));
|
||||
row.appendChild(ip);
|
||||
row.setAttribute("source", source || "tools");
|
||||
row.setAttribute("class", "var-row");
|
||||
row.style.display = hide ? 'none' : '';
|
||||
if (options) {
|
||||
if (options.convert) ip.convert = options.convert.bind(ip);
|
||||
if (options.disabled) ip.setAttribute("disabled", "true");
|
||||
if (options.title) row.setAttribute("title", options.title);
|
||||
if (options.action) action = options.action;
|
||||
}
|
||||
ip.onchange = function() { action() };
|
||||
ip.setVisible = row.setVisible;
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
function newBooleanField(label, action, options) {
|
||||
let row = newDiv(options),
|
||||
ip = DOC.createElement('input'),
|
||||
hide = options && options.hide;
|
||||
|
||||
if (label) {
|
||||
row.appendChild(newLabel(label));
|
||||
}
|
||||
row.appendChild(ip);
|
||||
row.setAttribute("class", "var-row");
|
||||
row.style.display = hide ? 'none' : '';
|
||||
ip.setAttribute("type", "checkbox");
|
||||
ip.checked = false;
|
||||
if (options) {
|
||||
if (options.disabled) ip.setAttribute("disabled", "true");
|
||||
if (options.title) {
|
||||
ip.setAttribute("title", options.title);
|
||||
row.setAttribute("title", options.title);
|
||||
}
|
||||
}
|
||||
if (action) ip.onclick = function() { action() };
|
||||
ip.setVisible = row.setVisible;
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
function newBlank(options) {
|
||||
let row = newDiv(options),
|
||||
hide = options && options.hide;
|
||||
|
||||
row.setAttribute("class", "var-row");
|
||||
row.style.display = hide ? 'none' : '';
|
||||
ip.setVisible = row.setVisible;
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
function newButton(label, action, options) {
|
||||
let opt = options || {},
|
||||
b = DOC.createElement('button');
|
||||
|
||||
b.onclick = function() {
|
||||
hidePoppers();
|
||||
if (action) action();
|
||||
};
|
||||
|
||||
if (opt.class) {
|
||||
opt.class.split(' ').forEach(ce => {
|
||||
b.classList.add(ce);
|
||||
});
|
||||
}
|
||||
if (opt.icon) {
|
||||
let d = DOC.createElement('div');
|
||||
d.innerHTML = opt.icon;
|
||||
b.appendChild(d);
|
||||
}
|
||||
if (label) {
|
||||
b.appendChild(DOC.createTextNode(label));
|
||||
}
|
||||
|
||||
addModeControls(b, options);
|
||||
addId(b, options);
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
function newTableRow(arrayOfArrays, options) {
|
||||
return newRow(newTables(arrayOfArrays), options);
|
||||
}
|
||||
|
||||
function newTables(arrayOfArrays) {
|
||||
let array = [];
|
||||
for (let i=0; i<arrayOfArrays.length; i++) {
|
||||
array.push(newRowTable(arrayOfArrays[i]));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
function newRowTable(array) {
|
||||
let div = newDiv();
|
||||
div.setAttribute("class", "table-row");
|
||||
array.forEach(function(c) {
|
||||
div.appendChild(c);
|
||||
});
|
||||
return div;
|
||||
}
|
||||
|
||||
function newRow(children, options) {
|
||||
let row = addCollapsableElement((options && options.noadd) ? null : addTo);
|
||||
if (children) children.forEach(function (c) { row.appendChild(c) });
|
||||
addModeControls(row, options);
|
||||
if (options && options.class) {
|
||||
options.class.split(' ').forEach(ce => {
|
||||
row.classList.add(ce);
|
||||
});
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
function addCollapsableElement(parent) {
|
||||
let row = newDiv();
|
||||
if (parent) parent.appendChild(row);
|
||||
if (lastGroup) lastGroup.push(row);
|
||||
return row;
|
||||
}
|
||||
|
||||
})();
|
||||
747
src/kiri/widget.js
Normal file
747
src/kiri/widget.js
Normal file
|
|
@ -0,0 +1,747 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (self.kiri.Widget) return;
|
||||
|
||||
const KIRI = self.kiri,
|
||||
DRIVERS = KIRI.driver,
|
||||
BASE = self.base,
|
||||
CONF = BASE.config,
|
||||
DBUG = BASE.debug,
|
||||
UTIL = BASE.util,
|
||||
POLY = BASE.polygons,
|
||||
MATH = Math,
|
||||
SQRT = MATH.sqrt,
|
||||
PRO = Widget.prototype,
|
||||
time = UTIL.time,
|
||||
solid_opacity = 1.0;
|
||||
|
||||
let nextId = 0,
|
||||
groups = [];
|
||||
|
||||
KIRI.Widget = Widget;
|
||||
KIRI.newWidget = newWidget;
|
||||
|
||||
function newWidget(id,group) { return new Widget(id,group) }
|
||||
|
||||
/** ******************************************************************
|
||||
* Group helpers
|
||||
******************************************************************* */
|
||||
|
||||
let Group = Widget.Groups = {
|
||||
|
||||
forid: function(id) {
|
||||
for (let i=0; i<groups.length; i++) {
|
||||
if (groups[i].id === id) return groups[i];
|
||||
}
|
||||
let group = [];
|
||||
group.id = id;
|
||||
groups.push(group);
|
||||
return group;
|
||||
},
|
||||
|
||||
remove: function(widget) {
|
||||
groups.slice().forEach(group => {
|
||||
let pos = group.indexOf(widget);
|
||||
if (pos >= 0) {
|
||||
group.splice(pos,1);
|
||||
}
|
||||
if (group.length === 0) {
|
||||
pos = groups.indexOf(group);
|
||||
groups.splice(pos,1);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
blocks: function() {
|
||||
return groups.map(group => {
|
||||
return {
|
||||
w: group[0].track.box.w,
|
||||
h: group[0].track.box.h,
|
||||
move: (x,y,z,abs) => {
|
||||
group.forEach(widget => {
|
||||
widget.mesh.material.visible = true;
|
||||
widget._move(x, y, z, abs);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
loadDone: function() {
|
||||
groups.forEach(group => {
|
||||
if (!group.centered) {
|
||||
group[0].center();
|
||||
group.centered = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
bounds: function(group) {
|
||||
let bounds = null;
|
||||
group.forEach(widget => {
|
||||
let wb = widget.mesh.getBoundingBox(true);
|
||||
if (bounds) {
|
||||
bounds = bounds.union(wb);
|
||||
} else {
|
||||
bounds = wb;
|
||||
}
|
||||
});
|
||||
return bounds;
|
||||
}
|
||||
};
|
||||
|
||||
/** ******************************************************************
|
||||
* Constructor
|
||||
******************************************************************* */
|
||||
|
||||
/**
|
||||
* @params {String} [id]
|
||||
* @constructor
|
||||
*/
|
||||
function Widget(id,group) {
|
||||
this.id = id || new Date().getTime().toString(36)+(nextId++);
|
||||
this.group = group || [];
|
||||
this.group.push(this);
|
||||
if (!this.group.id) {
|
||||
this.group.id = this.id;
|
||||
}
|
||||
if (groups.indexOf(this.group) < 0) {
|
||||
groups.push(this.group);
|
||||
}
|
||||
this.mesh = null;
|
||||
this.points = null;
|
||||
// todo resolve use of this vs. mesh.bounds
|
||||
this.bounds = null;
|
||||
this.wire = null;
|
||||
this.topo = null;
|
||||
this.slices = null;
|
||||
this.settings = null;
|
||||
this.modified = true;
|
||||
this.track = {
|
||||
// box size for packer
|
||||
box: {
|
||||
w: 0,
|
||||
h: 0,
|
||||
d: 0
|
||||
},
|
||||
scale: {
|
||||
x: 1.0,
|
||||
y: 1.0,
|
||||
z: 1.0
|
||||
},
|
||||
rot: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
},
|
||||
pos: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
},
|
||||
mirror: false
|
||||
},
|
||||
this.stats = {
|
||||
slice_time: 0,
|
||||
load_time: 0,
|
||||
progress: 0
|
||||
};
|
||||
this.saved = false;
|
||||
this.support = false; // is synthesized support widget
|
||||
}
|
||||
|
||||
/** ******************************************************************
|
||||
* Widget Class Functions
|
||||
******************************************************************* */
|
||||
|
||||
Widget.loadFromCatalog = function(filename, ondone) {
|
||||
KIRI.catalog.getFile(filename, function(data) {
|
||||
ondone(newWidget().loadVertices(data));
|
||||
});
|
||||
};
|
||||
|
||||
Widget.loadFromState = function(id, ondone, move) {
|
||||
KIRI.odb.get('ws-save-'+id, function(data) {
|
||||
if (data) {
|
||||
let vertices = data.geo || data,
|
||||
track = data.track || undefined,
|
||||
group = data.group || id,
|
||||
widget = newWidget(id, Group.forid(group));
|
||||
widget.saved = time();
|
||||
ondone(widget.loadVertices(vertices));
|
||||
// restore widget position if specified
|
||||
if (move && track && track.pos) {
|
||||
widget.track = track;
|
||||
widget.move(track.pos.x, track.pos.y, track.pos.z, true);
|
||||
}
|
||||
} else {
|
||||
ondone(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Widget.deleteFromState = function(id,ondone) {
|
||||
KIRI.odb.remove('ws-save-'+id, ondone);
|
||||
};
|
||||
|
||||
/**
|
||||
* converts a geometry point array into a kiri point array
|
||||
* with auto-decimation
|
||||
*
|
||||
* @param {Float32Array} array
|
||||
* @param {boolean} [decimate]
|
||||
* @returns {Array}
|
||||
*/
|
||||
Widget.verticesToPoints = function(array,decimate) {
|
||||
let parr = new Array(array.length / 3),
|
||||
i = 0,
|
||||
j = 0,
|
||||
t = time(),
|
||||
hash = {},
|
||||
unique = 0,
|
||||
passes = 0,
|
||||
points,
|
||||
oldpoints = parr.length,
|
||||
newpoints;
|
||||
// replace point objects with their equivalents
|
||||
while (i < array.length) {
|
||||
let p = BASE.newPoint(array[i++], array[i++], array[i++]),
|
||||
k = p.key,
|
||||
m = hash[k];
|
||||
if (!m) {
|
||||
m = p;
|
||||
hash[k] = p;
|
||||
unique++;
|
||||
}
|
||||
parr[j++] = m;
|
||||
}
|
||||
// decimate until all point spacing > precision_decimate
|
||||
while (parr.length > BASE.config.decimate_threshold && decimate && BASE.config.precision_decimate > 0.0) {
|
||||
let lines = [], line, dec = 0;
|
||||
for (i=0; i<oldpoints; ) {
|
||||
let p1 = parr[i++],
|
||||
p2 = parr[i++],
|
||||
p3 = parr[i++];
|
||||
lines.push( {p1:p1, p2:p2, d:SQRT(p1.distToSq3D(p2))} );
|
||||
lines.push( {p1:p1, p2:p3, d:SQRT(p1.distToSq3D(p3))} );
|
||||
lines.push( {p1:p2, p2:p3, d:SQRT(p2.distToSq3D(p3))} );
|
||||
}
|
||||
// sort by ascending line length
|
||||
lines.sort(function(a,b) {
|
||||
return a.d - b.d
|
||||
});
|
||||
// create offset mid-points
|
||||
for (i=0; i<lines.length; i++) {
|
||||
line = lines[i];
|
||||
if (line.d >= BASE.config.precision_decimate) break;
|
||||
if (line.p1.op || line.p2.op) continue;
|
||||
// todo skip dropping lines where either point is a "sharp" on 3 vectors
|
||||
line.p1.op = line.p2.op = line.p1.midPointTo3D(line.p2);
|
||||
dec++;
|
||||
}
|
||||
// exit if nothing to decimate
|
||||
if (dec === 0) break;
|
||||
passes++;
|
||||
// create new facets
|
||||
points = new Array(oldpoints);
|
||||
newpoints = 0;
|
||||
for (i=0; i<oldpoints; ) {
|
||||
let p1 = parr[i++],
|
||||
p2 = parr[i++],
|
||||
p3 = parr[i++];
|
||||
// drop facets with two offset points
|
||||
if (p1.op && p1.op === p2.op) continue;
|
||||
if (p1.op && p1.op === p3.op) continue;
|
||||
if (p2.op && p2.op === p3.op) continue;
|
||||
// otherwise emit altered facet
|
||||
points[newpoints++] = p1.op || p1;
|
||||
points[newpoints++] = p2.op || p2;
|
||||
points[newpoints++] = p3.op || p3;
|
||||
}
|
||||
parr = points.slice(0,newpoints);
|
||||
oldpoints = newpoints;
|
||||
}
|
||||
if (passes) DBUG.log({
|
||||
before: array.length / 3,
|
||||
after: parr.length,
|
||||
unique: unique,
|
||||
decimations: passes,
|
||||
time: (time() - t)
|
||||
});
|
||||
return parr;
|
||||
};
|
||||
|
||||
Widget.pointsToVertices = function(points) {
|
||||
let vertices = new Float32Array(points.length * 3),
|
||||
i = 0, vi = 0;
|
||||
while (i < points.length) {
|
||||
vertices[vi++] = points[i].x;
|
||||
vertices[vi++] = points[i].y;
|
||||
vertices[vi++] = points[i++].z;
|
||||
}
|
||||
return vertices;
|
||||
};
|
||||
|
||||
/** ******************************************************************
|
||||
* Widget Prototype Functions
|
||||
******************************************************************* */
|
||||
|
||||
PRO.saveToCatalog = function(filename) {
|
||||
let widget = this;
|
||||
let time = UTIL.time();
|
||||
KIRI.catalog.putFile(filename, this.getGeoVertices(), function(vertices) {
|
||||
if (vertices && vertices.length) {
|
||||
console.log("saving decimated mesh ["+vertices.length+"] time ["+(UTIL.time()-time)+"]");
|
||||
widget.loadVertices(vertices);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
PRO.saveState = function(ondone) {
|
||||
let widget = this;
|
||||
KIRI.odb.put('ws-save-'+this.id, {
|
||||
geo:widget.getGeoVertices(),
|
||||
track:widget.track,
|
||||
group:this.group.id
|
||||
}, function(result) {
|
||||
widget.saved = time();
|
||||
if (ondone) ondone();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Float32Array} vertices
|
||||
* @returns {Widget}
|
||||
*/
|
||||
PRO.loadVertices = function(vertices) {
|
||||
if (this.mesh) {
|
||||
this.mesh.geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
|
||||
this.mesh.geometry.computeFaceNormals();
|
||||
this.mesh.geometry.computeVertexNormals();
|
||||
this.points = null;
|
||||
return this;
|
||||
} else {
|
||||
let geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
|
||||
return this.loadGeometry(geometry);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {THREE.Geometry} geometry
|
||||
* @returns {Widget}
|
||||
*/
|
||||
PRO.loadGeometry = function(geometry) {
|
||||
let mesh = new THREE.Mesh(
|
||||
geometry,
|
||||
new THREE.MeshPhongMaterial({
|
||||
color: 0xffff00,
|
||||
specular: 0x181818,
|
||||
shininess: 100,
|
||||
transparent: true,
|
||||
opacity: solid_opacity
|
||||
})
|
||||
);
|
||||
// fix invalid normals
|
||||
geometry.computeFaceNormals();
|
||||
geometry.computeVertexNormals();
|
||||
// to fix mirroring of normals not working as expected
|
||||
mesh.material.side = THREE.DoubleSide;
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
mesh.widget = this;
|
||||
this.mesh = mesh;
|
||||
// invalidates points cache (like any scale/rotation)
|
||||
this.center(true);
|
||||
return this;
|
||||
};
|
||||
|
||||
PRO.groupBounds = function() {
|
||||
return Group.bounds(this.group);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Point[]} points
|
||||
* @returns {Widget}
|
||||
*/
|
||||
PRO.setPoints = function(points) {
|
||||
this.points = points || null;
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* remove slice data and their views
|
||||
*/
|
||||
PRO.clearSlices = function() {
|
||||
let slices = this.slices,
|
||||
mesh = this.mesh;
|
||||
if (slices) {
|
||||
slices.forEach(function(slice) {
|
||||
mesh.remove(slice.view);
|
||||
});
|
||||
this.slices = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {number} color
|
||||
*/
|
||||
PRO.setColor = function(color,settings) {
|
||||
if (Array.isArray(color)) {
|
||||
color = color[this.getExtruder(settings) % color.length];
|
||||
}
|
||||
let material = this.mesh.material;
|
||||
material.color.set(color);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
*/
|
||||
PRO.setOpacity = function(value) {
|
||||
let mesh = this.mesh;
|
||||
if (value <= 0.0) {
|
||||
mesh.material.transparent = solid_opacity < 1.0;
|
||||
mesh.material.opacity = solid_opacity;
|
||||
mesh.material.visible = false;
|
||||
} else if (UTIL.inRange(value, 0.0, solid_opacity)) {
|
||||
mesh.material.transparent = value < 1.0;
|
||||
mesh.material.opacity = value;
|
||||
mesh.material.visible = true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* center geometry bottom (on platform) at 0,0,0
|
||||
*/
|
||||
PRO.center = function(init) {
|
||||
let bb = init ? this.mesh.getBoundingBox(true) : this.groupBounds(),
|
||||
bm = bb.min.clone(),
|
||||
bM = bb.max.clone(),
|
||||
bd = bM.sub(bm).multiplyScalar(0.5),
|
||||
dx = bm.x + bd.x,
|
||||
dy = bm.y + bd.y,
|
||||
dz = bm.z;
|
||||
// move mesh for each widget in group
|
||||
if (!init) {
|
||||
this.group.forEach(w => {
|
||||
w.moveMesh(dx,dy,dz);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* called by center() and Group.center()
|
||||
*/
|
||||
PRO.moveMesh = function(x, y, z) {
|
||||
let gap = this.mesh.geometry.attributes.position,
|
||||
pa = gap.array;
|
||||
// center point array on 0,0,0
|
||||
for (let i=0; i < pa.length; i += 3) {
|
||||
pa[i ] -= x;
|
||||
pa[i + 1] -= y;
|
||||
pa[i + 2] -= z;
|
||||
}
|
||||
gap.needsUpdate = true;
|
||||
let bb = this.groupBounds();
|
||||
this.track.box = {
|
||||
w: (bb.max.x - bb.min.x),
|
||||
h: (bb.max.y - bb.min.y),
|
||||
d: (bb.max.z - bb.min.z)
|
||||
};
|
||||
// for use with the packer
|
||||
// invalidate cached points
|
||||
this.points = null;
|
||||
this.modified = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* moves top of widget to given Z
|
||||
* used in CAM mode
|
||||
*
|
||||
* @param {number} z position
|
||||
*/
|
||||
PRO.setTopZ = function(z) {
|
||||
let mesh = this.mesh,
|
||||
pos = this.track.pos;
|
||||
if (z) {
|
||||
pos.z = mesh.getBoundingBox().max.z - z;
|
||||
mesh.position.z = -pos.z - 0.01;
|
||||
} else {
|
||||
pos.z = 0;
|
||||
mesh.position.z = 0;
|
||||
}
|
||||
this.modified = true;
|
||||
}
|
||||
|
||||
PRO.move = function(x, y, z, abs) {
|
||||
this.group.forEach(w => {
|
||||
w._move(x, y, z, abs);
|
||||
});
|
||||
};
|
||||
|
||||
PRO._move = function(x, y, z, abs) {
|
||||
let mesh = this.mesh,
|
||||
pos = this.track.pos;
|
||||
// do not allow moves in pure slice view
|
||||
if (!mesh.material.visible) return;
|
||||
if (abs) {
|
||||
mesh.position.set(x,y,z);
|
||||
pos.x = (x || 0);
|
||||
pos.y = (y || 0);
|
||||
pos.z = (z || 0);
|
||||
} else {
|
||||
mesh.position.x += ( x || 0);
|
||||
mesh.position.y += ( y || 0);
|
||||
mesh.position.z += (-z || 0);
|
||||
pos.x += (x || 0);
|
||||
pos.y += (y || 0);
|
||||
pos.z += (z || 0);
|
||||
}
|
||||
if (x || y || z) {
|
||||
this.modified = true;
|
||||
KIRI.api.event.emit('widget.move', {widget: this, pos});
|
||||
}
|
||||
};
|
||||
|
||||
PRO.scale = function(x, y, z) {
|
||||
this.group.forEach(w => {
|
||||
w._scale(x, y, z);
|
||||
});
|
||||
this.center();
|
||||
};
|
||||
|
||||
PRO._scale = function(x, y, z) {
|
||||
let mesh = this.mesh,
|
||||
scale = this.track.scale;
|
||||
this.bounds = null;
|
||||
this.setWireframe(false);
|
||||
this.clearSlices();
|
||||
mesh.geometry.applyMatrix4(new THREE.Matrix4().makeScale(x, y, z));
|
||||
scale.x *= (x || 1.0);
|
||||
scale.y *= (y || 1.0);
|
||||
scale.z *= (z || 1.0);
|
||||
};
|
||||
|
||||
PRO.rotate = function(x, y, z) {
|
||||
this.group.forEach(w => {
|
||||
w._rotate(x, y, z, false);
|
||||
});
|
||||
this.center();
|
||||
};
|
||||
|
||||
PRO._rotate = function(x, y, z, temp) {
|
||||
if (!temp) {
|
||||
this.bounds = null;
|
||||
this.setWireframe(false);
|
||||
this.clearSlices();
|
||||
}
|
||||
let m4 = new THREE.Matrix4();
|
||||
let euler = typeof(x) === 'number';
|
||||
if (euler) {
|
||||
m4 = m4.makeRotationFromEuler(new THREE.Euler(x || 0, y || 0, z || 0));
|
||||
} else {
|
||||
m4 = m4.makeRotationFromQuaternion(x);
|
||||
}
|
||||
this.mesh.geometry.applyMatrix4(m4);
|
||||
if (!temp && euler) {
|
||||
let rot = this.track.rot;
|
||||
rot.x += (x || 0);
|
||||
rot.y += (y || 0);
|
||||
rot.z += (z || 0);
|
||||
}
|
||||
};
|
||||
|
||||
PRO.mirror = function() {
|
||||
this.group.forEach(w => {
|
||||
w._mirror();
|
||||
});
|
||||
this.center();
|
||||
};
|
||||
|
||||
PRO._mirror = function() {
|
||||
this.setWireframe(false);
|
||||
this.clearSlices();
|
||||
let i,
|
||||
o = this.track,
|
||||
geo = this.mesh.geometry,
|
||||
at = geo.attributes,
|
||||
pa = at.position.array,
|
||||
nm = at.normal.array;
|
||||
for (i = 0 ; i < pa.length; i += 3) {
|
||||
pa[i] = -pa[i];
|
||||
nm[i] = -nm[i];
|
||||
}
|
||||
geo.computeFaceNormals();
|
||||
geo.computeVertexNormals();
|
||||
o.mirror = !o.mirror;
|
||||
};
|
||||
|
||||
PRO.getGeoVertices = function() {
|
||||
return this.mesh.geometry.getAttribute('position').array;
|
||||
};
|
||||
|
||||
PRO.getPoints = function() {
|
||||
if (!this.points) {
|
||||
// convert and cache points from geometry vertices
|
||||
this.points = Widget.verticesToPoints(this.getGeoVertices());
|
||||
}
|
||||
return this.points;
|
||||
};
|
||||
|
||||
PRO.getBoundingBox = function(refresh) {
|
||||
if (!this.bounds || refresh) {
|
||||
this.bounds = new THREE.Box3();
|
||||
this.bounds.setFromPoints(this.getPoints());
|
||||
}
|
||||
return this.bounds;
|
||||
};
|
||||
|
||||
PRO.isModified = function() {
|
||||
return this.modified;
|
||||
};
|
||||
|
||||
PRO.getExtruder = function(settings) {
|
||||
if (settings && settings.widget) {
|
||||
let rec = settings.widget[this.id];
|
||||
return rec && rec.extruder >= 0 ? rec.extruder : 0;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* processes points into facets, then into slices
|
||||
*
|
||||
* once upon a time there were multiple slicers. this was the fastest in most cases.
|
||||
* lines are added to all the buckets they cross. then buckets are processed in order.
|
||||
* buckets are contiguous ranges of z slicers. the advantage of this method is that
|
||||
* as long as a large percentage of lines do not cross large z distances, this reduces
|
||||
* the number of lines each slice has to consider thus improving speed.
|
||||
*
|
||||
* @params {Object} settings
|
||||
* @params {Function} [ondone]
|
||||
* @params {Function} [onupdate]
|
||||
* @params {boolean} [remote]
|
||||
*/
|
||||
PRO.slice = function(settings, ondone, onupdate, remote) {
|
||||
let widget = this,
|
||||
startTime = UTIL.time();
|
||||
|
||||
widget.settings = settings;
|
||||
widget.clearSlices();
|
||||
onupdate(0.0001, "slicing");
|
||||
|
||||
if (remote) {
|
||||
// in case result of slice is nothing, do not preserve previous
|
||||
widget.slices = []
|
||||
|
||||
// executed from kiri.js
|
||||
KIRI.work.slice(settings, this, function (reply) {
|
||||
if (reply.update) {
|
||||
onupdate(reply.update, reply.updateStatus);
|
||||
}
|
||||
if (reply.send_start) {
|
||||
widget.xfer = {start: reply.send_start};
|
||||
}
|
||||
if (reply.topo) {
|
||||
widget.topo = reply.topo;
|
||||
}
|
||||
if (reply.stats) {
|
||||
widget.stats = reply.stats;
|
||||
}
|
||||
if (reply.send_end) {
|
||||
widget.stats.load_time = widget.xfer.start - reply.send_end;
|
||||
}
|
||||
if (reply.slice) {
|
||||
widget.slices.push(KIRI.codec.decode(reply.slice, {mesh:widget.mesh}));
|
||||
}
|
||||
if (reply.polish) {
|
||||
widget.polish = KIRI.codec.decode(reply.polish);
|
||||
}
|
||||
if (reply.error) {
|
||||
ondone(false, reply.error);
|
||||
}
|
||||
if (reply.done) {
|
||||
widget.modified = false;
|
||||
ondone(true);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
// executed from kiri-worker.js
|
||||
let catchdone = function(error) {
|
||||
if (error) {
|
||||
return ondone(error);
|
||||
}
|
||||
|
||||
onupdate(1.0, "transferring");
|
||||
|
||||
widget.stats.slice_time = UTIL.time() - startTime;
|
||||
widget.modified = false;
|
||||
|
||||
ondone();
|
||||
};
|
||||
|
||||
let catchupdate = function(progress, message) {
|
||||
onupdate(progress, message);
|
||||
};
|
||||
|
||||
let driver = DRIVERS[settings.mode.toUpperCase()];
|
||||
|
||||
if (driver) {
|
||||
driver.slice(settings, widget, catchupdate, catchdone);
|
||||
} else {
|
||||
DBUG.log('invalid mode: '+settings.mode);
|
||||
ondone('invalid mode: '+settings.mode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PRO.getCamBounds = function(settings) {
|
||||
let bounds = this.getBoundingBox().clone();
|
||||
bounds.max.z += settings.process.camZTopOffset;
|
||||
return bounds;
|
||||
};
|
||||
|
||||
/**
|
||||
* render all slice and processed data
|
||||
*/
|
||||
PRO.render = function() {
|
||||
DRIVERS[this.settings.mode.toUpperCase()].sliceRender(this);
|
||||
};
|
||||
|
||||
PRO.hideSlices = function() {
|
||||
let showing = false;
|
||||
if (this.slices) this.slices.forEach(function(slice) {
|
||||
showing = showing || slice.view.visible;
|
||||
slice.view.visible = false;
|
||||
});
|
||||
return showing;
|
||||
};
|
||||
|
||||
PRO.toggleWireframe = function (color, opacity) {
|
||||
this.setWireframe(!this.wire, color, opacity);
|
||||
};
|
||||
|
||||
PRO.setWireframe = function(set, color, opacity) {
|
||||
let mesh = this.mesh,
|
||||
widget = this;
|
||||
if (this.wire) {
|
||||
mesh.remove(this.wire);
|
||||
this.wire = null;
|
||||
this.setOpacity(solid_opacity);
|
||||
this.hideSlices();
|
||||
}
|
||||
if (set) {
|
||||
widget.wire = base.render.wireframe(mesh, this.getPoints(), color);
|
||||
widget.setOpacity(opacity);
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
176
src/kiri/work.js
Normal file
176
src/kiri/work.js
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
// this code runs in kiri's main loop
|
||||
if (!self.kiri) self.kiri = {};
|
||||
|
||||
let loc = self.location,
|
||||
host = loc.hostname,
|
||||
port = loc.port,
|
||||
proto = loc.protocol,
|
||||
time = function() { return new Date().getTime() },
|
||||
KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
seqid = 1,
|
||||
running = {},
|
||||
slicing = {},
|
||||
worker = null;
|
||||
|
||||
function send(fn, data, onreply, async, zerocopy) {
|
||||
let seq = seqid++;
|
||||
|
||||
running[seq] = {fn:onreply, async:async||false};
|
||||
// console.log('send', data);
|
||||
worker.postMessage({
|
||||
seq: seq,
|
||||
task: fn,
|
||||
time: time(),
|
||||
data: data
|
||||
}, zerocopy);
|
||||
}
|
||||
|
||||
KIRI.work = {
|
||||
isSlicing : function() {
|
||||
let current = 0;
|
||||
for (let key in slicing) {
|
||||
current++;
|
||||
}
|
||||
return current > 0;
|
||||
},
|
||||
|
||||
restart : function() {
|
||||
if (worker) {
|
||||
worker.terminate();
|
||||
}
|
||||
|
||||
for (let key in slicing) {
|
||||
slicing[key]({error: "cancelled slicing"});
|
||||
}
|
||||
|
||||
slicing = {};
|
||||
running = {};
|
||||
worker = new Worker(`/code/worker.js?${self.kiri.version}`);
|
||||
|
||||
worker.onmessage = function(e) {
|
||||
let now = time(),
|
||||
reply = e.data,
|
||||
record = running[reply.seq],
|
||||
onreply = record.fn;
|
||||
// console.log('recv', reply.data)
|
||||
if (reply.done) {
|
||||
delete running[reply.seq];
|
||||
}
|
||||
|
||||
// calculate and replace recv time
|
||||
reply.time_recv = now - reply.time_recv;
|
||||
|
||||
onreply(reply.data, reply);
|
||||
};
|
||||
},
|
||||
|
||||
decimate : function(vertices, callback) {
|
||||
vertices = vertices.buffer.slice(0);
|
||||
send("decimate", vertices, function(output) {
|
||||
callback(output);
|
||||
});
|
||||
},
|
||||
|
||||
clear : function(widget) {
|
||||
send("clear", widget ? {id:widget.id} : {}, function(reply) {
|
||||
// console.log({clear:reply});
|
||||
});
|
||||
},
|
||||
|
||||
snap : function(data) {
|
||||
send("snap", data, function(reply) {
|
||||
// console.log({snap:reply})
|
||||
});
|
||||
},
|
||||
|
||||
slice : function(settings, widget, callback) {
|
||||
let rotation = (Math.PI/180) * (settings.process.sliceRotation || 0);
|
||||
let centerz;
|
||||
let movez;
|
||||
if (rotation) {
|
||||
let bbox1 = widget.getBoundingBox(true);
|
||||
widget._rotate(0,rotation,0,true);
|
||||
widget.center();
|
||||
let bbox2 = widget.getBoundingBox(true);
|
||||
centerz = (bbox2.max.z - bbox2.min.z)/2;
|
||||
movez = centerz - (bbox1.max.z - bbox1.min.z)/2;
|
||||
}
|
||||
let vertices = widget.getGeoVertices().buffer.slice(0),
|
||||
snapshot = KIRI.api.view.snapshot;
|
||||
if (rotation) {
|
||||
widget._rotate(0,-rotation,0,true);
|
||||
widget.center();
|
||||
}
|
||||
slicing[widget.id] = callback;
|
||||
send("slice", {
|
||||
id: widget.id,
|
||||
settings: settings,
|
||||
vertices: vertices,
|
||||
position: widget.mesh.position,
|
||||
tracking: widget.track,
|
||||
snapshot: snapshot,
|
||||
// for rotation / unrotation
|
||||
state: {
|
||||
rotation: rotation,
|
||||
centerz: centerz,
|
||||
movez: movez
|
||||
}
|
||||
}, function(reply) {
|
||||
if (reply.done || reply.error) delete slicing[widget.id];
|
||||
callback(reply);
|
||||
}, null, [vertices]);
|
||||
},
|
||||
|
||||
printSetup : function(settings, callback) {
|
||||
send("printSetup", {settings:settings}, function(reply) {
|
||||
callback(reply);
|
||||
}, undefined);
|
||||
},
|
||||
|
||||
printExport : function(settings, online, ondone) {
|
||||
let lines = [];
|
||||
send("printExport", {settings:settings}, function(reply) {
|
||||
if (reply.line) {
|
||||
online(reply.line);
|
||||
}
|
||||
if (reply.done) {
|
||||
ondone(reply.done);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
printGCode : function(callback) {
|
||||
let gcode = [];
|
||||
let start = BASE.util.time();
|
||||
send("printGCode", {}, function(reply) {
|
||||
if (reply.line) {
|
||||
gcode.push(reply.line);
|
||||
} else {
|
||||
if (!reply.gcode) reply.gcode = gcode.join("\n");
|
||||
// console.log({printGCode:(BASE.util.time() - start)});
|
||||
callback(reply);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
sliceToGCode : function(settings, vertices, callback) {
|
||||
vertices = widget.getGeoVertices().buffer.slice(0);
|
||||
let wid = new Date().toString(36);
|
||||
send("slice", {settings:settings, id:wiwd, vertices:vertices, position:{x:0,y:0,z:0}}, function(reply) {
|
||||
send("printSetup", {settings:settings}, function(reply) {
|
||||
send("printGCode", {}, function(reply) {
|
||||
callback(reply);
|
||||
});
|
||||
}, null, [vertices]);
|
||||
callback(reply);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// start worker
|
||||
KIRI.work.restart();
|
||||
208
src/kiri/worker.js
Normal file
208
src/kiri/worker.js
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
const base = self.base,
|
||||
util = base.util,
|
||||
time = util.time,
|
||||
kiri = self.kiri,
|
||||
ver = kiri.version,
|
||||
Widget = kiri.Widget;
|
||||
|
||||
let currentPrint,
|
||||
currentSnap,
|
||||
cache = {};
|
||||
|
||||
console.log(`kiri | init work | ${ver}`);
|
||||
base.debug.disable();
|
||||
|
||||
// catch clipper alerts and convert to console messages
|
||||
self.alert = function(o) {
|
||||
console.log(o);
|
||||
};
|
||||
|
||||
let dispatch = {
|
||||
decimate: function(vertices, send) {
|
||||
vertices = new Float32Array(vertices),
|
||||
vertices = Widget.pointsToVertices(Widget.verticesToPoints(vertices, true));
|
||||
send.done(vertices);
|
||||
},
|
||||
|
||||
snap: function(data, send) {
|
||||
currentSnap = data;
|
||||
send.done();
|
||||
},
|
||||
|
||||
slice: function(data, send) {
|
||||
let settings = data.settings,
|
||||
vertices = new Float32Array(data.vertices),
|
||||
position = data.position,
|
||||
tracking = data.tracking,
|
||||
points = Widget.verticesToPoints(vertices),
|
||||
state = data.state || {},
|
||||
rotation = state.rotation,
|
||||
centerz = state.centerz,
|
||||
movez = state.movez;
|
||||
|
||||
if (rotation) {
|
||||
state.rotate = new THREE.Matrix4().makeRotationY(-rotation);
|
||||
}
|
||||
|
||||
send.data({update:0.05, updateStatus:"slicing"});
|
||||
|
||||
let widget = kiri.newWidget(data.id).setPoints(points),
|
||||
last = util.time(),
|
||||
now;
|
||||
|
||||
// do it here so cancel can work
|
||||
cache[data.id] = widget;
|
||||
|
||||
// fake mesh object to satisfy printing
|
||||
widget.track = tracking;
|
||||
widget.mesh = {
|
||||
widget: widget,
|
||||
position: position
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
widget.slice(settings, function(error) {
|
||||
if (error) {
|
||||
delete cache[data.id];
|
||||
send.data({error: error});
|
||||
} else {
|
||||
let slices = widget.slices || [];
|
||||
send.data({send_start: time()});
|
||||
send.data({
|
||||
topo: settings.synth.sendTopo ? widget.topo : null,
|
||||
stats: widget.stats,
|
||||
slices: slices.length
|
||||
});
|
||||
slices.forEach(function(slice,index) {
|
||||
send.data({index: index, slice: slice.encode(state)});
|
||||
})
|
||||
if (self.debug && widget.polish) {
|
||||
send.data({polish: kiri.codec.encode(widget.polish)});
|
||||
}
|
||||
send.data({send_end: time()});
|
||||
}
|
||||
send.done({done: true});
|
||||
}, function(update, msg) {
|
||||
now = util.time();
|
||||
if (now - last < 10 && update < 0.99) return;
|
||||
// on update
|
||||
send.data({update: (0.05 + update * 0.95), updateStatus: msg});
|
||||
last = now;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
send.data({error: error.toString()});
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
|
||||
printSetup: function(data, send) {
|
||||
let widgets = [], key;
|
||||
for (key in cache) {
|
||||
if (cache.hasOwnProperty(key)) widgets.push(cache[key]);
|
||||
}
|
||||
|
||||
send.data({update:0.05, updateStatus:"preview"});
|
||||
|
||||
currentPrint = kiri.newPrint(data.settings, widgets, data.id);
|
||||
currentPrint.setup(false, function(update, msg) {
|
||||
send.data({
|
||||
update: update,
|
||||
updateStatus: msg
|
||||
});
|
||||
}, function() {
|
||||
send.done({
|
||||
done: true,
|
||||
output: currentPrint.encodeOutput()
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
printExport: function(data, send) {
|
||||
currentPrint.export(false, function(line, direct) {
|
||||
send.data({line}, direct);
|
||||
}, function(done, direct) {
|
||||
send.done({done}, direct);
|
||||
});
|
||||
},
|
||||
|
||||
printGCode: function(data, send) {
|
||||
currentPrint.exportGCode(false, function(gcode) {
|
||||
send.done({
|
||||
gcode: gcode,
|
||||
lines: currentPrint.lines,
|
||||
bytes: currentPrint.bytes,
|
||||
bounds: currentPrint.bounds,
|
||||
distance: currentPrint.distance,
|
||||
time: currentPrint.time
|
||||
});
|
||||
}, function(line) {
|
||||
send.data({line:line});
|
||||
});
|
||||
},
|
||||
|
||||
clear: function(data, send) {
|
||||
currentSnap = null;
|
||||
currentPrint = null;
|
||||
if (!data.id) {
|
||||
cache = {};
|
||||
send.done({ clear: true });
|
||||
return;
|
||||
}
|
||||
let had = cache[data.id] !== undefined;
|
||||
delete cache[data.id];
|
||||
send.done({
|
||||
id: data.id,
|
||||
had: had,
|
||||
has: cache[data.id] !== undefined
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
self.onmessage = function(e) {
|
||||
let time_recv = util.time(),
|
||||
msg = e.data,
|
||||
run = dispatch[msg.task],
|
||||
send = {
|
||||
data : function(data,direct) {
|
||||
self.postMessage({
|
||||
seq: msg.seq,
|
||||
task: msg.task,
|
||||
done: false,
|
||||
data: data
|
||||
},direct);
|
||||
},
|
||||
done : function(data,direct) {
|
||||
self.postMessage({
|
||||
seq: msg.seq,
|
||||
task: msg.task,
|
||||
done: true,
|
||||
data: data
|
||||
},direct);
|
||||
}
|
||||
};
|
||||
|
||||
if (run) {
|
||||
let time_xfer = (time_recv - msg.time),
|
||||
output = run(msg.data, send),
|
||||
time_send = util.time(),
|
||||
time_proc = time_send - time_recv;
|
||||
|
||||
if (output) self.postMessage({
|
||||
seq: msg.seq,
|
||||
task: msg.task,
|
||||
time_send: time_xfer,
|
||||
time_proc: time_proc,
|
||||
// replaced on reply side
|
||||
time_recv: util.time(),
|
||||
data: output
|
||||
});
|
||||
} else {
|
||||
console.log({kiri_msg:e});
|
||||
}
|
||||
};
|
||||
Loading…
Reference in a new issue