package bs.bs2d.slicer;
import bs.bs2d.geom.JTSUtils;
import bs.properties.BSProperties;
import bs.properties.LayerProperties;
import bs.testing.TestFrame;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.TopologyException;
import com.vividsolutions.jts.geom.util.AffineTransformation;
import com.vividsolutions.jts.operation.linemerge.LineMerger;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.swing.JPanel;
import javax.vecmath.Vector2d;
/**
* Can slice geometry into layers and generate gcode using a GCodeGenerator. The
* gcode is then stored in an internal GCodeAssembler and can either be
* retrieved as a string or written to a gcode file.
* Any geometry submitted for slicing is appended to the internally stored
* gcode. Use the reset method to clear clear the gcode and
* @author Djen
*/
public class Slicer {
private BSProperties properties;
private final GCodeAssembler gcode;
private double filamentUsed = 0;
public Slicer(BSProperties properties) {
this.properties = properties;
gcode = new GCodeAssembler();
}
/**
* Slices the given geometry and keeps the generated gcode in this sclicer's
* internal GCodeAssembler.
* @param objects A list of objects to slice (it is assumed all objects are
* adequately placed on the print bed)
*/
public void slice(List<PrintObject> objects) throws TopologyException{
System.out.println("slicing...");
// calculate number of layers for each object
int total = 0; //total number of layers
for (int i = 0; i < objects.size(); i++) {
PrintObject obj = objects.get(i);
double height = obj.getHeight() - properties.first_layer_height;
obj.layers = (int)(height / properties.layer_height) + 1;
if(obj.layers > total)
total = obj.layers;
}
gcode.setRetraction(properties.retract_speed, properties.retract_length, properties.retract_before_travel);
initPrint();
for (int i = 0; i < total; i++) {
LayerProperties lp = new LayerProperties(i, properties);
generateLayer(objects, lp);
}
finalizePrint();
filamentUsed = gcode.getTotalExtrusionLength();
}
private void initPrint(){
gcode.appendLine("; extrusion width = " + properties.extrusion_width);
gcode.appendLine("; first layer extrusion width = " + properties.first_layer_extrusion_width);
gcode.appendLine("G21 ; set units to millimeters");
gcode.appendLine("M107 ; turn off fan");
gcode.appendLine("M190 S" + properties.first_layer_bed_temperature + " ; wait for bed temperature to be reached");
gcode.appendLine("M104 S" + properties.first_layer_temperature + " ; set temperature");
gcode.appendLines(properties.start_gcode);
gcode.appendLine("M109 S" + properties.first_layer_temperature + " ; wait for temperature to be reached");
gcode.appendLine("G90 ; use absolute coordinates");
gcode.appendLine("M82 ; use absolute distances for extrusion");
}
private void finalizePrint(){
gcode.appendLines(properties.end_gcode);
double fil = (int)(gcode.getTotalExtrusionLength() * 10) / 10.0;
gcode.appendLine("; filament used = " + fil + "mm");
}
private void generateLayer(List<PrintObject> objects, LayerProperties lp) throws TopologyException{
GCodeGenerator ggen = new GCodeGenerator(gcode, properties);
ggen.initLayer(lp);
// skirt
if(lp.skirt){
generateSkirt(ggen, objects, lp);
}
for (PrintObject obj : objects) {
generateObjectLayer(ggen, obj, lp);
}
ggen.finalizeLayer();
}
private void generateSkirt(GCodeGenerator ggen, List<PrintObject> objects, LayerProperties lp) throws TopologyException{
// assemble all shells in one geometry collection
Geometry[] shells = new Geometry[objects.size()];
for (int i = 0; i < objects.size(); i++){
PrintObject o = objects.get(i);
shells[i] = o.getTranslate().transform(o.getAreaSet().getShell());
}
Geometry totalShell;
totalShell = JTSUtils.getFactory().createGeometryCollection(shells);
LineString[] skirt;
skirt = JTSUtils.getLineStrings(totalShell.convexHull().buffer(lp.skirt_distance));
ggen.parse(skirt[0], lp.height, lp.extrusion_width, lp.perimeter_speed);
}
private void generateObjectLayer(GCodeGenerator ggen, PrintObject obj, LayerProperties lp) throws TopologyException{
LineString[] lines;
double lh = lp.height;
double ew = lp.extrusion_width;
Geometry shell = obj.getAreaSet().getShell();
Envelope bounds = shell.getEnvelopeInternal(); // bounds for hatch lines
// using bounds of outer shell to ascertain that hatch lines on
// different layers align
AffineTransformation t = obj.getTranslate();
// currently not used --> always 0
if(lp.outline_offset != 0)
shell = shell.buffer(-lp.outline_offset);
Geometry infillArea = shell.buffer(-lp.infill_offset);
// outlines
lines = getOutlines(shell, lp);
ggen.parseAll(lines, lh, ew, lp.perimeter_speed, t);
// infill
if(isSolid(obj, lp.layer)){ // make solid infill
lines = getInfill(infillArea, bounds, 1, lp.fill_angle, ew);
ggen.parseAll(lines, lh, ew, lp.infill_speed, t);
} else {
MultiPolygon[] polys = obj.getAreaSet().getPolygons();
// separators
lines = getSeparators(polys, infillArea);
ggen.parseAll(lines, lh, ew, lp.infill_speed, t);
// infill
for (int i = 0; i < polys.length; i++) {
double offset = 0.5 * ew + lp.cap_length;
Geometry g = polys[i].buffer(-offset).intersection(infillArea);
MultiPolygon p = JTSUtils.toMultiPolygon(g);
lines = getInfill(p, bounds, obj.getDensity(i), lp.fill_angle, ew);
ggen.parseAll(lines, lh, ew, lp.infill_speed, t);
}
}
}
private boolean isSolid(PrintObject obj, int layer){
return layer < properties.bottom_solid_layers ||
layer >= obj.layers - properties.top_solid_layers;
}
/**
* Generates the outline path for the print, that is the inward parallel
* offset of the polygon outlines by half the extrusion width.
* @param poly the shapes for wich to generate the outline path
* @param ew the extrusion width for the outlines
* @param perimtr the number of perimeters to generate
* @return the outline path
*/
private LineString[] getOutlines(Geometry poly, LayerProperties lp) throws TopologyException{
ArrayList<LineString> lines = new ArrayList<>();
for (int i = lp.perimeters-1; i >= 0; i--) {
double offset = -lp.extrusion_width * (i + 0.5);
MultiPolygon p = JTSUtils.toMultiPolygon(poly.buffer(offset));
lines.addAll(Arrays.asList(JTSUtils.getLineStrings(p)));
}
return lines.toArray(new LineString[lines.size()]);
}
private LineString[] getSeparators(Geometry[] polys, Geometry infillArea) throws TopologyException{
GeometryFactory gf = JTSUtils.getFactory();
// create empty geometry
Geometry union = gf.buildGeometry(new ArrayList(0));
// get separators for each polygon
for (Geometry poly : polys) {
LineString[] lines = JTSUtils.getLineStrings(poly);
MultiLineString mls;
mls = gf.createMultiLineString(lines);
Geometry g = infillArea.intersection(mls);
lines = JTSUtils.getLineStrings(g);
//unify line segments to delete duplicates
for (LineString l : lines) {
if(l.isEmpty())
continue;
union = union.union(l);
}
}
// merge segments
LineMerger merger = new LineMerger();
merger.add(union);
union = gf.buildGeometry(merger.getMergedLineStrings());
return JTSUtils.getLineStrings(union);
}
/**
* Generates a hatch to infill the given areas.
* @param infillArea the area to infill
* @param bounds the bounding box for the hatch
* @param density the fill density
* @param angle the hatch angle
* @param eWidth the extrusion width
* @return a set of hatch lines
*/
private LineString[] getInfill(Geometry infillArea, Envelope bounds,
double density, double angle, double eWidth) throws TopologyException{
// get hatch lines and clip to area
LineString[] lines = getHatchLines(bounds, density, angle, eWidth);
for (int i = 1; i < lines.length; i += 2) {
lines[i] = (LineString)lines[i].reverse();
}
MultiLineString hatchLines;
hatchLines = JTSUtils.getFactory().createMultiLineString(lines);
Geometry intersection = hatchLines.intersection(infillArea);
lines = JTSUtils.getLineStrings(intersection);
// // split area outlines along hatch lines
// MultiLineString outline;
// outline = gf.createMultiLineString(JTSUtils.getLineStrings(hatchArea));
// outline = (MultiLineString) outline.difference(hatchLines);
//
// // stitch hatch lines and outline segments together
// LinkedList<LineString> hlines = new LinkedList<>();
// hlines.addAll(Arrays.asList(JTSUtils.getLineStrings(hatchLines)));
// LinkedList<LineString> olines = new LinkedList<>();
// olines.addAll(Arrays.asList(JTSUtils.getLineStrings(outline)));
//
// ArrayList<LineString> stitch = new ArrayList<>();
// LineString ls = hlines.removeFirst();
// Point point;
// while(true){
// point = ls.getStartPoint();
//
// if(hlines.isEmpty())
// break;
// }
return lines;
}
/**
* Generates parallel hatch lines for the box defined by 'bounds'.
* @param bounds the box to hatch
* @param density the hatch density [0 1]
* @param angle the hatch angle in °
* @param eWidth the extrusion width
* @return a set of hatch lines
*/
private LineString[] getHatchLines(Envelope bounds, double density, double angle, double eWidth){
GeometryFactory gf = JTSUtils.getFactory();
double gap = eWidth / density; // distance between lines
boolean lowAngles = angle < 45 || angle > 135;
angle = Math.toRadians(angle % 180);
double m, d1, d2, min, max;
if(lowAngles){
//horizontally oriented
m = Math.tan(angle);
d1 = bounds.getWidth();
min = bounds.getMinY();
max = bounds.getMaxY();
} else {
angle -= Math.PI / 2;
m = -Math.tan(angle);
d1 = bounds.getHeight();
min = bounds.getMinX();
max = bounds.getMaxX();
}
d2 = m * d1;
double agap = Math.abs(gap / Math.cos(angle)); // gap along axis
if(d2 >= 0){
min -= d2;
} else {
max -= d2; // dy is negativ
}
int n = (int)((max - min) / agap);
double missing = ((max - min) % agap);
if(density == 1.0){// solid layer
if(missing/agap > 0.3)
n++;
//TODO: put this back in when hatch stitching ist implemented
// min += params.extrusionWidth;
// max -= params.extrusionWidth;
} else { // not solid
missing = (missing + agap - eWidth) / 2;
min += missing;
max -= missing;
}
agap = (max - min) / (n-1);
double xbase, ybase, dx, dy, xInc = 0, yInc = 0;
if(lowAngles){
xbase = bounds.getMinX();
ybase = min;
dx = d1;
dy = d2;
yInc = agap;
} else {
xbase = min;
ybase = bounds.getMinY();
dx = d2;
dy = d1;
xInc = agap;
}
LineString[] lines = new LineString[n];
for (int i = 0; i < n; i++) {
double x = xbase + i * xInc;
double y = ybase + i * yInc;
Coordinate p1 = new Coordinate(x, y);
Coordinate p2 = new Coordinate(x + dx, y + dy);
lines[i] = gf.createLineString(new Coordinate[]{p1, p2});
}
return lines;
}
/**
* @return the properties
*/
protected BSProperties getProperties() {
return properties;
}
/**
* @param properties the properties to set
*/
protected void setProperties(BSProperties properties) {
this.properties = properties;
}
/**
* Returns the amount of filament used in the gcode currently stored in this
* Slicer (in mm). This does not include any filament extruded in the start
* or end code specified in the properties.
* @return the filament used
*/
public double getFilamentUsed() {
return filamentUsed;
}
/**
*
* @return the internally stored gcode as a string
*/
public String getGCode(){
return gcode.getGcode().toString();
}
/**
* Writes the internally stored gcode to the given file. The proper ".gcode"
* file extension will be added if necessary.
* @param outputFile the file to write the gcode to
* @throws IOException if an I/O error occurs
*/
public void writeToFile(File outputFile) throws IOException{
gcode.writeToFile(outputFile);
}
// for testing...
public static void main(String[] args){
JPanel jp = new JPanel(){
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Coordinate[] points = new Coordinate[]{
new Coordinate(0, 0),
new Coordinate(10, 0),
new Coordinate(10, 7),
new Coordinate(0, 7),
new Coordinate(0, 0),
};
MultiPolygon p1 = JTSUtils.toMultiPolygon(JTSUtils.getFactory().createPolygon(points));
BSProperties props = new BSProperties();
Slicer slicer = new Slicer(null);
double ew = 0.37;
double d = 0.5;
double a = 40;
int layers = 2;
LayerProperties lp = new LayerProperties(0, props);
MultiLineString outlines = JTSUtils.getFactory().createMultiLineString(slicer.getOutlines(p1, lp));
System.out.println(outlines.getNumGeometries());
LineString[] hl = slicer.getInfill(p1, p1.getEnvelopeInternal(), 1, lp.fill_angle, lp.extrusion_width);
MultiLineString hatch = JTSUtils.getFactory().createMultiLineString(hl);
AffineTransform t = AffineTransform.getTranslateInstance(50, 500);
t.concatenate(AffineTransform.getScaleInstance(50, -50));
AffineTransform gt = g2.getTransform();
g2.transform(t);
g2.setStroke(new BasicStroke((float)ew, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
// g2.setColor(Color.ORANGE);
// g2.draw(JTSUtils.toShape(buffer));
g2.setColor(Color.RED);
g2.draw(JTSUtils.toShape(hatch));
g2.setColor(Color.BLUE);
g2.draw(JTSUtils.toShape(outlines));
g2.setTransform(gt);
}
};
jp.setBackground(Color.WHITE);
new TestFrame(jp).setVisible(true);
}
}