split fdm and sla drivers
This commit is contained in:
parent
0b2a4599e7
commit
3ef015287a
14 changed files with 2975 additions and 2895 deletions
8
app.js
8
app.js
|
|
@ -263,8 +263,11 @@ const script = {
|
|||
"kiri/slice",
|
||||
// "kiri/slicer",
|
||||
"mode/fdm/driver",
|
||||
"mode/fdm/client",
|
||||
"mode/sla/driver",
|
||||
"mode/sla/client",
|
||||
"mode/cam/driver",
|
||||
"mode/cam/client",
|
||||
"mode/laser/driver",
|
||||
"kiri/layer",
|
||||
"kiri/widget",
|
||||
|
|
@ -303,7 +306,12 @@ const script = {
|
|||
"kiri/slicer",
|
||||
"kiri/slicer2",
|
||||
"mode/fdm/driver",
|
||||
"mode/fdm/slice",
|
||||
"mode/fdm/prepare",
|
||||
"mode/fdm/export",
|
||||
"mode/sla/driver",
|
||||
"mode/sla/slice",
|
||||
"mode/sla/export",
|
||||
"mode/cam/driver",
|
||||
"mode/cam/tool",
|
||||
"mode/cam/topo",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
|
||||
if (!self.kiri) {
|
||||
self.kiri = {
|
||||
loader: [] // module loading: array of functions
|
||||
driver: {}, // driver modules
|
||||
loader: [] // module loading: array of functions
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -248,18 +248,18 @@
|
|||
|
||||
// more generic. only used by SLA at the moment
|
||||
PRO.export = function(remote, online, ondone) {
|
||||
let driver = KIRI.driver[this.settings.mode];
|
||||
|
||||
if (!(driver && driver.printExport)) {
|
||||
console.log({missing_export_driver: mode});
|
||||
ondone(null);
|
||||
return;
|
||||
}
|
||||
let mode = this.settings.mode;
|
||||
let driver = KIRI.driver[mode];
|
||||
|
||||
if (remote) {
|
||||
// executed from kiri.js
|
||||
KIRI.work.printExport(this.settings, online, ondone);
|
||||
} else {
|
||||
if (!(driver && driver.printExport)) {
|
||||
console.log({missing_export_driver: mode});
|
||||
ondone(null);
|
||||
return;
|
||||
}
|
||||
// executed from kiri-worker.js
|
||||
driver.printExport(this, online, ondone);
|
||||
}
|
||||
|
|
|
|||
99
src/mode/cam/client.js
Normal file
99
src/mode/cam/client.js
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
let KIRI = self.kiri,
|
||||
CAM = KIRI.driver.CAM,
|
||||
PRO = CAM.process;
|
||||
|
||||
CAM.init = function(kiri, api) {
|
||||
api.event.on("mode.set", (mode) => {
|
||||
let isCAM = mode === 'CAM';
|
||||
$('set-tools').style.display = isCAM ? '' : 'none';
|
||||
kiri.space.platform.setColor(isCAM ? 0xeeeeee : 0xcccccc);
|
||||
});
|
||||
api.event.on("settings.saved", (settings) => {
|
||||
let proc = settings.process;
|
||||
api.ui.camTabs.marker.style.display = proc.camTabsOn ? 'flex' : 'none';
|
||||
api.ui.camRough.marker.style.display = proc.camRoughOn ? 'flex' : 'none';
|
||||
api.ui.camDrill.marker.style.display =
|
||||
proc.camDrillingOn || proc.camDrillReg !== 'none' ? 'flex' : 'none';
|
||||
api.ui.camOutline.marker.style.display = proc.camOutlineOn ? 'flex' : 'none';
|
||||
api.ui.camContour.marker.style.display =
|
||||
proc.camContourXOn || proc.camContourYOn ? 'flex' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
CAM.sliceRender = function(widget) {
|
||||
let slices = widget.slices;
|
||||
if (!slices) return;
|
||||
|
||||
slices.forEach(function(slice) {
|
||||
let tops = slice.tops,
|
||||
layers = slice.layers,
|
||||
outline = layers.outline,
|
||||
open = (slice.camMode === PRO.CONTOUR_X || slice.camMode === PRO.CONTOUR_Y);
|
||||
|
||||
layers.outline.clear(); // slice raw edges
|
||||
layers.trace.clear(); // roughing
|
||||
layers.solid.clear(); // outline
|
||||
layers.bridge.clear(); // outline x
|
||||
layers.flat.clear(); // outline y
|
||||
layers.fill.clear(); // facing
|
||||
|
||||
tops.forEach(function(top) {
|
||||
outline.poly(top.poly, 0x999900, true, open);
|
||||
// if (top.inner) outline.poly(top.inner, 0xdddddd, true);
|
||||
if (top.inner) outline.poly(top.inner, 0xff0000, true);
|
||||
});
|
||||
|
||||
// various outlining
|
||||
let layer;
|
||||
slice.tops.forEach(function(top) {
|
||||
switch (slice.camMode) {
|
||||
case PRO.OUTLINE:
|
||||
layer = layers.solid;
|
||||
break;
|
||||
case PRO.CONTOUR_X:
|
||||
layer = layers.bridge;
|
||||
break;
|
||||
case PRO.CONTOUR_Y:
|
||||
layer = layers.flat;
|
||||
break;
|
||||
default: // roughing
|
||||
layer = layers.trace;
|
||||
break;
|
||||
}
|
||||
if (top.traces) {
|
||||
layer.poly(top.traces, 0x010101, true, null);
|
||||
}
|
||||
});
|
||||
|
||||
// facing (previously separate. now part of roughing)
|
||||
layer = slice.layers.fill;
|
||||
slice.tops.forEach(function(top) {
|
||||
if (top.fill_lines) {
|
||||
layer.lines(top.fill_lines, fill_color);
|
||||
}
|
||||
});
|
||||
|
||||
outline.render();
|
||||
layers.trace.render();
|
||||
layers.solid.render();
|
||||
layers.bridge.render();
|
||||
layers.flat.render();
|
||||
layers.fill.render();
|
||||
});
|
||||
}
|
||||
|
||||
CAM.printRender = function(print) {
|
||||
return KIRI.driver.FDM.printRender(print, {
|
||||
aslines: true,
|
||||
color: 0x010101,
|
||||
move_color: 0xcc3333
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
|
|
@ -4,20 +4,14 @@
|
|||
|
||||
(function() {
|
||||
|
||||
if (!self.kiri.driver) self.kiri.driver = { };
|
||||
if (self.kiri.driver.CAM) return;
|
||||
|
||||
const KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
POLY = BASE.polygons,
|
||||
let KIRI = self.kiri,
|
||||
CAM = KIRI.driver.CAM = {
|
||||
init,
|
||||
// init, // src/mode/cam/client.js
|
||||
// sliceRender, // src/mode/cam/client.js
|
||||
// printRender // src/mode/cam/client.js
|
||||
// slice, // src/mode/cam/slice.js
|
||||
sliceRender,
|
||||
// printSetup, // src/mode/cam/prepare.js
|
||||
// printExport, // src/mode/cam/export.js
|
||||
printRender
|
||||
},
|
||||
CPRO = CAM.process = {
|
||||
LEVEL: 1,
|
||||
|
|
@ -27,96 +21,7 @@
|
|||
CONTOUR_Y: 5,
|
||||
TRACE: 6,
|
||||
DRILL: 7
|
||||
},
|
||||
newLine = BASE.newLine,
|
||||
newSlice = KIRI.newSlice,
|
||||
newPoint = BASE.newPoint,
|
||||
newPolygon = BASE.newPolygon;
|
||||
|
||||
function init(kiri, api) {
|
||||
api.event.on("mode.set", (mode) => {
|
||||
let isCAM = mode === 'CAM';
|
||||
$('set-tools').style.display = isCAM ? '' : 'none';
|
||||
kiri.space.platform.setColor(isCAM ? 0xeeeeee : 0xcccccc);
|
||||
});
|
||||
api.event.on("settings.saved", (settings) => {
|
||||
let proc = settings.process;
|
||||
api.ui.camTabs.marker.style.display = proc.camTabsOn ? 'flex' : 'none';
|
||||
api.ui.camRough.marker.style.display = proc.camRoughOn ? 'flex' : 'none';
|
||||
api.ui.camDrill.marker.style.display =
|
||||
proc.camDrillingOn || proc.camDrillReg !== 'none' ? 'flex' : 'none';
|
||||
api.ui.camOutline.marker.style.display = proc.camOutlineOn ? 'flex' : 'none';
|
||||
api.ui.camContour.marker.style.display =
|
||||
proc.camContourXOn || proc.camContourYOn ? 'flex' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// runs in browser main
|
||||
function sliceRender(widget) {
|
||||
let slices = widget.slices;
|
||||
if (!slices) return;
|
||||
|
||||
slices.forEach(function(slice) {
|
||||
let tops = slice.tops,
|
||||
layers = slice.layers,
|
||||
outline = layers.outline,
|
||||
open = (slice.camMode === CPRO.CONTOUR_X || slice.camMode === CPRO.CONTOUR_Y);
|
||||
|
||||
layers.outline.clear(); // slice raw edges
|
||||
layers.trace.clear(); // roughing
|
||||
layers.solid.clear(); // outline
|
||||
layers.bridge.clear(); // outline x
|
||||
layers.flat.clear(); // outline y
|
||||
layers.fill.clear(); // facing
|
||||
|
||||
tops.forEach(function(top) {
|
||||
outline.poly(top.poly, 0x999900, true, open);
|
||||
// if (top.inner) outline.poly(top.inner, 0xdddddd, true);
|
||||
if (top.inner) outline.poly(top.inner, 0xff0000, true);
|
||||
});
|
||||
|
||||
// various outlining
|
||||
let layer;
|
||||
slice.tops.forEach(function(top) {
|
||||
switch (slice.camMode) {
|
||||
case CPRO.OUTLINE:
|
||||
layer = layers.solid;
|
||||
break;
|
||||
case CPRO.CONTOUR_X:
|
||||
layer = layers.bridge;
|
||||
break;
|
||||
case CPRO.CONTOUR_Y:
|
||||
layer = layers.flat;
|
||||
break;
|
||||
default: // roughing
|
||||
layer = layers.trace;
|
||||
break;
|
||||
}
|
||||
if (top.traces) {
|
||||
layer.poly(top.traces, 0x010101, true, null);
|
||||
}
|
||||
});
|
||||
|
||||
// facing (previously separate. now part of roughing)
|
||||
layer = slice.layers.fill;
|
||||
slice.tops.forEach(function(top) {
|
||||
if (top.fill_lines) {
|
||||
layer.lines(top.fill_lines, fill_color);
|
||||
}
|
||||
});
|
||||
|
||||
outline.render();
|
||||
layers.trace.render();
|
||||
layers.solid.render();
|
||||
layers.bridge.render();
|
||||
layers.flat.render();
|
||||
layers.fill.render();
|
||||
});
|
||||
}
|
||||
|
||||
function printRender(print) {
|
||||
return KIRI.driver.FDM.printRender(print, {aslines: true, color: 0x010101, move_color: 0xcc3333});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Find top paths to trace when using ball and taper mills
|
||||
|
|
|
|||
270
src/mode/fdm/client.js
Normal file
270
src/mode/fdm/client.js
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
let KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
FDM = KIRI.driver.FDM;
|
||||
|
||||
FDM.init = function(kiri, api) {
|
||||
api.event.on("settings.load", (settings) => {
|
||||
if (settings.mode !== 'FDM') return;
|
||||
settings.process.outputOriginCenter = (settings.device.originCenter || false);
|
||||
});
|
||||
api.event.on("settings.saved", (settings) => {
|
||||
let proc = settings.process;
|
||||
api.ui.fdmSupport.marker.style.display = proc.sliceSupportEnable ? 'flex' : 'none';
|
||||
api.ui.fdmInfill.marker.style.display = proc.sliceFillSparse > 0 ? 'flex' : 'none';
|
||||
api.ui.fdmRaft.marker.style.display = proc.outputRaft ? 'flex' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DRIVER SLICE RENDER CONTRACT
|
||||
*
|
||||
* @param {Object} widget to render
|
||||
* @param {number} mode to use to select colors
|
||||
*/
|
||||
FDM.sliceRender = function(widget) {
|
||||
let slices = widget.slices;
|
||||
let settings = widget.settings;
|
||||
let extnum = widget.getExtruder(settings);
|
||||
let extarr = settings.device.extruders || [];
|
||||
let extinfo = extarr[extnum || 0];
|
||||
let extoff = (extinfo.extNozzle || 0.4) / 2;
|
||||
let thin = settings.controller.thinRender && settings.mode === 'FDM';
|
||||
|
||||
if (!slices) return;
|
||||
|
||||
// render outline
|
||||
slices.forEach(function(s) {
|
||||
let layers = s.layers;
|
||||
let tops = s.tops;
|
||||
let outline = layers.outline;
|
||||
let shells = layers.trace;
|
||||
let solids = layers.fill;
|
||||
let sparse = layers.sparse;
|
||||
let support = layers.support;
|
||||
let outsolid = layers.solid;
|
||||
let outbridge = layers.bridge;
|
||||
let outflat = layers.flat;
|
||||
|
||||
outline.clear();
|
||||
shells.clear();
|
||||
solids.clear();
|
||||
sparse.clear();
|
||||
support.clear();
|
||||
outsolid.clear();
|
||||
outbridge.clear();
|
||||
outflat.clear();
|
||||
|
||||
outline.setTransparent(true);
|
||||
shells.setTransparent(false);
|
||||
solids.setTransparent(false);
|
||||
sparse.setTransparent(false);
|
||||
support.setTransparent(false);
|
||||
outsolid.setTransparent(true);
|
||||
outbridge.setTransparent(true);
|
||||
outflat.setTransparent(true);
|
||||
|
||||
tops.forEach(function(top) {
|
||||
// outline
|
||||
outline.poly(top.poly, 0xdddddd, true, false);
|
||||
outline.solid(top.poly, thin ? 0xdddddd : 0xcccccc);
|
||||
if (top.inner) outline.poly(top.inner, 0xdddddd, true);
|
||||
// if (top.thinner) outline.poly(top.thinner, 0x559999, true, null);
|
||||
|
||||
// shells
|
||||
if (thin) {
|
||||
shells.poly(top.traces, 0x77bbcc, true);
|
||||
} else {
|
||||
shells.noodle(top.traces, extoff, 0x88aadd, 0x77bbcc);
|
||||
}
|
||||
if (top.polish) {
|
||||
shells.poly(top.polish.x, 0x880000, true);
|
||||
shells.poly(top.polish.y, 0x880000, true);
|
||||
}
|
||||
|
||||
// solid fill
|
||||
if (thin) {
|
||||
solids.lines(top.thin_fill, 0x77bbcc);
|
||||
solids.lines(top.fill_lines, 0x77bbcc);
|
||||
} else {
|
||||
solids.noodle_lines(top.thin_fill, extoff, 0x88aadd, 0x77bbcc, s.z);
|
||||
solids.noodle_lines(top.fill_lines, extoff, 0x88aadd, 0x77bbcc, s.z);
|
||||
}
|
||||
|
||||
// sparse fill
|
||||
if (top.fill_sparse) {
|
||||
top.fill_sparse.forEach(function(poly) {
|
||||
if (thin) {
|
||||
// todo cull polys with single point before this
|
||||
if (poly.length > 1) poly.render(sparse, 0x0, false, true);
|
||||
} else {
|
||||
sparse.noodle_open(poly, extoff, 0x88aadd, 0x77bbcc, s.z);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// support
|
||||
if (s.supports) {
|
||||
if (thin) {
|
||||
s.supports.forEach(function(poly) {
|
||||
support.poly(poly, 0xffaadd, true);
|
||||
support.lines(poly.fills, 0xffaadd);
|
||||
});
|
||||
} else {
|
||||
support.noodle(s.supports, extoff, 0xeeaadd, 0x77bbcc);
|
||||
s.supports.forEach(function(poly) {
|
||||
support.noodle_lines(poly.fills, extoff, 0xeeaadd, 0x77bbcc, s.z);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// solid outlines
|
||||
let trimmed = s.solids.trimmed;
|
||||
if (trimmed) trimmed.forEach(function(poly) {
|
||||
poly.setZ(s.z + 0.025);
|
||||
outsolid.poly(poly, 0x00cc00, true, false);
|
||||
outsolid.solid(poly, 0x00dd00);
|
||||
});
|
||||
|
||||
// diff bridges
|
||||
if (s.bridges) s.bridges.forEach(function (poly) {
|
||||
poly.setZ(s.z + 0.05);
|
||||
outbridge.poly(poly, 0x0099ee, true, false);
|
||||
outbridge.solid(poly, 0x00aaff);
|
||||
});
|
||||
|
||||
// diff flats
|
||||
if (s.flats) s.flats.forEach(function (poly) {
|
||||
poly.setZ(s.z + 0.05);
|
||||
outflat.poly(poly, 0xee0099, true, false);
|
||||
outflat.solid(poly, 0xff00aa);
|
||||
});
|
||||
|
||||
outline.renderAll();
|
||||
shells.renderAll();
|
||||
solids.renderAll();
|
||||
sparse.renderAll();
|
||||
support.renderAll();
|
||||
outsolid.renderAll();
|
||||
outbridge.renderAll();
|
||||
outflat.renderAll();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
FDM.printRender = function(print, options) {
|
||||
let debug = KIRI.api.const.LOCAL;
|
||||
let scope = print, emits, last,
|
||||
moves, moving = true,
|
||||
opt = options || {},
|
||||
tools = opt.tools || [],
|
||||
showmoves = !opt.nomoves,
|
||||
maxspeed = 0;
|
||||
// find max speed
|
||||
scope.output.forEach(function(layerout) {
|
||||
layerout.forEach(function(out, index) {
|
||||
if (out.emit && out.speed) {
|
||||
maxspeed = Math.max(maxspeed, out.speed);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (maxspeed === 0) {
|
||||
maxspeed = 4000;
|
||||
}
|
||||
maxspeed *= 1.001;
|
||||
// render layered output
|
||||
scope.lines = 0;
|
||||
scope.output.forEach(function(layerout) {
|
||||
let move = [], print = [], cprint;
|
||||
layerout.forEach(function(out, index) {
|
||||
let point = toPoint(out.point);
|
||||
if (last) {
|
||||
// drop short segments
|
||||
// if (point.emit === last.emit && UTIL.distSq(last, point) < 0.001 && point.z === last.z) {
|
||||
// return;
|
||||
// }
|
||||
if (out.emit > 0) {
|
||||
if (moving || !cprint) {
|
||||
cprint = base.newPolygon().setOpen(true).append(last);
|
||||
print.push({
|
||||
poly: cprint,
|
||||
speed: out.speed || maxspeed || 4000,
|
||||
tool:tools[out.tool]
|
||||
});
|
||||
}
|
||||
try {
|
||||
cprint.append(point);
|
||||
} catch (e) {
|
||||
console.log(e, {cprint});
|
||||
}
|
||||
moving = false;
|
||||
} else {
|
||||
cprint = null;
|
||||
move.push(last);
|
||||
move.push(point);
|
||||
moving = true;
|
||||
}
|
||||
// move direction arrow heads
|
||||
if (debug && last.z == point.z) {
|
||||
let rs = BASE.newSlope(
|
||||
{x: point.x, y: point.y},
|
||||
{x: last.x, y: last.y}
|
||||
);
|
||||
let ao1 = BASE.newSlopeFromAngle(rs.angle + 25);
|
||||
let ao2 = BASE.newSlopeFromAngle(rs.angle - 25);
|
||||
let sp = BASE.newPoint(point.x, point.y, point.z);
|
||||
move.push(sp);
|
||||
move.push(sp.projectOnSlope(ao1, BASE.config.debug_arrow));
|
||||
move.push(sp);
|
||||
move.push(sp.projectOnSlope(ao2, BASE.config.debug_arrow));
|
||||
}
|
||||
}
|
||||
last = point;
|
||||
});
|
||||
emits = KIRI.newLayer(scope.group);
|
||||
if (showmoves) {
|
||||
moves = KIRI.newLayer(scope.group);
|
||||
moves.lines(move, opt.move_color || 0x888888);
|
||||
}
|
||||
emits.setTransparent(false);
|
||||
// emit printing shapes
|
||||
print.forEach(segment => {
|
||||
let {poly, speed, tool} = segment;
|
||||
let off = tool ? (tool.extNozzle || 0.4) / 2 : 0.2;
|
||||
let sint = Math.min(maxspeed, parseInt(speed));
|
||||
let rgb = scope.hsv2rgb({h:sint/maxspeed, s:1, v:1});
|
||||
let color = ((rgb.r * 0xff) << 16) |
|
||||
((rgb.g * 0xff) << 8) |
|
||||
((rgb.b * 0xff) << 0);
|
||||
if (opt.flat) {
|
||||
poly = poly.clone().setZ(0);
|
||||
}
|
||||
if (opt.aslines) {
|
||||
emits.poly(poly, opt.color || color, false, true);
|
||||
} else {
|
||||
// first point may be from the layer below, so use second point
|
||||
emits.noodle_open(poly, off - 0.02, color, 0x0, poly.getZ(1));
|
||||
}
|
||||
});
|
||||
emits.renderAll();
|
||||
if (showmoves) {
|
||||
moves.render();
|
||||
scope.movesView.push(moves);
|
||||
}
|
||||
scope.printView.push(emits);
|
||||
scope.lines += print.length;
|
||||
});
|
||||
}
|
||||
|
||||
function toPoint(obj) {
|
||||
return base.newPoint(obj.x, obj.y, obj.z);
|
||||
}
|
||||
|
||||
})();
|
||||
File diff suppressed because it is too large
Load diff
382
src/mode/fdm/export.js
Normal file
382
src/mode/fdm/export.js
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
let KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
FDM = KIRI.driver.FDM;
|
||||
|
||||
/**
|
||||
* @returns {Array} gcode lines
|
||||
*/
|
||||
FDM.printExport = function(print, online) {
|
||||
let layers = print.output,
|
||||
settings = FDM.fixExtruders(print.settings),
|
||||
device = settings.device,
|
||||
extruders = device.extruders,
|
||||
process = settings.process,
|
||||
gcodeFan = device.gcodeFan,
|
||||
gcodeLayer = device.gcodeLayer,
|
||||
gcodeTrack = device.gcodeTrack,
|
||||
tool = 0,
|
||||
extruder = extruders[tool],
|
||||
offset_x = extruder.extOffsetX,
|
||||
offset_y = extruder.extOffsetY,
|
||||
extrudeAbs = device.extrudeAbs,
|
||||
time = 0,
|
||||
layer = 0,
|
||||
pause = [],
|
||||
pauseCmd = device.gcodePause,
|
||||
output = [],
|
||||
outputLength = 0,
|
||||
lastProgress = 0,
|
||||
decimals = BASE.config.gcode_decimals || 4,
|
||||
progress = 0,
|
||||
distance = 0,
|
||||
emitted = 0,
|
||||
retracted = 0,
|
||||
pos = {x:0, y:0, z:0, f:0},
|
||||
last = null,
|
||||
zpos = 0,
|
||||
zhop = process.zHopDistance || 0,
|
||||
seekMMM = process.outputSeekrate * 60,
|
||||
retDist = process.outputRetractDist,
|
||||
retSpeed = process.outputRetractSpeed * 60,
|
||||
retDwell = process.outputRetractDwell || 0,
|
||||
timeDwell = retDwell / 1000,
|
||||
offset = process.outputOriginCenter ? null : {
|
||||
x: device.bedWidth/2,
|
||||
y: device.bedDepth/2
|
||||
},
|
||||
subst = {
|
||||
travel_speed: seekMMM,
|
||||
retract_speed: retSpeed,
|
||||
retract_distance: retDist,
|
||||
temp: process.firstLayerNozzleTemp || process.outputTemp,
|
||||
temp_bed: process.firstLayerBedTemp || process.outputBedTemp,
|
||||
bed_temp: process.firstLayerBedTemp || process.outputBedTemp,
|
||||
fan_speed: process.outputFanMax,
|
||||
speed: process.outputFanMax, // legacy
|
||||
top: offset ? device.bedDepth : device.bedDepth/2,
|
||||
left: offset ? 0 : -device.bedWidth/2,
|
||||
right: offset ? device.bedWidth : device.bedWidth/2,
|
||||
bottom: offset ? 0 : -device.bedDepth/2,
|
||||
z_max: device.maxHeight,
|
||||
layers: layers.length,
|
||||
nozzle: 0,
|
||||
tool: 0
|
||||
},
|
||||
pidx, path, out, speedMMM, emitMM, emitPerMM, lastp, laste, dist,
|
||||
append,
|
||||
lines = 0,
|
||||
bytes = 0;
|
||||
|
||||
(process.gcodePauseLayers || "").split(",").forEach(function(lv) {
|
||||
let v = parseInt(lv);
|
||||
if (v >= 0) pause.push(v);
|
||||
});
|
||||
|
||||
if (online) {
|
||||
append = function(line) {
|
||||
if (line) {
|
||||
lines++;
|
||||
bytes += line.length;
|
||||
output.append(line);
|
||||
}
|
||||
if (!line || output.length > 1000) {
|
||||
online(output.join("\n"));
|
||||
output = [];
|
||||
}
|
||||
};
|
||||
} else {
|
||||
append = function(line) {
|
||||
if (!line) return;
|
||||
output.append(line);
|
||||
lines++;
|
||||
bytes += line.length;
|
||||
}
|
||||
}
|
||||
|
||||
function appendSub(line) {
|
||||
append(print.constReplace(line, subst));
|
||||
}
|
||||
|
||||
function appendAll(arr) {
|
||||
if (!arr) return;
|
||||
if (!Array.isArray(arr)) arr = [ arr ];
|
||||
arr.forEach(function(line) { append(line) });
|
||||
}
|
||||
|
||||
function appendAllSub(arr) {
|
||||
if (!arr) return;
|
||||
if (!Array.isArray(arr)) arr = [ arr ];
|
||||
arr.forEach(function(line) { appendSub(line) });
|
||||
}
|
||||
|
||||
append(`; Generated by Kiri:Moto ${KIRI.version}`);
|
||||
append(`; ${new Date().toString()}`);
|
||||
appendSub("; Bed left:{left} right:{right} top:{top} bottom:{bottom}");
|
||||
append(`; Target: ${settings.filter[settings.mode]}`);
|
||||
append("; --- process ---");
|
||||
for (let pk in process) {
|
||||
append("; " + pk + " = " + process[pk]);
|
||||
}
|
||||
append("; --- startup ---");
|
||||
let t0 = false;
|
||||
let t1 = false;
|
||||
for (let i=0; i<device.gcodePre.length; i++) {
|
||||
let line = device.gcodePre[i];
|
||||
if (line.indexOf('T0') >= 0) t0 = true; else
|
||||
if (line.indexOf('T1') >= 0) t1 = true; else
|
||||
if (line.indexOf('M82') >= 0) extrudeAbs = true; else
|
||||
if (line.indexOf('M83') >= 0) extrudeAbs = false; else
|
||||
if (line.indexOf('G90') >= 0) extrudeAbs = true; else
|
||||
if (line.indexOf('G91') >= 0) extrudeAbs = false; else
|
||||
if (line.indexOf('G92') === 0) {
|
||||
line.split(";")[0].split(' ').forEach(function (tok) {
|
||||
let val = parseFloat(tok.substring(1) || 0) || 0;
|
||||
switch (tok[0]) {
|
||||
case 'X': pos.x = val; break;
|
||||
case 'Y': pos.y = val; break;
|
||||
case 'Z': pos.z = val; break;
|
||||
case 'E': outputLength = val; break;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (extrudeAbs && line.indexOf('E') > 0) {
|
||||
line.split(";")[0].split(' ').forEach(function (tok) {
|
||||
// use max E position from gcode-preamble
|
||||
if (tok[0] == 'E') {
|
||||
outputLength = Math.max(outputLength, parseFloat(tok.substring(1)) || 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
appendSub(line);
|
||||
}
|
||||
|
||||
function dwell(ms) {
|
||||
append(`G4 P${ms}`);
|
||||
time += timeDwell;
|
||||
}
|
||||
|
||||
function retract(zhop) {
|
||||
retracted = retDist;
|
||||
moveTo({e:-retracted}, retSpeed, `e-retract ${retDist}`);
|
||||
if (zhop) moveTo({z:zpos + zhop}, seekMMM, "z-hop start");
|
||||
time += (retDist / retSpeed) * 60 * 2; // retraction time
|
||||
}
|
||||
|
||||
function moveTo(newpos, rate, comment) {
|
||||
if (comment) {
|
||||
append(";; " + comment);
|
||||
}
|
||||
let o = [!rate && !newpos.e ? 'G0' : 'G1'];
|
||||
if (typeof newpos.x === 'number') {
|
||||
pos.x = newpos.x;//UTIL.round(newpos.x,decimals);
|
||||
o.append(" X").append(pos.x.toFixed(decimals));
|
||||
}
|
||||
if (typeof newpos.y === 'number') {
|
||||
pos.y = newpos.y;//UTIL.round(newpos.y,decimals);
|
||||
o.append(" Y").append(pos.y.toFixed(decimals));
|
||||
}
|
||||
if (typeof newpos.z === 'number') {
|
||||
pos.z = newpos.z;//UTIL.round(newpos.z,decimals);
|
||||
o.append(" Z").append(pos.z.toFixed(decimals));
|
||||
}
|
||||
if (typeof newpos.e === 'number') {
|
||||
outputLength += newpos.e;
|
||||
if (extrudeAbs) {
|
||||
// for cumulative (absolute) extruder positions
|
||||
// o.append(" E").append(UTIL.round(outputLength, decimals));
|
||||
o.append(" E").append(outputLength.toFixed(decimals));
|
||||
} else {
|
||||
// o.append(" E").append(UTIL.round(newpos.e, decimals));
|
||||
o.append(" E").append(newpos.e.toFixed(decimals));
|
||||
}
|
||||
}
|
||||
if (rate && rate != pos.f) {
|
||||
o.append(" F").append(Math.round(rate));
|
||||
pos.f = rate
|
||||
}
|
||||
let line = o.join('');
|
||||
if (last == line) {
|
||||
// console.log({dup:line});
|
||||
return;
|
||||
}
|
||||
last = line;
|
||||
append(line);
|
||||
}
|
||||
|
||||
// calc total distance traveled by head as proxy for progress
|
||||
let allout = [], totaldistance = 0;
|
||||
layers.forEach(function(outs) {
|
||||
allout.appendAll(outs);
|
||||
});
|
||||
allout.forEachPair(function (o1, o2) {
|
||||
totaldistance += o1.point.distTo2D(o2.point);
|
||||
}, 1);
|
||||
|
||||
// retract before first move
|
||||
retract();
|
||||
|
||||
while (layer < layers.length) {
|
||||
path = layers[layer];
|
||||
emitPerMM = print.extrudePerMM(
|
||||
extruder.extNozzle,
|
||||
extruder.extFilament,
|
||||
path.layer === 0 ?
|
||||
(process.firstSliceHeight || process.sliceHeight) : path.height);
|
||||
|
||||
subst.z = (zpos + path.height).toFixed(3);
|
||||
subst.Z = subst.z;
|
||||
subst.layer = layer;
|
||||
subst.height = path.height.toFixed(3);
|
||||
|
||||
if (pauseCmd && pause.indexOf(layer) >= 0) {
|
||||
appendAllSub(pauseCmd)
|
||||
}
|
||||
|
||||
if (gcodeLayer && gcodeLayer.length) {
|
||||
appendAllSub(gcodeLayer);
|
||||
} else {
|
||||
append(`;; --- layer ${layer} (${subst.height} @ ${subst.z}) ---`);
|
||||
}
|
||||
|
||||
if (layer > 0 && process.outputLayerRetract) {
|
||||
retract();
|
||||
}
|
||||
|
||||
// enable fan at fan layer
|
||||
if (gcodeFan && layer === process.outputFanLayer) {
|
||||
appendAllSub(gcodeFan);
|
||||
}
|
||||
|
||||
// second layer transitions
|
||||
if (layer === 1) {
|
||||
// update temps when first layer overrides are present
|
||||
if (process.firstLayerNozzleTemp) {
|
||||
subst.temp = process.outputTemp;
|
||||
if (t0) appendSub("M104 S{temp} T0");
|
||||
if (t1) appendSub("M104 S{temp} T1");
|
||||
if (!(t0 || t1)) appendSub("M104 S{temp} T{tool}");
|
||||
}
|
||||
if (process.firstLayerBedTemp) {
|
||||
subst.bed_temp = subst.temp_bed = process.outputBedTemp;
|
||||
appendSub("M140 S{temp_bed} T0");
|
||||
}
|
||||
}
|
||||
|
||||
// move Z to layer height
|
||||
zpos += path.height;
|
||||
moveTo({z:zpos}, seekMMM);
|
||||
|
||||
// iterate through layer outputs
|
||||
for (pidx=0; pidx<path.length; pidx++) {
|
||||
out = path[pidx];
|
||||
speedMMM = (out.speed || process.outputFeedrate) * 60;
|
||||
|
||||
// look for extruder change and recalc emit factor
|
||||
if (out.tool !== undefined && out.tool !== tool) {
|
||||
tool = out.tool;
|
||||
subst.nozzle = subst.tool = tool;
|
||||
extruder = extruders[tool];
|
||||
offset_x = extruder.extOffsetX;
|
||||
offset_y = extruder.extOffsetY;
|
||||
emitPerMM = print.extrudePerMM(
|
||||
extruder.extNozzle,
|
||||
extruder.extFilament,
|
||||
path.layer === 0 ?
|
||||
(process.firstSliceHeight || process.sliceHeight) : path.height);
|
||||
appendAllSub(extruder.extSelect);
|
||||
}
|
||||
|
||||
// if no point in output, it's a dwell command
|
||||
if (!out.point) {
|
||||
dwell(out.speed);
|
||||
continue;
|
||||
}
|
||||
|
||||
let x = out.point.x + offset_x,
|
||||
y = out.point.y + offset_y,
|
||||
z = out.point.z;
|
||||
|
||||
// adjust for inversions and offsets
|
||||
if (process.outputInvertX) x = -x;
|
||||
if (process.outputInvertY) y = -y;
|
||||
if (offset) {
|
||||
x += offset.x;
|
||||
y += offset.y;
|
||||
}
|
||||
|
||||
dist = lastp ? lastp.distTo2D(out.point) : 0;
|
||||
|
||||
// re-engage post-retraction before new extrusion
|
||||
if (out.emit && retracted) {
|
||||
// when enabled, resume previous Z
|
||||
if (zhop && pos.z != zpos) moveTo({z:zpos}, seekMMM, "z-hop end");
|
||||
// re-engage retracted filament
|
||||
moveTo({e:retracted}, retSpeed, `e-engage ${retracted}`);
|
||||
retracted = 0;
|
||||
// optional dwell after re-engaging filament to allow pressure to build
|
||||
if (retDwell) dwell(retDwell);
|
||||
time += (retDist / retSpeed) * 60 * 2; // retraction time
|
||||
}
|
||||
|
||||
if (lastp && out.emit) {
|
||||
emitMM = emitPerMM * out.emit * dist;
|
||||
moveTo({x:x, y:y, e:emitMM}, speedMMM);
|
||||
emitted += emitMM;
|
||||
} else {
|
||||
moveTo({x:x, y:y}, seekMMM);
|
||||
// TODO disabling out of plane z moves until a better mechanism
|
||||
// can be built that doesn't rely on computed zpos from layer heights...
|
||||
// when making z moves (like polishing) allow slowdown vs fast seek
|
||||
// let moveSpeed = (lastp && lastp.z !== z) ? speedMMM : seekMMM;
|
||||
// moveTo({x:x, y:y, z:z}, moveSpeed);
|
||||
}
|
||||
|
||||
// retract filament if point retract flag set
|
||||
if (!retracted && out.retract) {
|
||||
retract(zhop);
|
||||
}
|
||||
|
||||
// update time and distance (should calc in moveTo() instead)
|
||||
time += (dist / speedMMM) * 60 * 1.5;
|
||||
distance += dist;
|
||||
subst.progress = progress = Math.round((distance / totaldistance) * 100);
|
||||
|
||||
// emit tracked progress
|
||||
if (gcodeTrack && progress != lastProgress) {
|
||||
appendAllSub(gcodeTrack);
|
||||
lastProgress = progress;
|
||||
}
|
||||
|
||||
lastp = out.point;
|
||||
laste = out.emit;
|
||||
}
|
||||
layer++;
|
||||
}
|
||||
|
||||
subst.time = UTIL.round(time,2);
|
||||
subst.material = UTIL.round(emitted,2);
|
||||
|
||||
append("; --- shutdown ---");
|
||||
appendAllSub(device.gcodePost);
|
||||
append(`; --- filament used: ${subst.material} mm ---`);
|
||||
append(`; --- print time: ${time.toFixed(0)}s ---`);
|
||||
|
||||
// force emit of buffer
|
||||
append();
|
||||
|
||||
print.distance = emitted;
|
||||
print.lines = lines;
|
||||
print.bytes = bytes + lines - 1;
|
||||
print.time = time;
|
||||
|
||||
return online ? null : output.join("\n");
|
||||
};
|
||||
|
||||
})();
|
||||
373
src/mode/fdm/prepare.js
Normal file
373
src/mode/fdm/prepare.js
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
let KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
FDM = KIRI.driver.FDM,
|
||||
newPoint = BASE.newPoint;
|
||||
|
||||
/**
|
||||
* DRIVER PRINT CONTRACT
|
||||
*
|
||||
* @param {Object} print state object
|
||||
* @param {Function} update incremental callback
|
||||
*/
|
||||
FDM.printSetup = function(print, update) {
|
||||
let widgets = print.widgets.slice(),
|
||||
settings = FDM.fixExtruders(print.settings),
|
||||
device = settings.device,
|
||||
nozzle = device.extruders[0].extNozzle,
|
||||
process = settings.process,
|
||||
mode = settings.mode,
|
||||
output = print.output,
|
||||
bounds = settings.bounds,
|
||||
printPoint = newPoint(0,0,0),
|
||||
firstLayerHeight = process.firstSliceHeight || process.sliceHeight,
|
||||
maxLayers = 0,
|
||||
layer = 0,
|
||||
zoff = 0,
|
||||
meshIndex,
|
||||
lastIndex,
|
||||
closest,
|
||||
mindist,
|
||||
minidx,
|
||||
find,
|
||||
layerout = [],
|
||||
slices = [],
|
||||
sliceEntry;
|
||||
|
||||
// TODO pick a widget with a slice on the first layer and use that nozzle
|
||||
// create brim, skirt, raft if specificed in FDM mode (code shared by laser)
|
||||
if (process.outputBrimCount || process.outputRaft) {
|
||||
let brims = [],
|
||||
offset = process.outputBrimOffset || (process.outputRaft ? 4 : 0);
|
||||
|
||||
// compute first brim
|
||||
widgets.forEach(function(widget) {
|
||||
let tops = [];
|
||||
// collect top outer polygons
|
||||
widget.slices[0].tops.forEach(function(top) {
|
||||
tops.push(top.poly.clone());
|
||||
});
|
||||
// collect support polygons
|
||||
if (widget.slices[0].supports)
|
||||
widget.slices[0].supports.forEach(function(support) {
|
||||
tops.push(support.clone());
|
||||
});
|
||||
// nest and offset tops
|
||||
POLY.nest(tops).forEach(function(poly) {
|
||||
poly.offset(-offset + nozzle / 2).forEach(function(brim) {
|
||||
brim.move(widget.mesh.position);
|
||||
brims.push(brim);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// merge brims
|
||||
brims = POLY.union(brims);
|
||||
|
||||
// if brim is offset, over-expand then shrink to induce brims to merge
|
||||
if (offset && brims.length) {
|
||||
let extra = process.sliceSupportExtra + 2;
|
||||
let zheight = brims[0].getZ();
|
||||
brims = POLY.expand(brims, extra, zheight, null, 1);
|
||||
brims = POLY.expand(brims, -extra, zheight, null, 1);
|
||||
}
|
||||
|
||||
// if raft is specified
|
||||
if (process.outputRaft) {
|
||||
let offset = newPoint(0,0,0),
|
||||
height = nozzle;
|
||||
|
||||
// cause first point of raft to be used
|
||||
printPoint = null;
|
||||
|
||||
let raft = function(height, angle, spacing, speed, extrude) {
|
||||
let slice = kiri.newSlice(zoff + height / 2);
|
||||
brims.forEach(function(brim) {
|
||||
// use first point of first brim as start point
|
||||
if (printPoint === null) printPoint = brim.first();
|
||||
let t = slice.addTop(brim);
|
||||
t.traces = [ brim ];
|
||||
t.inner = POLY.expand(t.traces, -nozzle * 0.5, 0, null, 1);
|
||||
// tweak bounds for fill to induce an offset
|
||||
t.inner[0].bounds.minx -= nozzle/2;
|
||||
t.inner[0].bounds.maxx += nozzle/2;
|
||||
t.fill_lines = POLY.fillArea(t.inner, angle, spacing, []);
|
||||
})
|
||||
offset.z = slice.z;
|
||||
printPoint = print.slicePrintPath(slice, printPoint, offset, layerout, {
|
||||
speed: speed,
|
||||
mult: extrude,
|
||||
});
|
||||
layerout.height = height;
|
||||
output.append(layerout);
|
||||
|
||||
layerout = [];
|
||||
zoff += height;
|
||||
};
|
||||
|
||||
raft(nozzle/1, process.sliceFillAngle + 0 , nozzle * 6.0, process.firstLayerRate / 3, 4);
|
||||
raft(nozzle/1, process.sliceFillAngle + 0 , nozzle * 6.0, process.firstLayerRate / 2, 4);
|
||||
raft(nozzle/2, process.sliceFillAngle + 90, nozzle * 3.0, process.outputFeedrate, 2.5);
|
||||
raft(nozzle/2, process.sliceFillAngle + 0 , nozzle * 1.0, process.outputFeedrate, 1.5);
|
||||
raft(nozzle/2, process.sliceFillAngle + 0 , nozzle * 1.0, process.outputFeedrate, 1.0);
|
||||
|
||||
// raise first layer off raft slightly to lessen adhesion
|
||||
firstLayerHeight += process.outputRaftSpacing || 0;
|
||||
|
||||
// retract after last raft layer
|
||||
output.last().last().retract = true;
|
||||
}
|
||||
// raft excludes brims
|
||||
else
|
||||
// if using brim vs raft
|
||||
if (process.outputBrimCount) {
|
||||
let polys = [],
|
||||
preout = [];
|
||||
|
||||
// expand specified # of brims
|
||||
brims.forEach(function(brim) {
|
||||
POLY.offset([brim], nozzle, {outs: polys, flat: true, count: process.outputBrimCount});
|
||||
});
|
||||
|
||||
// output brim points
|
||||
printPoint = print.poly2polyEmit(polys, printPoint, function(poly, index, count, startPoint) {
|
||||
return print.polyPrintPath(poly, startPoint, preout, {
|
||||
rate: process.firstLayerRate,
|
||||
onfirst: function(point) {
|
||||
if (preout.length && point.distTo2D(startPoint) > 2) {
|
||||
// retract between brims
|
||||
preout.last().retract = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}, null, true);
|
||||
|
||||
print.addPrintPoints(preout, layerout, null);
|
||||
|
||||
if (preout.length) {
|
||||
// retract between brims and print
|
||||
preout.last().retract = true;
|
||||
}
|
||||
}
|
||||
// recompute bounds for purge block offsets
|
||||
let bbounds = BASE.newBounds();
|
||||
brims.forEach(brim => {
|
||||
bbounds.merge(brim.bounds);
|
||||
});
|
||||
bounds.min.x = Math.min(bounds.min.x, bbounds.minx);
|
||||
bounds.min.y = Math.min(bounds.min.y, bbounds.miny);
|
||||
bounds.max.x = Math.max(bounds.max.x, bbounds.maxx);
|
||||
bounds.max.y = Math.max(bounds.max.y, bbounds.maxy);
|
||||
}
|
||||
|
||||
// synthesize a support widget, if needed
|
||||
if (process.sliceSupportEnable) {
|
||||
let swidget = KIRI.newWidget();
|
||||
let sslices = swidget.slices = [];
|
||||
widgets.forEach(function(widget) {
|
||||
let slices = widget.slices;
|
||||
while (sslices.length < slices.length) {
|
||||
let sslice = KIRI.newSlice(slices[sslices.length].z);
|
||||
sslice.extruder = process.sliceSupportNozzle;
|
||||
sslices.push(sslice);
|
||||
}
|
||||
slices.forEach((slice,index) => {
|
||||
if (!slice.supports) return;
|
||||
if (!sslices[index].supports) {
|
||||
sslices[index].supports = [];
|
||||
}
|
||||
sslices[index].supports.appendAll(slice.supports.map(p => {
|
||||
if (p.fills) p.fills.forEach(p => p.move(widget.track.pos));
|
||||
return p.move(widget.track.pos);
|
||||
}));
|
||||
});
|
||||
});
|
||||
swidget.support = true;
|
||||
swidget.mesh = { widget: swidget }; // fake for lookup
|
||||
settings.widget[swidget.id] = { extruder: process.sliceSupportNozzle };
|
||||
widgets.push(swidget);
|
||||
}
|
||||
|
||||
let extruders = [];
|
||||
let extcount = 0;
|
||||
|
||||
// find max layers (for updates)
|
||||
// generate list of used extruders for purge blocks
|
||||
widgets.forEach(function(widget) {
|
||||
maxLayers = Math.max(maxLayers, widget.slices.length);
|
||||
let extruder = (settings.widget[widget.id] || {}).extruder || 0;
|
||||
if (!extruders[extruder]) {
|
||||
extruders[extruder] = {};
|
||||
extcount++;
|
||||
}
|
||||
});
|
||||
|
||||
let blokpos, walkpos, blok;
|
||||
if (bounds.min.x < bounds.min.y) {
|
||||
let dx = ((bounds.max.x - bounds.min.x) - (extcount * 10)) / 2 + 5;
|
||||
blokpos = { x:bounds.min.x + dx, y: bounds.max.y + 5};
|
||||
walkpos = { x:10, y:0 };
|
||||
blok = { x:9, y:4 };
|
||||
} else {
|
||||
let dy = ((bounds.max.y - bounds.min.y) - (extcount * 10)) / 2 + 5;
|
||||
blokpos = { x:bounds.max.x + 5, y: bounds.min.y + dy};
|
||||
walkpos = { x:0, y:10 };
|
||||
blok = { x:4, y:9 };
|
||||
}
|
||||
|
||||
// compute purge blocks
|
||||
extruders = extruders.map((ext,i) => {
|
||||
if (!ext) return ext;
|
||||
let noz = device.extruders[i].extNozzle,
|
||||
pos = {x:blokpos.x, y:blokpos.y, z:0},
|
||||
rec = {
|
||||
extruder: i,
|
||||
poly: BASE.newPolygon().centerSpiral(pos, blok.x, blok.y, noz*2, 3)
|
||||
};
|
||||
blokpos.x += walkpos.x;
|
||||
blokpos.y += walkpos.y;
|
||||
return rec;
|
||||
});
|
||||
|
||||
// generate purge block for given nozzle
|
||||
function purge(nozzle, track, layer, start, z, using) {
|
||||
if (extcount < 2) {
|
||||
return start;
|
||||
}
|
||||
let rec = track[nozzle];
|
||||
if (rec) {
|
||||
track[nozzle] = null;
|
||||
if (layer.last()) layer.last().retract = true;
|
||||
start = print.polyPrintPath(rec.poly.clone().setZ(z), start, layer, {
|
||||
tool: using || nozzle,
|
||||
open: true,
|
||||
simple: true
|
||||
});
|
||||
layer.last().retract = true;
|
||||
return start;
|
||||
} else {
|
||||
console.log({already_purged: nozzle, from: track, layer});
|
||||
return start;
|
||||
}
|
||||
}
|
||||
|
||||
// increment layer count until no widget has remaining slices
|
||||
for (;;) {
|
||||
// create list of mesh slice arrays with their platform offsets
|
||||
for (meshIndex = 0; meshIndex < widgets.length; meshIndex++) {
|
||||
let mesh = widgets[meshIndex].mesh;
|
||||
if (!mesh.widget) {
|
||||
continue;
|
||||
}
|
||||
let mslices = mesh.widget.slices;
|
||||
if (mslices && mslices[layer]) {
|
||||
slices.push({
|
||||
slice: mslices[layer],
|
||||
offset: mesh.position || {x:0, y:0, z:0},
|
||||
widget: mesh.widget
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// exit if no slices
|
||||
if (slices.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// track purge blocks generated for each layer
|
||||
let track = extruders.slice();
|
||||
let lastOut;
|
||||
let lastExt;
|
||||
|
||||
// iterate over layer slices, find closest widget, print, eliminate
|
||||
for (;;) {
|
||||
closest = null;
|
||||
mindist = Infinity;
|
||||
let order = [];
|
||||
// select slices of the same extruder type first then distance
|
||||
for (meshIndex = 0; meshIndex < slices.length; meshIndex++) {
|
||||
sliceEntry = slices[meshIndex];
|
||||
if (sliceEntry) {
|
||||
find = sliceEntry.slice.findClosestPointTo(printPoint.sub(sliceEntry.offset));
|
||||
if (find) {
|
||||
let ext = sliceEntry.slice.extruder;
|
||||
let lex = lastOut ? lastOut.extruder : ext;
|
||||
let dst = Math.abs(find.distance);
|
||||
if (ext !== lex) dst *= 10000;
|
||||
order.push({dst,sliceEntry,meshIndex});
|
||||
}
|
||||
}
|
||||
}
|
||||
order.sort((a,b) => {
|
||||
return a.dst - b.dst;
|
||||
});
|
||||
if (order.length) {
|
||||
let find = order.shift();
|
||||
closest = find.sliceEntry;
|
||||
minidx = find.meshIndex;
|
||||
}
|
||||
|
||||
if (!closest) {
|
||||
if (sliceEntry) lastOut = sliceEntry.slice;
|
||||
break;
|
||||
}
|
||||
// retract between widgets
|
||||
if (layerout.length && minidx !== lastIndex) {
|
||||
layerout.last().retract = true;
|
||||
}
|
||||
layerout.height = layerout.height || closest.slice.height;
|
||||
slices[minidx] = null;
|
||||
closest.offset.z = zoff;
|
||||
// detect extruder change and print purge block
|
||||
if (!lastOut || lastOut.extruder !== closest.slice.extruder) {
|
||||
printPoint = purge(closest.slice.extruder, track, layerout, printPoint, closest.slice.z);
|
||||
}
|
||||
// output seek to start point between mesh slices if previous data
|
||||
printPoint = print.slicePrintPath(
|
||||
closest.slice,
|
||||
printPoint.sub(closest.offset),
|
||||
closest.offset,
|
||||
layerout,
|
||||
{
|
||||
first: closest.slice.index === 0,
|
||||
support: closest.widget.support
|
||||
}
|
||||
);
|
||||
lastOut = closest.slice;
|
||||
lastExt = lastOut.ext
|
||||
lastIndex = minidx;
|
||||
}
|
||||
|
||||
// if a declared extruder isn't used in a layer, use selected
|
||||
// extruder to fill the relevant purge blocks for later support
|
||||
track.forEach(ext => {
|
||||
if (ext) {
|
||||
printPoint = purge(ext.extruder, track, layerout, printPoint, lastOut.z, lastExt);
|
||||
}
|
||||
});
|
||||
|
||||
// if layer produced output, append to output array
|
||||
if (layerout.length) output.append(layerout);
|
||||
|
||||
// notify progress
|
||||
layerout.layer = layer++;
|
||||
update(layer / maxLayers);
|
||||
|
||||
// retract after last layer
|
||||
if (layer === maxLayers && layerout.length) {
|
||||
layerout.last().retract = true;
|
||||
}
|
||||
|
||||
slices = [];
|
||||
layerout = [];
|
||||
lastOut = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
372
src/mode/fdm/slice.js
Normal file
372
src/mode/fdm/slice.js
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
let KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
CONF = BASE.config,
|
||||
FDM = KIRI.driver.FDM,
|
||||
SLICER = KIRI.slicer;
|
||||
|
||||
/**
|
||||
* DRIVER SLICE CONTRACT
|
||||
*
|
||||
* Given a widget and settings object, call functions necessary to produce
|
||||
* slices and then the computations using those slices. This function is
|
||||
* designed to run client or server-side and provides all output via
|
||||
* callback functions.
|
||||
*
|
||||
* @param {Object} settings
|
||||
* @param {Widget} Widget
|
||||
* @param {Function} onupdate (called with % complete and optional message)
|
||||
* @param {Function} ondone (called when complete with an array of Slice objects)
|
||||
*/
|
||||
FDM.slice = function(settings, widget, onupdate, ondone) {
|
||||
FDM.fixExtruders(settings);
|
||||
let spro = settings.process,
|
||||
sdev = settings.device,
|
||||
update_start = Date.now(),
|
||||
minSolid = spro.sliceSolidMinArea,
|
||||
solidLayers = spro.sliceSolidLayers,
|
||||
vaseMode = spro.sliceFillType === 'vase',
|
||||
doSolidLayers = solidLayers && !vaseMode,
|
||||
metadata = settings.widget[widget.id] || {},
|
||||
extruder = metadata.extruder || 0,
|
||||
sliceHeight = spro.sliceHeight,
|
||||
nozzleSize = sdev.extruders[extruder].extNozzle,
|
||||
firstOffset = nozzleSize / 2,
|
||||
shellOffset = nozzleSize,
|
||||
fillOffsetMult = 1.0 - bound(spro.sliceFillOverlap, 0, 0.8),
|
||||
fillSpacing = nozzleSize,
|
||||
fillOffset = nozzleSize * fillOffsetMult,
|
||||
sliceFillAngle = spro.sliceFillAngle,
|
||||
view = widget.mesh && widget.mesh.newGroup ? widget.mesh.newGroup() : null;
|
||||
|
||||
if (!(sliceHeight > 0 && sliceHeight < 100)) {
|
||||
return ondone("invalid slice height");
|
||||
}
|
||||
if (!(nozzleSize >= 0.01 && nozzleSize < 100)) {
|
||||
return ondone("invalid nozzle size");
|
||||
}
|
||||
|
||||
if (spro.firstSliceHeight === 0) {
|
||||
spro.firstSliceHeight = sliceHeight;
|
||||
}
|
||||
|
||||
if (spro.firstSliceHeight < sliceHeight) {
|
||||
DBUG.log("invalid first layer height < slice height");
|
||||
DBUG.log("reverting to slice height");
|
||||
spro.firstSliceHeight = sliceHeight;
|
||||
}
|
||||
|
||||
SLICER.sliceWidget(widget, {
|
||||
height: sliceHeight,
|
||||
minHeight: sliceHeight > spro.sliceMinHeight ? spro.sliceMinHeight : 0,
|
||||
firstHeight: spro.firstSliceHeight,
|
||||
view: view
|
||||
}, onSliceDone, onSliceUpdate);
|
||||
|
||||
function onSliceUpdate(update) {
|
||||
return onupdate(0.0 + update * 0.5);
|
||||
}
|
||||
|
||||
function onSliceDone(slices) {
|
||||
// slices = slices.filter(slice => slice.tops.length);
|
||||
// remove all empty slices above part but leave below
|
||||
// for multi-part (multi-extruder) setups where the void is ok
|
||||
let found = false;
|
||||
slices = slices.reverse().filter(slice => {
|
||||
if (slice.tops.length) {
|
||||
return found = true;
|
||||
} else {
|
||||
return found;
|
||||
}
|
||||
}).reverse();
|
||||
|
||||
widget.slices = slices;
|
||||
|
||||
if (!slices) return;
|
||||
|
||||
// calculate % complete and call onupdate()
|
||||
function doupdate(index, from, to, msg) {
|
||||
onupdate(0.5 + (from + ((index/slices.length) * (to-from))) * 0.5, msg);
|
||||
}
|
||||
|
||||
// for each slice, performe a function and call doupdate()
|
||||
function forSlices(from, to, fn, msg) {
|
||||
slices.forEach(function(slice) {
|
||||
fn(slice);
|
||||
doupdate(slice.index, from, to, msg)
|
||||
});
|
||||
}
|
||||
|
||||
// do not hint polygin fill longer than a max span length
|
||||
CONF.hint_len_max = UTIL.sqr(spro.sliceBridgeMax);
|
||||
|
||||
// reset (if necessary) for solids and support projections
|
||||
slices.forEach(function(slice) {
|
||||
slice.extruder = extruder;
|
||||
slice.invalidateFill();
|
||||
slice.invalidateSolids();
|
||||
slice.invalidateSupports();
|
||||
});
|
||||
|
||||
let supportEnabled = spro.sliceSupportEnable && spro.sliceSupportDensity > 0.0,
|
||||
supportMinArea = spro.sliceSupportArea;
|
||||
|
||||
// create shells and diff inner fillable areas
|
||||
forSlices(0.0, 0.2, function(slice) {
|
||||
let solid = (
|
||||
slice.index < spro.sliceBottomLayers ||
|
||||
slice.index > slices.length - spro.sliceTopLayers-1 ||
|
||||
spro.sliceFillSparse > 0.95
|
||||
) && !vaseMode;
|
||||
slice.doShells(spro.sliceShells, firstOffset, shellOffset, fillOffset, {
|
||||
vase: vaseMode,
|
||||
thin: spro.detectThinWalls
|
||||
});
|
||||
if (solid) slice.doSolidLayerFill(fillSpacing, sliceFillAngle);
|
||||
sliceFillAngle += 90.0;
|
||||
}, "offsets");
|
||||
|
||||
// calculations only relevant when solid layers are used
|
||||
if (doSolidLayers) {
|
||||
forSlices(0.2, 0.34, function(slice) {
|
||||
slice.doDiff(minSolid);
|
||||
}, "diff");
|
||||
forSlices(0.34, 0.35, function(slice) {
|
||||
slice.projectFlats(solidLayers);
|
||||
slice.projectBridges(solidLayers);
|
||||
}, "solids");
|
||||
forSlices(0.35, 0.5, function(slice) {
|
||||
slice.doSolidsFill(fillSpacing, sliceFillAngle, minSolid);
|
||||
sliceFillAngle += 90.0;
|
||||
}, "solids");
|
||||
}
|
||||
|
||||
// calculations only relevant when supports are enabled
|
||||
if (supportEnabled) {
|
||||
forSlices(0.5, 0.7, function(slice) {
|
||||
slice.doSupport(spro.sliceSupportOffset, spro.sliceSupportSpan, spro.sliceSupportExtra, supportMinArea, spro.sliceSupportSize, spro.sliceSupportOffset, spro.sliceSupportGap);
|
||||
}, "support");
|
||||
forSlices(0.7, 0.8, function(slice) {
|
||||
slice.doSupportFill(nozzleSize, spro.sliceSupportDensity, supportMinArea);
|
||||
}, "support");
|
||||
}
|
||||
|
||||
// sparse layers only present when non-vase mose and sparse % > 0
|
||||
if (!vaseMode && spro.sliceFillSparse > 0.0) {
|
||||
forSlices(0.8, 1.0, function(slice) {
|
||||
slice.doSparseLayerFill({
|
||||
settings: settings,
|
||||
process: spro,
|
||||
device: sdev,
|
||||
lineWidth: nozzleSize,
|
||||
spacing: fillOffset,
|
||||
density: spro.sliceFillSparse,
|
||||
bounds: widget.getBoundingBox(),
|
||||
height: sliceHeight,
|
||||
type: spro.sliceFillType
|
||||
});
|
||||
}, "infill");
|
||||
}
|
||||
|
||||
// let polish = spro.polishLayers;
|
||||
// // experimental polishing
|
||||
// if (polish) {
|
||||
// let polish_layer = Math.floor(polish);
|
||||
// let polish_step = Math.max(polish - polish_layer || 1, 0.25);
|
||||
// widget.polish = {};
|
||||
// let px = [];
|
||||
// let py = [];
|
||||
// // compute x polishing slices
|
||||
// SLICER.sliceWidget(widget, {
|
||||
// height: nozzleSize * polish_step,
|
||||
// swapX: true,
|
||||
// swapY: false,
|
||||
// simple: true
|
||||
// }, (polish_done => {
|
||||
// widget.polish.x = polish_done
|
||||
// .filter(s => s.groups.length)
|
||||
// .map(s => s.groups)
|
||||
// .forEach(p => px.appendAll(p));
|
||||
// }), (polish_update) => {
|
||||
// // console.log({polish_update});
|
||||
// });
|
||||
// // compute y polishing slices
|
||||
// SLICER.sliceWidget(widget, {
|
||||
// height: nozzleSize * polish_step,
|
||||
// swapX: false,
|
||||
// swapY: true,
|
||||
// simple: true
|
||||
// }, (polish_done => {
|
||||
// widget.polish.y = polish_done
|
||||
// .filter(s => s.groups.length)
|
||||
// .map(s => s.groups)
|
||||
// .forEach(p => py.appendAll(p));
|
||||
// }), (polish_update) => {
|
||||
// // console.log({polish_update});
|
||||
// });
|
||||
// // apply polishing finishes to layers
|
||||
// forSlices(1.0, 1.0, (slice) => {
|
||||
// if (polish_layer >= 2) {
|
||||
// let sai = (slice.index - polish_layer);
|
||||
// if (sai % (polish_layer-1) !== 0) {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// if (slice.index >= polish_layer) {
|
||||
// let sd = slice;
|
||||
// for (let i=0; i<polish_layer; i++) {
|
||||
// sd = sd.down;
|
||||
// }
|
||||
// let zb = sd.z;
|
||||
// let zt = slice.z;
|
||||
// let pout = [];
|
||||
// let cont = 0;
|
||||
// [px, py].forEach(pa => {
|
||||
//
|
||||
// let polys = [];
|
||||
// pa.forEach(p => {
|
||||
// // rotate and test for self-intersection (points under model)
|
||||
// p.ensureXY();
|
||||
// let ox = p._aligned == 'yz' ? 0.1 : 0;
|
||||
// let oy = p._aligned == 'yz' ? 0 : 0.1;
|
||||
// p.forEachPoint(pt => {
|
||||
// let int = p.intersections(
|
||||
// pt.offset(ox,oy,0),
|
||||
// pt.offset(ox*10000,oy*10000,0));
|
||||
// if (int.length > 0) {
|
||||
// pt._under = true;
|
||||
// }
|
||||
// });
|
||||
// p.restoreXY();
|
||||
//
|
||||
// let lastp = undefined;
|
||||
// let poly = [];
|
||||
// // look for qualifying segments
|
||||
// p.forEachSegment((p1, p2) => {
|
||||
// // eliminate segments that projected up
|
||||
// // intersect with the polygon (bottom faces)
|
||||
// if (p1._under || p2._under) {
|
||||
// return;
|
||||
// }
|
||||
// // skip when both below layer range
|
||||
// if (p1.z < zb && p2.z < zb) {
|
||||
// return;
|
||||
// }
|
||||
// // skip when both above layer range
|
||||
// if (p1.z > zt && p2.z > zt) {
|
||||
// return;
|
||||
// }
|
||||
// // skip vertical
|
||||
// if (p1.x === p2.x && p1.y === p2.y) {
|
||||
// return;
|
||||
// }
|
||||
// // skip horizontal
|
||||
// // if (p1.z === p2.z) {
|
||||
// // return;
|
||||
// // }
|
||||
// // order points lowest to highest
|
||||
// let swap = false;
|
||||
// if (p1.z > p2.z) {
|
||||
// let t = p2;
|
||||
// p2 = p1;
|
||||
// p1 = t;
|
||||
// swap = true;
|
||||
// }
|
||||
// let trimlo = false;
|
||||
// let trimhi = false;
|
||||
// if (p1.z < zb) {
|
||||
// trimlo = true;
|
||||
// }
|
||||
// if (p2.z > zt) {
|
||||
// trimhi = true;
|
||||
// }
|
||||
// let xaxis = p1.x === p2.x;
|
||||
// if (xaxis) {
|
||||
// p1 = BASE.newPoint(p1.y,p1.z,p1.x);
|
||||
// p2 = BASE.newPoint(p2.y,p2.z,p2.x);
|
||||
// } else {
|
||||
// p1 = BASE.newPoint(p1.x,p1.z,p1.y);
|
||||
// p2 = BASE.newPoint(p2.x,p2.z,p2.y);
|
||||
// }
|
||||
// let slope = BASE.newSlope(p1, p2);
|
||||
// let angle = slope.angle;
|
||||
// if (angle > 80 && angle < 100) {
|
||||
// return;
|
||||
// }
|
||||
// let len = p1.distTo2D(p2);
|
||||
// let np1 = p1;
|
||||
// if (trimlo) {
|
||||
// let zunder = zb - p1.y;
|
||||
// let zover = p2.y - zb;
|
||||
// let zdelt = p2.y - p1.y;
|
||||
// let pct = zunder / zdelt;
|
||||
// np1 = p1.follow(slope, len * pct);
|
||||
// }
|
||||
// if (trimhi) {
|
||||
// let zunder = zt - p1.y;
|
||||
// let zover = p2.y - zt;
|
||||
// let zdelt = p2.y - p1.y;
|
||||
// let pct = zover / zdelt;
|
||||
// p2 = p2.follow(slope.invert(), len * pct);
|
||||
// }
|
||||
// p1 = np1;
|
||||
// if (xaxis) {
|
||||
// p1 = BASE.newPoint(p1.z,p1.x,p1.y);
|
||||
// p2 = BASE.newPoint(p2.z,p2.x,p2.y);
|
||||
// } else {
|
||||
// p1 = BASE.newPoint(p1.x,p1.z,p1.y);
|
||||
// p2 = BASE.newPoint(p2.x,p2.z,p2.y);
|
||||
// }
|
||||
// if (!lastp) {
|
||||
// poly.push(p1);
|
||||
// poly.push(p2);
|
||||
// } else if (p1.isMergable2D(lastp)) {
|
||||
// // } else if (p1.isEqual(lastp)) {
|
||||
// poly.push(p2);
|
||||
// cont++;
|
||||
// } else if (poly.length) {
|
||||
// polys.push(poly);
|
||||
// poly = [p1, p2];
|
||||
// }
|
||||
// lastp = p2;
|
||||
// });
|
||||
// if (poly.length) {
|
||||
// polys.push(poly);
|
||||
// }
|
||||
// });
|
||||
// pout.push(polys);
|
||||
// polys = [];
|
||||
// });
|
||||
//
|
||||
// if (pout.length && slice.tops.length) {
|
||||
// slice.tops[0].polish = {
|
||||
// x: pout[0]
|
||||
// .map(a => BASE.newPolygon(a).setOpen())
|
||||
// // .filter(p => p.perimeter() > nozzleSize)
|
||||
// ,
|
||||
// y: pout[1]
|
||||
// .map(a => BASE.newPolygon(a).setOpen())
|
||||
// // .filter(p => p.perimeter() > nozzleSize)
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// report slicing complete
|
||||
ondone();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function bound(v,min,max) {
|
||||
return Math.max(min,Math.min(max,v));
|
||||
}
|
||||
|
||||
})();
|
||||
188
src/mode/sla/client.js
Normal file
188
src/mode/sla/client.js
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
let KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
SLA = KIRI.driver.SLA;
|
||||
|
||||
SLA.init = function(kiri, api) {
|
||||
api.event.on("mode.set", (mode) => {
|
||||
if (mode === 'SLA') {
|
||||
api.ui.act.preview.classList.add('hide');
|
||||
} else {
|
||||
api.ui.act.preview.classList.remove('hide');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SLA.sliceRender = function(widget) {
|
||||
// legacy debug
|
||||
return;
|
||||
|
||||
widget.slices.forEach(slice => {
|
||||
let layers = slice.layers,
|
||||
outline = layers.outline,
|
||||
support = layers.support;
|
||||
|
||||
if (slice.solids.unioned) {
|
||||
// console.log('solid', slice.index)
|
||||
slice.solids.unioned.forEach(poly => {
|
||||
poly = poly.clone(true);//.move(widget.track.pos);
|
||||
outline.poly(poly, 0x010101, true);
|
||||
outline.solid(poly, 0x0099cc);
|
||||
});
|
||||
} else if (slice.tops) {
|
||||
// console.log('top', slice.index)
|
||||
slice.tops.forEach(top => {
|
||||
let poly = top.poly;//.clone(true).move(widget.track.pos);
|
||||
outline.poly(poly, 0x010101, true, false);
|
||||
outline.solid(poly, 0xfcba03);
|
||||
});
|
||||
}
|
||||
|
||||
if (slice.supports) {
|
||||
// console.log('support', slice.index)
|
||||
slice.supports.forEach(poly => {
|
||||
//poly = poly.clone(true).move(widget.track.pos);
|
||||
support.poly(poly, 0x010101, true, false);
|
||||
support.solid(poly, 0xfcba03);
|
||||
});
|
||||
}
|
||||
|
||||
slice.renderDiff();
|
||||
slice.renderSolidOutlines();
|
||||
|
||||
outline.renderAll();
|
||||
support.renderAll();
|
||||
});
|
||||
}
|
||||
|
||||
SLA.printRender = function(print) {
|
||||
let widgets = print.widgets,
|
||||
settings = print.settings,
|
||||
device = settings.device,
|
||||
process = settings.process;
|
||||
|
||||
for (let index=0; ; index++) {
|
||||
let layer = KIRI.newLayer(print.group);
|
||||
let count = 0;
|
||||
|
||||
widgets.forEach(widget => {
|
||||
let slice = widget.slices[index];
|
||||
if (!slice) {
|
||||
return;
|
||||
}
|
||||
count++;
|
||||
let polys = slice.solids.unioned;
|
||||
if (!polys) polys = slice.tops.map(t => t.poly);
|
||||
if (slice.supports) polys.appendAll(slice.supports);
|
||||
polys.forEach(poly => {
|
||||
poly = poly.clone(true).move(widget.track.pos);
|
||||
layer.poly(poly, 0x777777, true);
|
||||
layer.solid(poly, 0x0099cc);
|
||||
});
|
||||
});
|
||||
|
||||
layer.renderSolid();
|
||||
layer.render();
|
||||
|
||||
if (count === 0) {
|
||||
// TODO fix with contract for exposing layer count
|
||||
// hack uses expected gcode output array in print object
|
||||
print.output = print.printView;
|
||||
return;
|
||||
}
|
||||
|
||||
print.printView.push(layer);
|
||||
}
|
||||
}
|
||||
|
||||
SLA.printDownload = function(print) {
|
||||
let { API, lines, done } = print.sla;
|
||||
let filename = `print-${new Date().getTime().toString(36)}`;
|
||||
|
||||
API.ajax("/kiri/output-sla.html", html => {
|
||||
API.ui.print.innerHTML = html;
|
||||
|
||||
let printset = print.settings,
|
||||
process = printset.process,
|
||||
device = printset.device,
|
||||
print_sec = (process.slaBaseLayers * process.slaBaseOn) +
|
||||
(lines.length - process.slaBaseLayers) * process.slaLayerOn;
|
||||
|
||||
// add peel lift/drop times to total print time
|
||||
for (let i=0; i<lines.length; i++) {
|
||||
let dist = process.slaPeelDist,
|
||||
lift = process.slaPeelLiftRate,
|
||||
drop = process.slaPeelDropRate,
|
||||
off = process.slaLayerOff;
|
||||
if (i < process.slaBaseLayers) {
|
||||
dist = process.slaBasePeelDist;
|
||||
lift = process.slaBasePeelLiftRate;
|
||||
off = process.slaBaseOff;
|
||||
}
|
||||
print_sec += (dist * lift) / 60;
|
||||
print_sec += (dist * drop) / 60;
|
||||
print_sec += off;
|
||||
}
|
||||
|
||||
let print_min = Math.floor(print_sec/60),
|
||||
print_hrs = Math.floor(print_min/60),
|
||||
download = $('print-photon');
|
||||
|
||||
// add lift/drop time
|
||||
print_sec -= (print_min * 60);
|
||||
print_min -= (print_hrs * 60);
|
||||
print_sec = Math.round(print_sec).toString().padStart(2,'0');
|
||||
print_min = print_min.toString().padStart(2,'0');
|
||||
print_hrs = print_hrs.toString().padStart(2,'0');
|
||||
|
||||
$('print-filename').value = filename;
|
||||
$('print-layers').value = lines.length;
|
||||
$('print-time').value = `${print_hrs}:${print_min}:${print_sec}`;
|
||||
|
||||
switch (device.deviceName) {
|
||||
case 'Anycubic.Photon':
|
||||
download.innerText += " .photon";
|
||||
download.onclick = () => { saveFile(API, done.file, ".photon") };
|
||||
break;
|
||||
case 'Anycubic.Photon.S':
|
||||
download.innerText += " .photons";
|
||||
download.onclick = () => { saveFile(API, done.file, ".photons") };
|
||||
break;
|
||||
}
|
||||
|
||||
let canvas = $('print-canvas');
|
||||
let ctx = canvas.getContext('2d');
|
||||
let img = ctx.createImageData(done.height, done.width);
|
||||
let imgDV = new DataView(img.data.buffer);
|
||||
let range = $('print-range');
|
||||
range.value = 0;
|
||||
range.min = 0;
|
||||
range.max = lines.length - 1;
|
||||
range.oninput = function() {
|
||||
let lineDV = new DataView(lines[range.value]);
|
||||
for (let i=0; i<lineDV.byteLength; i++) {
|
||||
imgDV.setUint32(i*4, lineDV.getUint8(i));
|
||||
}
|
||||
ctx.putImageData(img,0,0);
|
||||
$('print-layer').innerText = range.value.padStart(4,'0');
|
||||
};
|
||||
|
||||
range.oninput();
|
||||
API.modal.show('print');
|
||||
});
|
||||
}
|
||||
|
||||
function saveFile(API, file, ext) {
|
||||
saveAs(
|
||||
new Blob([file], { type: "application/octet-stream" }),
|
||||
$('print-filename').value + ext);
|
||||
API.modal.hide();
|
||||
}
|
||||
|
||||
})();
|
||||
File diff suppressed because it is too large
Load diff
627
src/mode/sla/export.js
Normal file
627
src/mode/sla/export.js
Normal file
|
|
@ -0,0 +1,627 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
let KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
SLA = KIRI.driver.SLA;
|
||||
|
||||
/**
|
||||
* DRIVER PRINT CONTRACT - runs in worker
|
||||
* @param {Object} print state object
|
||||
* @param {Function} online streaming reply
|
||||
* @param {Function} ondone last reply
|
||||
*/
|
||||
SLA.printExport = function(print, online, ondone) {
|
||||
let widgets = print.widgets,
|
||||
settings = print.settings,
|
||||
device = settings.device,
|
||||
process = settings.process,
|
||||
output = print.output,
|
||||
layermax = 0,
|
||||
width = 2560,
|
||||
height = 1440,
|
||||
width2 = width/2,
|
||||
height2 = height/2,
|
||||
scaleX = width / device.bedWidth,
|
||||
scaleY = height / device.bedDepth,
|
||||
mark = Date.now(),
|
||||
layers = process.slaAntiAlias || 1,
|
||||
masks = [],
|
||||
images = [],
|
||||
slices = [],
|
||||
legacyMode = SLA.legacy || layers > 1,
|
||||
part1 = legacyMode ? 0.25 : 0.85,
|
||||
part2 = legacyMode ? 0.75 : 0.15;
|
||||
|
||||
let d = 8 / layers;
|
||||
for (let i=0; i<layers; i++) {
|
||||
masks.push((1 << (8 - i * d)) - 1);
|
||||
}
|
||||
|
||||
// find max layer count
|
||||
widgets.forEach(widget => {
|
||||
layermax = Math.max(widget.slices.length);
|
||||
});
|
||||
|
||||
let render = legacyMode ? renderLayer : renderLayerWasm;
|
||||
|
||||
// generate layer bitmaps
|
||||
// in wasm mode, rle layers generated here, too
|
||||
for (let index=0; index < layermax; index++) {
|
||||
let param = { index, width, height, widgets, scaleX, scaleY, masks };
|
||||
let {image, layers, end} = render(param);
|
||||
images.push(image);
|
||||
slices.push(layers);
|
||||
// transfer images to browser main
|
||||
image = image.buffer;
|
||||
online({
|
||||
progress: (index / layermax) * part1,
|
||||
message: "image_gen",
|
||||
data: image
|
||||
},[image]);
|
||||
if (end) break;
|
||||
}
|
||||
|
||||
let exp_func;
|
||||
|
||||
switch (device.deviceName) {
|
||||
case 'Anycubic.Photon':
|
||||
exp_func = generatePhoton;
|
||||
break;
|
||||
case 'Anycubic.Photon.S':
|
||||
exp_func = generatePhotons;
|
||||
break;
|
||||
}
|
||||
|
||||
let file = exp_func(print, {
|
||||
width: width,
|
||||
height: height,
|
||||
small: SLA.previewSmall.data,
|
||||
large: SLA.previewLarge.data,
|
||||
lines: images,
|
||||
slices: slices
|
||||
}, (progress, message) => {
|
||||
online({progress: progress * part2 + part1, message});
|
||||
});
|
||||
ondone({
|
||||
width: width,
|
||||
height: height,
|
||||
file: file
|
||||
},[file]);
|
||||
|
||||
console.log('print.export', Date.now() - mark);
|
||||
};
|
||||
|
||||
function generatePhoton(print, conf, progress) {
|
||||
let printset = print.settings,
|
||||
process = printset.process,
|
||||
device = printset.device,
|
||||
width = conf.width,
|
||||
height = conf.height,
|
||||
layerCount = conf.lines.length,
|
||||
layerBytes = width * height,
|
||||
small = conf.small,
|
||||
large = conf.large,
|
||||
slices = conf.slices,
|
||||
subcount = process.slaAntiAlias || 1,
|
||||
masks = [],
|
||||
coded;
|
||||
|
||||
if (SLA.legacy || subcount > 1) {
|
||||
let d = 8 / subcount;
|
||||
for (let i=0; i<subcount; i++) {
|
||||
masks.push((1 << (8 - i * d)) - 1);
|
||||
}
|
||||
let ccl = 0;
|
||||
let tcl = conf.lines.length * subcount;
|
||||
let converted = conf.lines.map((line, index) => {
|
||||
let count = line.length;
|
||||
let lineDV = new DataView(line.buffer);
|
||||
let bits = new Uint8Array(line.length);
|
||||
let bitsDV = new DataView(bits.buffer);
|
||||
let subs = [{ data: bits, view: bitsDV }];
|
||||
for (let sl=1; sl<subcount; sl++) {
|
||||
bits = bits.slice();
|
||||
bitsDV = new DataView(bits.buffer);
|
||||
subs.push({ data: bits, view: bitsDV });
|
||||
}
|
||||
// use R from RGB since that was painted on the canvas
|
||||
for (let s=0; s<subcount; s++) {
|
||||
let view = subs[s].view;
|
||||
let mask = masks[s];
|
||||
for (let i = 0; i < count; i++) {
|
||||
let dv = lineDV.getUint8(i);
|
||||
view.setUint8(i, (dv / subcount) & mask ? 1 : 0);
|
||||
}
|
||||
progress((ccl++/tcl) * 0.4, `layer_convert`);
|
||||
}
|
||||
return { subs };
|
||||
});
|
||||
|
||||
coded = encodeLayers(converted, "photon", (pro => {
|
||||
progress(pro * 0.4 + 0.4, "layer_encode");
|
||||
}));
|
||||
} else {
|
||||
let codedlen = slices.reduce((t,l) => {
|
||||
return t + l.reduce((t,a) => {
|
||||
return t + a.length
|
||||
}, 0);
|
||||
}, 0);
|
||||
coded = {
|
||||
layers: slices.map(slice => { return { sublayers: slice }}),
|
||||
length: codedlen
|
||||
};
|
||||
}
|
||||
|
||||
let codelen = coded.layers.length;
|
||||
let buflen = 3000 + coded.length + (codelen * subcount * 28) + small.byteLength + large.byteLength;
|
||||
let filebuf = new ArrayBuffer(buflen);
|
||||
let filedat = new self.DataWriter(new DataView(filebuf));
|
||||
let printtime = (process.slaBaseLayers * process.slaBaseOn) +
|
||||
(coded.layers.length - process.slaBaseLayers) * process.slaLayerOn;
|
||||
|
||||
filedat.writeU32(0x1900fd12); // header
|
||||
filedat.writeU32(2,true); // version
|
||||
filedat.writeF32(68.04, true); // bed x
|
||||
filedat.writeF32(120.96, true); // bed y
|
||||
filedat.writeF32(150.0, true); // bed z
|
||||
filedat.skip(12); // padding
|
||||
filedat.writeF32(process.slaSlice, true); // layer height
|
||||
filedat.writeF32(process.slaLayerOn, true); // default lamp on
|
||||
filedat.writeF32(process.slaBaseOn, true); // base lamp on
|
||||
filedat.writeF32(process.slaLayerOff, true); // lamp off
|
||||
filedat.writeU32(process.slaBaseLayers, true); // base layers
|
||||
filedat.writeU32(1440, true); // device x
|
||||
filedat.writeU32(2560, true); // device y
|
||||
let hirez = filedat.skip(4); // hirez preview address filled pater
|
||||
let layerpos = filedat.skip(4); // layer data address filled later
|
||||
filedat.writeU32(codelen, true);
|
||||
let lorez = filedat.skip(4); // hirez preview address filled later
|
||||
filedat.writeU32(printtime, true); // print time seconds
|
||||
filedat.writeU32(1, true); // projection type (1=lcd, 0=cast)
|
||||
let proppos = filedat.skip(4); // print properties address filled later
|
||||
let proplen = filedat.skip(4); // print properties length filled later
|
||||
filedat.writeU32(subcount, true); // AA level (sub layers)
|
||||
filedat.writeU16(0x00ff, true); // light pwm (TODO);
|
||||
filedat.writeU16(0x00ff, true); // light pwm bottom (TODO);
|
||||
|
||||
let propstart = filedat.pos;
|
||||
filedat.view.setUint32(proppos, filedat.pos, true);
|
||||
// write print properties
|
||||
filedat.writeF32(process.slaBasePeelDist, true);
|
||||
filedat.writeF32(process.slaBasePeelLiftRate * 60 , true);
|
||||
filedat.writeF32(process.slaPeelDist, true);
|
||||
filedat.writeF32(process.slaPeelLiftRate * 60 , true);
|
||||
filedat.writeF32(process.slaPeelDropRate * 60, true);
|
||||
filedat.writeF32(0, true); // volume of used
|
||||
filedat.writeF32(0, true); // weight of used
|
||||
filedat.writeF32(0, true); // cost of used
|
||||
filedat.writeF32(0, true); // bottom off delay time
|
||||
filedat.writeF32(0, true); // light off delay time
|
||||
filedat.writeU32(process.slaBaseLayers, true);
|
||||
filedat.writeF32(0, true); // p1 ?
|
||||
filedat.writeF32(0, true); // p2 ?
|
||||
filedat.writeF32(0, true); // p3 ?
|
||||
filedat.writeF32(0, true); // p4 ?
|
||||
filedat.view.setUint32(proplen, filedat.pos - propstart, true);
|
||||
|
||||
filedat.view.setUint32(layerpos, filedat.pos, true);
|
||||
// write layer headers
|
||||
let layers = coded.layers;
|
||||
let layerat = [];
|
||||
|
||||
for (let sc=0; sc<subcount; sc++)
|
||||
for (let l=0; l<layers.length; l++) {
|
||||
let layer = layers[l].sublayers[sc];
|
||||
filedat.writeF32(process.slaFirstOffset + process.slaSlice * l, true); // layer height
|
||||
filedat.writeF32(l < process.slaBaseLayers ? process.slaBaseOn : process.slaLayerOn, true);
|
||||
filedat.writeF32(l < process.slaBaseLayers ? process.slaBaseOff : process.slaLayerOff, true);
|
||||
layerat.push(layer.repos = filedat.skip(4)); // rewrite later
|
||||
filedat.writeU32(layer.length, true);
|
||||
filedat.skip(16); // padding
|
||||
}
|
||||
|
||||
// write layer data
|
||||
let clo = 0;
|
||||
let tlo = layers.length * subcount;
|
||||
for (let sc=0; sc<subcount; sc++)
|
||||
for (let l=0; l<layers.length; l++) {
|
||||
let layer = layers[l].sublayers[sc];
|
||||
filedat.view.setUint32(layer.repos, filedat.pos, true);
|
||||
for (let j=0; j<layer.length; j++) {
|
||||
filedat.writeU8(layer[j], false);
|
||||
}
|
||||
progress(((clo++/tlo) * 0.1) + 0.9, "layer_write");
|
||||
}
|
||||
|
||||
filedat.view.setUint32(hirez, filedat.pos, true);
|
||||
writePhotonImage({
|
||||
width: 400,
|
||||
height: 300,
|
||||
data: conf.large
|
||||
}, filedat);
|
||||
|
||||
filedat.view.setUint32(lorez, filedat.pos, true);
|
||||
writePhotonImage({
|
||||
width: 200,
|
||||
height: 125,
|
||||
data: conf.small
|
||||
}, filedat);
|
||||
|
||||
return filebuf;
|
||||
}
|
||||
|
||||
function generatePhotons(print, conf, progress) {
|
||||
let printset = print.settings,
|
||||
process = printset.process,
|
||||
device = printset.device,
|
||||
width = conf.width,
|
||||
height = conf.height,
|
||||
slices = conf.slices,
|
||||
layerCount = conf.lines.length,
|
||||
layerBytes = width * height,
|
||||
coded;
|
||||
|
||||
if (legacy) {
|
||||
let converted = conf.lines.map((line, index) => {
|
||||
let count = line.length / 4;
|
||||
let bits = new Uint8Array(line.length / 4);
|
||||
let bitsDV = new DataView(bits.buffer);
|
||||
let lineDV = new DataView(line.buffer);
|
||||
// reduce RGB to R = 0||1
|
||||
for (let i = 0; i < count; i++) {
|
||||
// defeat anti-aliasing for the moment
|
||||
bitsDV.setUint8(i, lineDV.getUint8(i * 4) > 0 ? 1 : 0);
|
||||
}
|
||||
progress(index / conf.lines.length);
|
||||
return { subs: [{
|
||||
exposureTime: process.slaLayerOn,
|
||||
data: bits
|
||||
}] };
|
||||
});
|
||||
coded = encodeLayers(converted, "photons");
|
||||
} else {
|
||||
let codedlen = slices.reduce((t,l) => {
|
||||
return t + l.reduce((t,a) => {
|
||||
return t + a.length
|
||||
}, 0);
|
||||
}, 0);
|
||||
coded = {
|
||||
layers: slices.map(slice => { return { sublayers: slice }}),
|
||||
length: codedlen
|
||||
};
|
||||
}
|
||||
|
||||
let filebuf = new ArrayBuffer(75366 + coded.length + 28 * layerCount);
|
||||
let filedat = new DataView(filebuf);
|
||||
let filePos = 0;
|
||||
|
||||
filedat.setUint32 (0, 2, false);
|
||||
filedat.setUint32 (4, 3227560, false);
|
||||
filedat.setUint32 (8, 824633720, false);
|
||||
filedat.setUint16 (12, 10, false);
|
||||
filedat.setFloat64(14, process.slaSlice, false);
|
||||
filedat.setFloat64(22, process.slaLayerOn, false);
|
||||
filedat.setFloat64(30, process.slaLayerOff, false);
|
||||
filedat.setFloat64(38, process.slaBaseOn, false);
|
||||
filedat.setUint32 (46, process.slaBaseLayers, false);
|
||||
filedat.setFloat64(50, process.slaPeelDist, false);
|
||||
filedat.setFloat64(58, process.slaPeelLift, false);
|
||||
filedat.setFloat64(66, process.slaPeelDrop, false);
|
||||
filedat.setFloat64(74, 69420, false);
|
||||
filedat.setUint32 (82, 224, false);
|
||||
filedat.setUint32 (86, 42, false);
|
||||
filedat.setUint32 (90, 168, false);
|
||||
filedat.setUint32 (94, 10, false);
|
||||
filedat.setUint32 (75362, layerCount, false);
|
||||
|
||||
filePos = 75366;
|
||||
for (let i = 0; i < layerCount; i++) {
|
||||
let layer = coded.layers[i],
|
||||
sublayer = layer.sublayers[0],
|
||||
numbytes = sublayer.length;
|
||||
|
||||
filedat.setUint32 (filePos + 0, 69420, false);
|
||||
filedat.setFloat64(filePos + 4, 0);
|
||||
filedat.setUint32 (filePos + 12, height, false);
|
||||
filedat.setUint32 (filePos + 16, width, false);
|
||||
filedat.setUint32 (filePos + 20, numbytes * 8 + 32, false);
|
||||
filedat.setUint32 (filePos + 24, 2684702720, false);
|
||||
filePos += 28;
|
||||
for (let j = 0; j < numbytes; j++) {
|
||||
filedat.setUint8(filePos + j, sublayer[j], false);
|
||||
}
|
||||
filePos += numbytes;
|
||||
progress((i / layerCount) / 2 + 0.5);
|
||||
}
|
||||
|
||||
return filebuf;
|
||||
}
|
||||
|
||||
function encodeLayers(input, type, progress) {
|
||||
let layers = [], length = 0, total = 0, count = 0;
|
||||
input.forEach(layer => {
|
||||
layer.subs.forEach(sub => total++);
|
||||
});
|
||||
for (let index = 0; index < input.length; index++) {
|
||||
let subs = input[index].subs,
|
||||
sublayers = [],
|
||||
sublength = 0;
|
||||
for (let subindex = 0; subindex < subs.length; subindex++) {
|
||||
let data = subs[subindex].data;
|
||||
let encoded = rleEncode(data, type);
|
||||
sublength += encoded.length;
|
||||
sublayers.push(encoded);
|
||||
if (progress) progress(count++/total);
|
||||
if (type == "photons") break;
|
||||
}
|
||||
length += sublength;
|
||||
layers.push({
|
||||
sublength,
|
||||
sublayers
|
||||
});
|
||||
}
|
||||
return { length, layers };
|
||||
}
|
||||
|
||||
function rleEncode(data, type) {
|
||||
let maxlen = (type === 'photons') ? 128 : 125,
|
||||
color = data[0],
|
||||
runlen = 1,
|
||||
output = [];
|
||||
for (let index = 1; index < data.length; index++) {
|
||||
let newColor = data[index];
|
||||
if (newColor !== color) {
|
||||
output.push(rleByte(color, runlen, type));
|
||||
color = newColor;
|
||||
runlen = 1;
|
||||
} else {
|
||||
if (runlen === maxlen) {
|
||||
output.push(rleByte(color, runlen, type));
|
||||
runlen = 1;
|
||||
} else {
|
||||
runlen++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (runlen > 0) {
|
||||
output.push(rleByte(color, runlen, type));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function rleByte(color, length, type) {
|
||||
switch (type) {
|
||||
case 'photon':
|
||||
return (length & 0x7f) | ((color << 7) & 0x80);
|
||||
case 'photons':
|
||||
length--;
|
||||
return (length & 1 ? 128 : 0) |
|
||||
(length & 2 ? 64 : 0) |
|
||||
(length & 4 ? 32 : 0) |
|
||||
(length & 8 ? 16 : 0) |
|
||||
(length & 16 ? 8 : 0) |
|
||||
(length & 32 ? 4 : 0) |
|
||||
(length & 64 ? 2 : 0) | color;
|
||||
}
|
||||
}
|
||||
|
||||
function rleDecode(data, type) {
|
||||
let bytes = [];
|
||||
if (type === 'photon') {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let val = data[i],
|
||||
color = val >> 7,
|
||||
count = val & 0x7f;
|
||||
for (let j = 0; j < count; j++) {
|
||||
bytes.push(color);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let val = data[i],
|
||||
color = val & 1,
|
||||
count =
|
||||
((val & 128 ? 1 : 0) |
|
||||
(val & 64 ? 2 : 0) |
|
||||
(val & 32 ? 4 : 0) |
|
||||
(val & 16 ? 8 : 0) |
|
||||
(val & 8 ? 16 : 0) |
|
||||
(val & 4 ? 32 : 0) |
|
||||
(val & 2 ? 64 : 0)) + 1;
|
||||
for (let j = 0; j < count; j++) {
|
||||
bytes.push(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// write out a thumbnail image
|
||||
function writePhotonImage(preview, writer) {
|
||||
let data = new Uint8Array(preview.data), len = data.byteLength;
|
||||
writer.writeU32(preview.width, true);
|
||||
writer.writeU32(preview.height, true);
|
||||
let hpos = writer.skip(4);
|
||||
writer.writeU32(len/2, true);
|
||||
writer.view.setUint32(hpos, writer.pos, true);
|
||||
let pos = 0;
|
||||
while (pos < len) {
|
||||
let r = data[pos++],
|
||||
g = data[pos++],
|
||||
b = data[pos++],
|
||||
a = data[pos++],
|
||||
v = (((r/4)&0x1f) << 11) |
|
||||
(((g/4)&0x1f) << 6) |
|
||||
(((b/4)&0x1f) << 0) ;
|
||||
writer.writeU16(v, true);
|
||||
}
|
||||
}
|
||||
|
||||
// load renderer code in worker context only
|
||||
fetch('/wasm/kiri-sla.wasm')
|
||||
.then(response => response.arrayBuffer())
|
||||
.then(bytes => WebAssembly.instantiate(bytes, {
|
||||
env: {
|
||||
reportf: (a,b) => { console.log('[f]',a,b) },
|
||||
reporti: (a,b) => { console.log('[i]',a,b) }
|
||||
}
|
||||
}))
|
||||
.then(results => {
|
||||
let {module, instance} = results;
|
||||
let {exports} = instance;
|
||||
let heap = new Uint8Array(exports.memory.buffer);
|
||||
self.wasm = {
|
||||
heap,
|
||||
memory: exports.memory,
|
||||
render: exports.render,
|
||||
rle_encode: exports.rle_encode
|
||||
};
|
||||
});
|
||||
|
||||
// new WebAssembly rasterizer
|
||||
function renderLayerWasm(params) {
|
||||
let { width, height, index, widgets, scaleX, scaleY, masks } = params;
|
||||
let width2 = width / 2, height2 = height / 2;
|
||||
let array = [];
|
||||
let count = 0;
|
||||
let wasm = self.wasm;
|
||||
|
||||
function scaleMovePoly(poly) {
|
||||
let points = poly.points;
|
||||
let bounds = poly.bounds = BASE.newBounds();
|
||||
for (let i=0, il=points.length; i<il; i++) {
|
||||
let p = points[i];
|
||||
p.y = height - (p.y * scaleY + height2);
|
||||
p.x = p.x * scaleX + width2;
|
||||
bounds.update(p);
|
||||
}
|
||||
if (poly.inner) {
|
||||
for (let i=0, ia=poly.inner, il=poly.inner.length; i<il; i++) {
|
||||
scaleMovePoly(ia[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// serialize poly into wasm heap memory
|
||||
function writePoly(writer, poly) {
|
||||
let pos = writer.skip(2);
|
||||
let inner = poly.inner;
|
||||
writer.writeU16(inner ? inner.length : 0, true);
|
||||
let points = poly.points;
|
||||
let bounds = poly.bounds;
|
||||
writer.writeU16(points.length, true);
|
||||
writer.writeU16(bounds.minx, true);
|
||||
writer.writeU16(bounds.maxx, true);
|
||||
writer.writeU16(bounds.miny, true);
|
||||
writer.writeU16(bounds.maxy, true);
|
||||
for (let j=0, jl=points.length; j<jl; j++) {
|
||||
let point = points[j];
|
||||
writer.writeF32(point.x, true);
|
||||
writer.writeF32(point.y, true);
|
||||
}
|
||||
if (inner && inner.length) {
|
||||
for (let i=0, il=inner.length; i<il; i++) {
|
||||
writePoly(writer, inner[i]);
|
||||
}
|
||||
}
|
||||
// write total struct length at struct head
|
||||
writer.view.setUint16(pos, writer.pos - pos, true);
|
||||
}
|
||||
|
||||
widgets.forEach(widget => {
|
||||
let slice = widget.slices[index];
|
||||
if (slice) {
|
||||
if (slice.synth) count++;
|
||||
let polys = slice.solids.unioned;
|
||||
if (!polys) polys = slice.tops.map(t => t.poly);
|
||||
if (slice.supports) polys.appendAll(slice.supports);
|
||||
array.appendAll(polys.map(poly => {
|
||||
return poly.clone(true).move(widget.track.pos);
|
||||
}));
|
||||
count += polys.length;
|
||||
}
|
||||
});
|
||||
|
||||
let imagelen = width * height;
|
||||
let writer = new self.DataWriter(new DataView(wasm.memory.buffer), imagelen);
|
||||
writer.writeU16(width, true);
|
||||
writer.writeU16(height, true);
|
||||
writer.writeU16(array.length, true);
|
||||
|
||||
// scale and move all polys to fit in rendered platform coordinates
|
||||
for (let i=0, il=array.length; i<il; i++) {
|
||||
let poly = array[i];
|
||||
scaleMovePoly(poly);
|
||||
writePoly(writer, poly);
|
||||
}
|
||||
wasm.render(0, imagelen, 0);
|
||||
let image = wasm.heap.slice(0, imagelen), layers = [];
|
||||
// one rle encoded bitstream for each mash (anti-alias sublayer)
|
||||
for (let l=0; l<masks.length; l++) {
|
||||
// while the image is still in wasm heap memory, rle encode it
|
||||
let rlelen = wasm.rle_encode(0, 0, imagelen, masks[l], imagelen, 0);
|
||||
layers.push(wasm.heap.slice(imagelen, imagelen + rlelen));
|
||||
}
|
||||
|
||||
return { image, layers, end: count === 0 };
|
||||
}
|
||||
|
||||
// legacy JS-only rasterizer uses OffscreenCanvas
|
||||
function renderLayer(params) {
|
||||
let {width, height, index, widgets, scaleX, scaleY} = params;
|
||||
let layer = new OffscreenCanvas(height,width);
|
||||
let opt = { scaleX, scaleY, width, height, width2: width/2, height2: height/2 };
|
||||
let ctx = layer.getContext('2d');
|
||||
ctx.fillStyle = 'rgb(200, 0, 0)';
|
||||
let count = 0;
|
||||
widgets.forEach(widget => {
|
||||
let slice = widget.slices[index];
|
||||
if (slice) {
|
||||
// prevent premature exit on empty synth slice
|
||||
if (slice.synth) count++;
|
||||
let polys = slice.solids.unioned;
|
||||
if (!polys) polys = slice.tops.map(t => t.poly);
|
||||
if (slice.supports) polys.appendAll(slice.supports);
|
||||
polys.forEach(poly => {
|
||||
poly.move(widget.track.pos);
|
||||
ctx.beginPath();
|
||||
polyout(poly.setClockwise(), ctx, opt);
|
||||
if (poly.inner) {
|
||||
poly.inner.forEach(inner => {
|
||||
polyout(inner.setCounterClockwise(), ctx, opt);
|
||||
});
|
||||
}
|
||||
ctx.fill();
|
||||
count++;
|
||||
});
|
||||
} else {
|
||||
// console.log({no_slice_at: index})
|
||||
}
|
||||
});
|
||||
let data = ctx.getImageData(0,0,height,width).data;
|
||||
// reduce RGBA to R
|
||||
let red = new Uint8ClampedArray(data.length / 4);
|
||||
for (let i=0; i<red.length; i++) {
|
||||
red[i] = data[i*4];
|
||||
}
|
||||
return { image: red, end: count === 0 };
|
||||
}
|
||||
|
||||
function polyout(poly, ctx, opt) {
|
||||
let { scaleX, scaleY, width, height, width2, height2 } = opt;
|
||||
poly.forEachPoint((p,i) => {
|
||||
if (i === 0) {
|
||||
ctx.moveTo(height - (p.y * scaleY + height2), p.x * scaleX + width2);
|
||||
} else {
|
||||
ctx.lineTo(height - (p.y * scaleY + height2), p.x * scaleX + width2);
|
||||
}
|
||||
}, true);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
})();
|
||||
622
src/mode/sla/slice.js
Normal file
622
src/mode/sla/slice.js
Normal file
|
|
@ -0,0 +1,622 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
let KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
SLA = KIRI.driver.SLA,
|
||||
SLICER = KIRI.slicer,
|
||||
newTop = KIRI.newTop,
|
||||
newSlice = KIRI.newSlice,
|
||||
newPoint = BASE.newPoint,
|
||||
newPolygon = BASE.newPolygon,
|
||||
fill_cache;
|
||||
|
||||
/**
|
||||
* DRIVER SLICE CONTRACT - runs in worker
|
||||
*
|
||||
* @param {Object} settings
|
||||
* @param {Widget} Widget
|
||||
* @param {Function} onupdate (called with % complete and optional message)
|
||||
* @param {Function} ondone (called when complete with an array of Slice objects)
|
||||
*/
|
||||
SLA.slice = function(settings, widget, onupdate, ondone) {
|
||||
let process = settings.process,
|
||||
device = settings.device,
|
||||
work_total,
|
||||
work_remain;
|
||||
|
||||
if (SLA.legacy && !self.OffscreenCanvas) {
|
||||
return ondone("browser lacks support for OffscreenCanvas",true);
|
||||
}
|
||||
|
||||
// calculate % complete and call onupdate()
|
||||
function doupdate(work, msg) {
|
||||
onupdate(0.25 + ((work_total - work_remain) / work_total) * 0.75, msg);
|
||||
work_remain -= work;
|
||||
}
|
||||
|
||||
// for each slice, perform a function and call doupdate()
|
||||
function forSlices(slices, work, fn, msg) {
|
||||
slices.forEach(function(slice,index) {
|
||||
fn(slice,index);
|
||||
doupdate(work / slices.length, msg)
|
||||
});
|
||||
}
|
||||
|
||||
let b64 = atob(self.worker.snap);
|
||||
let bin = Uint8Array.from(b64, c => c.charCodeAt(0));
|
||||
let img = new png.PNG();
|
||||
img.parse(bin, (err, data) => {
|
||||
SLA.preview = img;
|
||||
SLA.previewSmall = samplePNG(img, 200, 125);
|
||||
SLA.previewLarge = samplePNG(img, 400, 300);
|
||||
});
|
||||
let height = process.slaSlice || 0.05;
|
||||
|
||||
SLICER.sliceWidget(widget, {
|
||||
height: height,
|
||||
add: !process.slaOpenTop
|
||||
}, function(slices) {
|
||||
// hold onto last (empty) slice
|
||||
let last = slices[slices.length-1];
|
||||
// remove empty slices
|
||||
slices = widget.slices = slices.filter(slice => slice.tops.length);
|
||||
if (!process.slaOpenTop) {
|
||||
// re-add last empty slice for open top
|
||||
slices.push(last);
|
||||
}
|
||||
// prepend raft layers to slices array
|
||||
if (process.slaSupportEnable && process.slaSupportLayers) {
|
||||
let layers = process.slaSupportLayers,
|
||||
zoff = height / 2,
|
||||
snew = [],
|
||||
polys = [],
|
||||
gap = process.slaSupportGap, // gap layers above raft
|
||||
grow = height, // union per layer expand
|
||||
off = 1 - (layers * grow); // starting union offset from part
|
||||
let outer = slices.forEach(slice => {
|
||||
// poly.clone prevents inner voids from forming
|
||||
polys.appendAll(slice.tops.map(t => t.poly.clone()));
|
||||
});
|
||||
// p.clone prevents inner voids from forming
|
||||
let union = POLY.union(polys).map(p => p.clone());
|
||||
let expand = POLY.expand(union, off, zoff, [], 1);
|
||||
let lastraft;
|
||||
for (let s=0; s<layers + gap; s++) {
|
||||
let slice = newSlice(zoff);
|
||||
slice.height = height;
|
||||
slice.index = snew.length;
|
||||
if (s < layers) {
|
||||
slice.synth = true;
|
||||
expand.forEach(u => {
|
||||
slice.tops.push(newTop(u.clone(true).setZ(zoff)));
|
||||
});
|
||||
expand = POLY.expand(expand, grow, zoff, [], 1);
|
||||
lastraft = slice;
|
||||
}
|
||||
snew.push(slice);
|
||||
zoff += height;
|
||||
}
|
||||
// compensate for midline start
|
||||
zoff -= height / 2;
|
||||
// replace slices with new appended array
|
||||
slices = widget.slices = snew.concat(slices.map(s => {
|
||||
s.tops.forEach(t => t.poly.setZ(s.z + zoff));
|
||||
s.index += snew.length;
|
||||
s.z += zoff;
|
||||
return s;
|
||||
}));
|
||||
// annotate widget for support generation
|
||||
widget.union = union;
|
||||
widget.lastraft = lastraft;
|
||||
}
|
||||
// re-connect slices into linked list for island/bridge projections
|
||||
for (let i=1; i<slices.length; i++) {
|
||||
slices[i-1].up = slices[i];
|
||||
slices[i].down = slices[i-1];
|
||||
}
|
||||
// reset for solids and support projections
|
||||
slices.forEach(function(slice) {
|
||||
slice.invalidateFill();
|
||||
slice.invalidateSolids();
|
||||
slice.invalidateSupports();
|
||||
slice.isSolidFill = false;
|
||||
});
|
||||
let solidLayers = Math.round(process.slaShell / process.slaSlice);
|
||||
work_total = [
|
||||
5, // shell
|
||||
10, // diff
|
||||
solidLayers ? 10 : 0, // shell project
|
||||
solidLayers ? 10 : 0, // shell fill
|
||||
!solidLayers ? 10 : 0, // solid
|
||||
process.slaFillDensity && process.slaShell ? 60 : 0, // infill
|
||||
process.slaSupportEnable && process.slaSupportLayers && process.slaSupportDensity ? 100 : 0
|
||||
].reduce((t,v) => { return t+v });
|
||||
work_remain = work_total;
|
||||
forSlices(slices, 5, (slice,index) => {
|
||||
if (process.slaShell) {
|
||||
slice.doShells(2, 0, process.slaShell);
|
||||
} else {
|
||||
slice.doShells(1, 0);
|
||||
}
|
||||
}, "slice");
|
||||
forSlices(slices, 10, (slice) => {
|
||||
if (slice.synth) return;
|
||||
slice.doDiff(0.000001, 0.005, !process.slaOpenBase);
|
||||
}, "delta");
|
||||
if (solidLayers) {
|
||||
forSlices(slices, 10, (slice) => {
|
||||
if (slice.synth) return;
|
||||
slice.projectFlats(solidLayers);
|
||||
slice.projectBridges(solidLayers);
|
||||
}, "project");
|
||||
forSlices(slices, 10, (slice) => {
|
||||
if (slice.synth) return;
|
||||
slice.doSolidsFill(undefined, undefined, 0.001);
|
||||
let traces = POLY.nest(POLY.flatten(slice.gatherTraces([])));
|
||||
let trims = slice.solids.trimmed || [];
|
||||
traces.appendAll(trims);
|
||||
let union = POLY.union(traces);
|
||||
slice.solids.unioned = union;
|
||||
}, "solid");
|
||||
} else {
|
||||
forSlices(slices, 10, (slice) => {
|
||||
if (slice.synth) return;
|
||||
slice.solids.unioned = slice.gatherTopPolys([]);
|
||||
}, "solid");
|
||||
}
|
||||
if (process.slaFillDensity && process.slaShell) {
|
||||
fill_cache = [];
|
||||
forSlices(slices, 60, (slice) => {
|
||||
if (slice.synth) return;
|
||||
fillPolys(slice, settings);
|
||||
}, "infill");
|
||||
}
|
||||
if (process.slaSupportEnable && process.slaSupportLayers && process.slaSupportDensity) {
|
||||
computeSupports(widget, process, progress => {
|
||||
doupdate(100 * progress, "support");
|
||||
});
|
||||
}
|
||||
ondone();
|
||||
}, function(update) {
|
||||
return onupdate(0.0 + update * 0.25);
|
||||
});
|
||||
};
|
||||
|
||||
function computeSupports(widget, process, progress) {
|
||||
let area = widget.union.reduce((t,p) => { return t + p.areaDeep() }, 0),
|
||||
perim = widget.union.reduce((t,p) => { return t + p.perimeter() }, 0),
|
||||
slices = widget.slices,
|
||||
length = slices.length,
|
||||
tot_mass = 0, // total widget "mass"
|
||||
tot_bear = 0; // total "mass-bearing" area
|
||||
|
||||
let ops = slices.filter(slice => !slice.synth);
|
||||
|
||||
// compute total "mass" by slice
|
||||
ops.forEach(slice => {
|
||||
slice.mass = slice.solids.unioned.reduce((t,p) => { return t + p.areaDeep() }, 0);
|
||||
if (slice.up && slice.up.bridges) {
|
||||
slice.bear = slice.up.bridges.reduce((t,p) => { return t + p.areaDeep() }, 0);
|
||||
slice.bear_up = slice.up.bridges;
|
||||
tot_bear += slice.bear;
|
||||
} else {
|
||||
slice.bear = 0;
|
||||
}
|
||||
tot_mass += slice.mass;
|
||||
});
|
||||
|
||||
let mass_per_bear = (tot_mass / tot_bear) * (1 / process.slaSupportDensity);
|
||||
|
||||
// console.log({tot_mass, tot_bear, ratio: tot_mass / tot_bear, mass_per_bear});
|
||||
|
||||
let first;
|
||||
ops.slice().map((s,i) => {
|
||||
if (!first && s.bear) {
|
||||
s.ord_first = first = true;
|
||||
}
|
||||
// 30x lowest to 1x highest
|
||||
s.ord_weight = Math.pow(30,((ops.length - i) / ops.length));
|
||||
return s;
|
||||
}).sort((a,b) => {
|
||||
if (a.ord_first) return -1;
|
||||
if (b.ord_first) return 1;
|
||||
return (b.bear * b.ord_weight) - (a.bear * a.ord_weight);
|
||||
}).forEach((slice, index) => {
|
||||
slice.ord_bear = index;
|
||||
})
|
||||
|
||||
let rem_mass = tot_mass,
|
||||
rem_bear = tot_bear;
|
||||
|
||||
// compute remaining mass, bearing surface, for each slice
|
||||
let run, runLast, runCount = 0, runList = [];
|
||||
ops.forEach(slice => {
|
||||
slice.rem_mass = rem_mass;
|
||||
slice.rem_bear = rem_bear;
|
||||
// slice.can_bear = slice.mass * mass_per_bear;
|
||||
slice.can_bear = slice.bear * mass_per_bear;
|
||||
rem_mass -= slice.mass;
|
||||
rem_bear -= slice.bear;
|
||||
});
|
||||
|
||||
let ord = ops.sort((a,b) => {
|
||||
return a.ord_bear - b.ord_bear;
|
||||
});
|
||||
|
||||
rem_mass = tot_mass;
|
||||
rem_bear = tot_bear;
|
||||
|
||||
// in order of load bearing capability, select layer
|
||||
// and recompute the requirements on the slices below
|
||||
for (let i=0; i<ord.length; i++) {
|
||||
let slice = ord[i],
|
||||
bearing = Math.min(slice.can_bear, slice.rem_mass);
|
||||
|
||||
// remove from slices below the amount of mass they have to bear
|
||||
for (let j=slice.index - 1; j>ops[0].index; j--) {
|
||||
slices[j].rem_mass -= bearing;
|
||||
}
|
||||
|
||||
// remove total mass left to bear
|
||||
rem_mass -= bearing;
|
||||
slice.can_emit = true;
|
||||
if (rem_mass <= 0) {
|
||||
// console.log({break: i, of: ord.length});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let seq = 0,
|
||||
seqLast = 0,
|
||||
spacing = (1 - process.slaSupportDensity) * 10,
|
||||
size = Math.bound(process.slaSupportSize / 2, 0.25, 1);
|
||||
|
||||
// compute and project support pillars
|
||||
slices.forEach((slice,index) => {
|
||||
if (slice.can_emit) {
|
||||
if (seq === 0 || slice.index - seqLast > 5) {
|
||||
slice.bear_up.map(p => {
|
||||
return p.clone(true).setZ(slice.z);
|
||||
}).forEach(p => {
|
||||
projectSupport(process, slice, p, size, spacing);
|
||||
});
|
||||
seq++;
|
||||
} else if (seq > 5) {
|
||||
seq = 0;
|
||||
} else {
|
||||
seq++;
|
||||
}
|
||||
seqLast = slice.index;
|
||||
}
|
||||
progress(1 / slices.length);
|
||||
});
|
||||
|
||||
// union support pillars
|
||||
slices.forEach(slice => {
|
||||
if (slice.supports) {
|
||||
slice.supports = POLY.union(slice.supports,0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function projectSupport(process, slice, poly, size, spacing) {
|
||||
let flat = poly.circularityDeep() > 0.1,
|
||||
arr = [ poly ];
|
||||
|
||||
// insetting polys produces arrays. consume til gone
|
||||
while (arr.length) {
|
||||
poly = arr.shift();
|
||||
let out = [],
|
||||
seg = [],
|
||||
per = poly.perimeter(),
|
||||
crit = 0,
|
||||
polys = poly.clone(true).flattenTo([]);
|
||||
|
||||
polys.forEach(p => p.forEachSegment((p1, p2) => {
|
||||
let rec = {dist: p1.distTo2D(p2), p1, p2};
|
||||
if (rec.dist >= spacing || slice.ord_first) crit++;
|
||||
seg.push(rec);
|
||||
}));
|
||||
|
||||
seg.sort((a,b) => {
|
||||
return b.dist - a.dist;
|
||||
});
|
||||
|
||||
// emit all critical (long) segments. min 3
|
||||
while (seg.length && (crit > 0 || out.length < 3)) {
|
||||
let {dist, p1, p2} = seg.shift();
|
||||
if (dist >= spacing) {
|
||||
// spaced along line
|
||||
let num = Math.ceil(dist / spacing) + 1,
|
||||
step = dist / num,
|
||||
pt = p1;
|
||||
while (num-- >= 0) {
|
||||
out.push(pt);
|
||||
pt = pt.offsetPointTo(p2, step);
|
||||
}
|
||||
crit--;
|
||||
} else {
|
||||
// line midpoint
|
||||
out.push(p1.offsetPointTo(p2, dist / 2));
|
||||
}
|
||||
}
|
||||
|
||||
// drop points too close to other pillars
|
||||
if (!slice.ord_first)
|
||||
drop: for (let i=0; i<out.length; i++) {
|
||||
for (let j=i+1; j<out.length; j++) {
|
||||
if (out[i].distTo2D(out[j]) <= size) {
|
||||
out[i] = null;
|
||||
continue drop;
|
||||
}
|
||||
}
|
||||
if (slice.pillars) slice.pillars.forEach(rec => {
|
||||
if (out[i] && out[i].distTo2D(rec.point) <= size) {
|
||||
out[i] = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// mark support pillar for each point
|
||||
out.filter(p => p !== null)
|
||||
.forEach(p => {
|
||||
let track = projectPillar(process, slice, p, size, []);
|
||||
// find stunted pillars terminating on a face (not raft or merged)
|
||||
if (track.length && !(track.max || track.synth || track.merged)) {
|
||||
// remove stunted pillar
|
||||
track.forEach(rec => {
|
||||
let sp = rec.slice.supports;
|
||||
let rp = sp.indexOf(rec.pillar);
|
||||
if (rp >= 0) sp.splice(rp,1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// inset polygon for flat area support
|
||||
if (flat) poly.offset(Math.max(0.2, 1 - process.slaSupportDensity), arr);
|
||||
}
|
||||
}
|
||||
|
||||
function projectPillar(process, slice, point, size, track) {
|
||||
if (!slice.supports) slice.supports = [];
|
||||
if (!slice.pillars) slice.pillars = [];
|
||||
|
||||
let points = process.slaSupportPoints,
|
||||
pillar = newPolygon()
|
||||
.centerCircle(point, size/2, points, true)
|
||||
.setZ(slice.z),
|
||||
max = process.slaSupportSize,
|
||||
inc = process.slaSlice/2,
|
||||
end = false, // center intersects
|
||||
over = [], // overlapping points
|
||||
safe = [], // non-overlapping points
|
||||
low = slice.index < process.slaSupportGap + process.slaSupportLayers;
|
||||
|
||||
track.min = track.min ? Math.min(track.min, size) : size;
|
||||
track.max = (track.max ? true : false) || size >= max;
|
||||
track.synth = track.synth || slice.synth;
|
||||
|
||||
slice.tops.forEach(t => {
|
||||
if (track.length > 3 && point.isInPolygon(t.poly)) {
|
||||
end = true;
|
||||
}
|
||||
pillar.points.forEach(p => {
|
||||
let isin = p.isInPolygon(t.poly);// || p.nearPolygon(t.poly, 0.001);
|
||||
if (isin) {
|
||||
over.push(p);
|
||||
} else {
|
||||
safe.push(p);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (end) {
|
||||
// backtrack shrinking pillars to point if
|
||||
// not landing on the base and size is max'd
|
||||
if (!slice.synth && track.max) {
|
||||
let nusize = track.min;
|
||||
while (track.length) {
|
||||
let prec = track.pop(),
|
||||
npil = newPolygon()
|
||||
.centerCircle(prec.point, nusize/2, points, true)
|
||||
.setZ(prec.slice.z),
|
||||
spos = prec.slice.supports.indexOf(prec.pillar);
|
||||
// if we find our old pillar, replace
|
||||
if (spos >= 0) {
|
||||
prec.slice.supports[spos] = npil;
|
||||
nusize += inc;
|
||||
}
|
||||
if (nusize > prec.size) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
let nextpoint = point;
|
||||
|
||||
if (over.length) {
|
||||
// move toward average of safe (non-overlapping) points
|
||||
if (safe.length) {
|
||||
let x = 0, y = 0;
|
||||
safe.forEach(p => { x += p.x; y += p.y });
|
||||
x /= safe.length;
|
||||
y /= safe.length;
|
||||
nextpoint = point.offsetPointTo({x, y, z:slice.z}, inc);
|
||||
}
|
||||
// once max'ed out, can only shrink in size
|
||||
if (track.max) {
|
||||
size -= inc;
|
||||
}
|
||||
} else if (!track.max || low) {
|
||||
size += inc;
|
||||
if (low) max += (process.slaSupportGap * process.slaSlice * 2);
|
||||
}
|
||||
|
||||
if (size < track.min) {
|
||||
return track;
|
||||
}
|
||||
|
||||
// let close = [];
|
||||
// for (let i=0; i<slice.pillars.length; i++) {
|
||||
// let p = slice.pillars[i].point;
|
||||
// let d = point.distTo2D(p);
|
||||
// // terminate if we're inside another pillar
|
||||
// if (d < inc) {
|
||||
// track.merged = true;
|
||||
// return track;
|
||||
// }
|
||||
// if (d <= process.slaSupportSize * 1.5) {
|
||||
// close.push(p);
|
||||
// }
|
||||
// }
|
||||
// if (close.length) {
|
||||
// let newp = point.clone();
|
||||
// close.forEach(p => {
|
||||
// newp.x += p.x;
|
||||
// newp.y += p.y;
|
||||
// });
|
||||
// newp.x /= (close.length + 1);
|
||||
// newp.y /= (close.length + 1);
|
||||
// nextpoint = point.offsetPointTo(newp, inc);
|
||||
// }
|
||||
|
||||
slice.supports.push(pillar);
|
||||
slice.pillars.push({point, pillar, size});
|
||||
track.push({slice, point, pillar, size});
|
||||
if (slice.down) {
|
||||
projectPillar(process, slice.down, nextpoint, Math.min(size, max), track);
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
function fillPolys(slice, settings) {
|
||||
let process = settings.process,
|
||||
device = settings.device,
|
||||
polys = slice.solids.unioned,
|
||||
bounds = settings.bounds,
|
||||
width = bounds.max.x - bounds.min.x,
|
||||
depth = bounds.max.y - bounds.min.y,
|
||||
max = Math.max(width,depth),
|
||||
seq = Math.round(process.slaFillLine / process.slaSlice),
|
||||
linew = process.slaFillLine,
|
||||
units_w = (width / linew) * process.slaFillDensity,
|
||||
units_d = (depth / linew) * process.slaFillDensity,
|
||||
step_x = width / units_w,
|
||||
step_y = depth / units_d,
|
||||
start_x = -(width / 2),
|
||||
start_y = -(depth / 2),
|
||||
end_x = width / 2,
|
||||
end_y = depth / 2,
|
||||
fill = [];
|
||||
|
||||
let seq_i = Math.floor(slice.index / seq),
|
||||
seq_c = seq_i % 4,
|
||||
cached = fill_cache[seq_c];
|
||||
|
||||
if (!cached && seq_c !== 1)
|
||||
for (let x=start_x; x<end_x; x += step_x) {
|
||||
fill.push(
|
||||
BASE.newPolygon().centerRectangle({
|
||||
x: x + step_x/2,
|
||||
y: 0,
|
||||
z: slice.z
|
||||
}, linew, depth)
|
||||
);
|
||||
}
|
||||
|
||||
if (!cached && seq_c !== 3)
|
||||
for (let y=start_y; y<end_y; y += step_y) {
|
||||
fill.push(
|
||||
BASE.newPolygon().centerRectangle({
|
||||
x: 0,
|
||||
y: y + step_y/2,
|
||||
z: slice.z
|
||||
}, width, linew)
|
||||
);
|
||||
}
|
||||
|
||||
if (!cached) {
|
||||
fill = POLY.union(fill);
|
||||
fill_cache[seq_c] = fill;
|
||||
} else {
|
||||
fill = cached.slice().map(p => p.clone(true).setZ(slice.z));
|
||||
}
|
||||
|
||||
fill = POLY.trimTo(fill, slice.tops.map(t => t.poly));
|
||||
fill = POLY.union(slice.solids.unioned.appendAll(fill));
|
||||
|
||||
slice.solids.unioned = fill;
|
||||
}
|
||||
|
||||
function pixAt(png,x,y) {
|
||||
let idx = (x + png.width * y) * 4;
|
||||
let dat = png.data;
|
||||
return [
|
||||
dat[idx++],
|
||||
dat[idx++],
|
||||
dat[idx++],
|
||||
dat[idx++]
|
||||
];
|
||||
}
|
||||
|
||||
function averageBlock(png,x1,y1,x2,y2) {
|
||||
let val = [0, 0, 0, 0], count = 0, x, y, z, v2;
|
||||
for (x=x1; x<x2; x++) {
|
||||
for (y=y1; y<y2; y++) {
|
||||
v2 = pixAt(png,x,y);
|
||||
for (z=0; z<4; z++) {
|
||||
val[z] += v2[z];
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
for (z=0; z<4; z++) {
|
||||
val[z] = Math.abs(val[z] / count);
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
function samplePNG(png, width, height) {
|
||||
let th = width, tw = height,
|
||||
ratio = png.width / png.height,
|
||||
buf = new Uint8Array(th * tw * 4),
|
||||
div, xoff, yoff, dx, ex, dy, ey, bidx, pixval;
|
||||
|
||||
if (ratio > 4/3) {
|
||||
div = png.height / tw;
|
||||
xoff = Math.round((png.width - (th * div)) / 2);
|
||||
yoff = 0;
|
||||
} else {
|
||||
div = png.width / th;
|
||||
xoff = 0;
|
||||
yoff = Math.round((png.height - (tw * div)) / 2);
|
||||
}
|
||||
|
||||
for (let y=0; y<tw; y++) {
|
||||
dy = Math.round(y * div + yoff);
|
||||
if (dy < 0 || dy > png.height) continue;
|
||||
ey = Math.round((y+1) * div + yoff);
|
||||
for (let x=0; x<th; x++) {
|
||||
dx = Math.round(x * div + xoff);
|
||||
if (dx < 0 || dx > png.width) continue;
|
||||
ex = Math.round((x+1) * div + xoff);
|
||||
bidx = (y * th + x) * 4;
|
||||
pixval = averageBlock(png,dx,dy,ex,ey);
|
||||
buf[bidx+0] = pixval[0];
|
||||
buf[bidx+1] = pixval[1];
|
||||
buf[bidx+2] = pixval[2];
|
||||
buf[bidx+3] = pixval[3];
|
||||
}
|
||||
}
|
||||
|
||||
return {width, height, data:buf, png};
|
||||
}
|
||||
|
||||
})();
|
||||
Loading…
Reference in a new issue