package bs.bs2d.gui.views;
import bs.bs2d.geom.AreaSet;
import bs.bs2d.gui.BSConstants;
import bs.bs2d.gui.BusyStateListener;
import bs.bs2d.gui.UserMessage;
import bs.bs2d.slicer.PrintObject;
import bs.bs2d.slicer.Slicer;
import bs.properties.BSProperties;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.TopologyException;
import com.vividsolutions.jts.geom.util.AffineTransformation;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.vecmath.Point2d;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
/**
*
* @author Djen
*/
public class GCodeView implements BSGUIView{
private static final String NAME = "G-Code";
private static final boolean OPTIMIZE_PACKING = true;
// gui
private final JComponent content;
private final JTextArea gCodeArea;
private final GCodeViewControls controls;
private final JMenu[] menus;
// data
private AreaSet areaSet;
private PrintObject optimized, uniform;
private float[] densities;
private Slicer slicer;
private BSProperties bsp;
private Point2d[] positions;
private File gcodeFile;
BusyStateListener bsl;
public GCodeView(){
gCodeArea = new JTextArea();
content = createContent();
ActionListener alUpdate = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
bsl.setBusy(true);
new Thread(new Runnable() {
@Override
public void run() {
try{
updatePrintSettings();
} catch (TopologyException ex){
UserMessage.showError("Geometry Error", "A geometry error occurred during slicing. Try smoothing infill areas!");
ex.printStackTrace();
}
bsl.setBusy(false);
}
}).start();
}
};
ActionListener alGCode = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
bsl.setBusy(true);
new Thread(new Runnable() {
@Override
public void run() {
try{
generateGcode();
} catch (TopologyException ex){
UserMessage.showError("Geometry Error", "A geometry error occurred during slicing. Try smoothing infill areas!");
ex.printStackTrace();
}
bsl.setBusy(false);
}
}).start();
}
};
ActionListener alSave = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
saveGCode();
}
};
controls = new GCodeViewControls(alUpdate, alGCode, alSave,
getSettings(BSConstants.PRINT_DIR_NAME),
getSettings(BSConstants.PRINTER_DIR_NAME),
getSettings(BSConstants.FILAMENT_DIR_NAME));
menus = buildMenus();
}
private void saveGCode() {
File f;
if(gcodeFile != null)
f = gcodeFile;
else
f = BSConstants.workFile;
String path = f.getPath();
String base = FilenameUtils.getFullPath(path);
String name = FilenameUtils.getBaseName(path);
if(controls.getPrintPairs())
name += "_pairs";
else if(!controls.getPrintOptimized())
name += "_uniform_equivalent";
int count = controls.getPartCount();
name += "x" + count;
name += ".gcode";
f = FileUtils.getFile(base, name);
JFileChooser fc = new JFileChooser();
fc.setSelectedFile(f);
fc.setFileFilter(new FileNameExtensionFilter("*.gcode", "gcode"));
int result = fc.showSaveDialog(null);
if(result == JFileChooser.APPROVE_OPTION){
try {
slicer.writeToFile(fc.getSelectedFile());
} catch (IOException ex) {
UserMessage.showError("Error writing file!", "The gcode file could not be written!");
}
}
}
private void updatePrintSettings() throws TopologyException{
bsp = new BSProperties();
try {
File fPrint = controls.getPrint();
File fPrinter = controls.getPrinter();
File fFilament = controls.getFilament();
bsp.loadProperties(fPrint, fFilament, fPrinter);
} catch (IOException ex) {
UserMessage.showError("Error Loading Settings",
"An error occurred while loading the selected settings "
+ "files. Default settings will be used.");
}
Envelope bb = areaSet.getEnvelope();// bounding box
final double margin = bsp.skirt_distance + 1; // min 1 mm distance to print bed boundaries
int x = (int)((bsp.bed_size[0] - 2*margin + bsp.gap) / (bb.getWidth() + bsp.gap)); // x num of tiles
int y = (int)((bsp.bed_size[1] - 2*margin + bsp.gap) / (bb.getHeight() + bsp.gap)); // y num of tiles
positions = getTilePositions(areaSet, x, y, bsp);
if(OPTIMIZE_PACKING){
for (int i = y; i > y/2; i--) {
Point2d[] newPos = optimizePacking(areaSet, i, bsp);
if(newPos.length > positions.length){
positions = newPos;
System.out.println("successful packing!");
}
}
}
controls.setMaxTiles(positions.length/y, y);
if(x == 0 || y == 0){
UserMessage.showError("Error!", "Object does not fit on print bed!");
}
optimized = new PrintObject(areaSet, toDouble(densities), controls.getObjectHeight());
uniform = optimized.getUniform();
double filament = slice(optimized);
double eqDensity = getEquivalentDensity(uniform, filament, 0.0005);
uniform.setDensity(eqDensity);
controls.setResultingDensity(eqDensity);
}
private Point2d[] optimizePacking(AreaSet as, int lines, BSProperties bsp){
double lineHeight = (bsp.bed_size[1] - 2*(bsp.extrusion_width + bsp.skirt_distance) - (lines-1)*bsp.gap)/lines;
Geometry shell0 = (Geometry)as.getShell().clone();
Envelope env0 = shell0.getEnvelopeInternal();
AffineTransformation t;
t = AffineTransformation.translationInstance(-env0.getMinX(), -env0.getMinY());
shell0 = t.transform(shell0);
env0 = shell0.getEnvelopeInternal();
Geometry shell1 = (Geometry)shell0.clone();
t = AffineTransformation.translationInstance(env0.getWidth(), lineHeight - env0.getHeight());
shell1 = t.transform(shell1);
double dist = shell0.distance(shell1);
double error = dist-2.5;
while(Math.abs(error) > 0.5){
t = AffineTransformation.translationInstance(-error/2, 0);
shell1 = t.transform(shell1);
dist = shell0.distance(shell1);
System.out.println("shell 1 distance: " + dist);
error = dist-2.5;
}
Envelope env1 = shell1.getEnvelopeInternal();
Geometry shell2 = (Geometry)shell1.clone();
t = AffineTransformation.translationInstance(env1.getWidth(), -env1.getMinY());
shell2 = t.transform(shell2);
dist = shell1.distance(shell2);
error = dist-2.5;
while(Math.abs(error) > 0.5){
t = AffineTransformation.translationInstance(-error/3, 0);
shell2 = t.transform(shell2);
dist = shell1.distance(shell2);
System.out.println("shell 2 distance: " + dist);
error = dist-2.5;
}
Envelope env2 = shell2.getEnvelopeInternal();
Coordinate c0 = env0.centre();
Coordinate c1 = env1.centre();
Coordinate c2 = env2.centre();
double[] dx = new double[]{c2.x - c1.x, c1.x - c0.x};
double gap = bsp.extrusion_width + bsp.skirt_distance;
double[] py = new double[]{c0.y + gap, c1.y + gap};
double minX = gap + env0.getWidth()/2;
double maxX = bsp.bed_size[0] - minX;
ArrayList<Point2d> centers = new ArrayList<>();
double curX = minX + env0.getWidth()/2;
centers.add(new Point2d(curX, py[0]));
for (int i = 1; true; i++) {
curX += dx[i%2];
if(curX > maxX)
break;
centers.add(new Point2d(curX, py[i%2]));
}
int xCount = centers.size();
double dy = lineHeight + bsp.gap;
for (int i = 1; i < lines; i++) {
for (int j = 0; j < xCount; j++) {
Point2d pj = centers.get(j);
centers.add(new Point2d(pj.x, pj.y + i*dy));
}
}
return centers.toArray(new Point2d[centers.size()]);
}
private double slice(List<PrintObject> objects) throws TopologyException{
slicer = new Slicer(bsp);
slicer.slice(objects);
return slicer.getFilamentUsed();
}
private double slice(PrintObject obj) throws TopologyException{
List<PrintObject> o = new ArrayList<>(1);
o.add(obj);
return slice(o);
}
/**
* Calculates the density for a uniform infill print to use exactly the
* specified amount of filament.
* @param obj the Object to print
* @param filament the amount (length) of filament to be used
* @param maxError maximum relative error (0.01 = 1% deviation)
* @return the density for the uniform infill part
*/
private double getEquivalentDensity(PrintObject obj, double filament, double maxError) throws TopologyException{
double filamentUsed, error = Double.MAX_VALUE;
double minD = 0, maxD = 1; //min and max density
double eqD = 0.5; // equivalent density (to be determined)
while(Math.abs(error) > (filament * maxError)) {
eqD = (minD + maxD) / 2;
obj.setDensity(eqD);
filamentUsed = slice(obj);
error = filament - filamentUsed;
System.out.println("error: " + error + " at density: " + eqD);
if(error > 0) // using too little filament
minD = eqD;
else // using too much filament
maxD = eqD;
if(maxD-minD < 0.0000000001){
System.out.println("best possible approximation reached!");
break;
}
}
return eqD;
}
private void generateGcode() throws TopologyException{
int count = controls.getPartCount();
boolean pairs = controls.getPrintPairs();
boolean printOptimized = controls.getPrintOptimized();
// List<PrintObject> objects = getTiles(x, y, pairs, printOptimized);
List<PrintObject> objects = getPrintObjects(count, pairs, printOptimized);
slice(objects);
gCodeArea.setText(slicer.getGCode());
}
private Point2d[] getTilePositions(AreaSet as, int x, int y, BSProperties bsp){
Envelope bb = as.getEnvelope();// bounding box
// determine center for first tile and distance to next tile
double widthX = x * (bb.getWidth() + bsp.gap) - bsp.gap;
double t1x = bsp.print_center[0] - (widthX - bb.getWidth())/2;
double distx = bb.getWidth() + bsp.gap;
double widthY = y * (bb.getHeight() + bsp.gap) - bsp.gap;
double t1y = bsp.print_center[1] - (widthY - bb.getHeight())/2;
double disty = bb.getHeight() + bsp.gap;
Point2d[] positions = new Point2d[x*y];
for (int ix = 0; ix < x; ix++) {
for (int iy = 0; iy < y; iy++) {
positions[ix*y + iy] = new Point2d(t1x + ix*distx, t1y + iy*disty);
}
}
return positions;
}
private List<PrintObject> getPrintObjects(int count, boolean pairs, boolean printOptimized){
ArrayList<PrintObject> tiles = new ArrayList<>(count);
PrintObject obj;
for (int i = 0; i < count; i++) {
if(pairs){
if(i % 2 == 0)
obj = optimized;
else
obj = uniform;
} else {
if(printOptimized)
obj = optimized;
else
obj = uniform;
}
tiles.add(new PrintObject(obj, positions[i]));
}
return tiles;
}
private List<PrintObject> getTiles(int x, int y, boolean pairs, boolean printOptimized){
Envelope bb = optimized.getAreaSet().getEnvelope();// bounding box
// determine center for first tile and distance to next tile
double widthX = x * (bb.getWidth() + bsp.gap) - bsp.gap;
double t1x = bsp.print_center[0] - (widthX - bb.getWidth())/2;
double distx = bb.getWidth() + bsp.gap;
double widthY = y * (bb.getHeight() + bsp.gap) - bsp.gap;
double t1y = bsp.print_center[1] - (widthY - bb.getHeight())/2;
double disty = bb.getHeight() + bsp.gap;
ArrayList<PrintObject> tiles = new ArrayList<>(x * y);
int counter = 0;
PrintObject obj;
for (int ix = 0; ix < x; ix++) {
for (int iy = 0; iy < y; iy++) {
if(pairs){
if(counter % 2 == 0)
obj = optimized;
else
obj = uniform;
} else {
if(printOptimized)
obj = optimized;
else
obj = uniform;
}
Point2d pos = new Point2d(t1x + ix*distx, t1y + iy*disty);
tiles.add(new PrintObject(obj, pos));
counter++;
}
}
if(pairs && tiles.size() % 2 == 1){
tiles.remove(tiles.size()-1);
tiles.trimToSize();
}
return tiles;
}
private static double getAverage(double[] f){
double a = 0;
for (double g : f)
a += g;
return a / f.length;
}
private static double[] toDouble(float[] f){
double[] d = new double[f.length];
for (int i = 0; i < f.length; i++)
d[i] = f[i];
return d;
}
private JComponent createContent(){
gCodeArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
gCodeArea.setEditable(false);
return new JScrollPane(gCodeArea);
}
private JMenu[] buildMenus(){
JMenu[] ms = new JMenu[0];
return ms;
}
private File[] getSettings(String settings){
ArrayList<File> files = new ArrayList<>();
File f = FileUtils.getFile(BSConstants.SETTINGS_DIR, settings);
File[] list = f.listFiles(BSConstants.INI_EXTENSION_FILTER);
if(list != null)
files.addAll(Arrays.asList(list));
f = FileUtils.getFile(BSConstants.SLIC3R_DIR, settings);
list = f.listFiles(BSConstants.INI_EXTENSION_FILTER);
if(list != null)
files.addAll(Arrays.asList(list));
return files.toArray(new File[files.size()]);
}
@Override
public JComponent getContent() {
return content;
}
@Override
public JComponent getControls() {
return controls;
}
@Override
public JMenu[] getMenus() {
return menus;
}
@Override
public void init(Object data) {
if(data instanceof Object[]){
Object[] odata = (Object[]) data;
if(odata[0] instanceof AreaSet &&
odata[1] instanceof float[]){
areaSet = (AreaSet) odata[0];
densities = (float[]) odata[1];
return;
}
}
// invalid data
areaSet = null;
gCodeArea.setText("");
}
@Override
public Object getData() {
return null;
}
@Override
public String getName() {
return NAME;
}
@Override
public void setBusyStateListener(BusyStateListener bsl) {
this.bsl = bsl;
}
}