diff --git a/src/com/t_oster/liblasercut/drivers/Dummy.java b/src/com/t_oster/liblasercut/drivers/Dummy.java index 8c27a0d56a9a3adec462c60f3abadb26ac049107..e0a3c0d986362571562e1882ae619e515f0e5017 100644 --- a/src/com/t_oster/liblasercut/drivers/Dummy.java +++ b/src/com/t_oster/liblasercut/drivers/Dummy.java @@ -21,6 +21,12 @@ package com.t_oster.liblasercut.drivers; import com.t_oster.liblasercut.*; import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.util.*; /** @@ -33,26 +39,204 @@ public class Dummy extends LaserCutter { private static final String SETTING_BEDWIDTH = "Laserbed width"; private static final String SETTING_BEDHEIGHT = "Laserbed height"; private static final String SETTING_RUNTIME = "Fake estimated run-time in seconds (-1 to disable)"; + private static final String SETTING_SVG_OUTDIR = "SVG Debug output directory (set empty to disable)"; + + /** + * SVG output creator, mostly for testing vector-sorting + */ + class SVGWriter { + private double xPrev,xNow,yPrev,yNow; + private StringBuilder svg = new StringBuilder(); + private boolean vectorPathActive=false; + private boolean partActive=false; + private int idCounter=0; + private int partCounter=0; + private LaserCutter cutter; + private double dpi; + + + public SVGWriter(LaserCutter cutter) { + this.cutter = cutter; + } + + /** + * start a new JobPart + * @param title some string that will be included in the group ID + * @param dpi + */ + public void startPart(String title, double dpi) { + endPart(); + partCounter += 1; + this.dpi=dpi; + this.partActive=true; + svg.append("<g style=\"fill:none;stroke:#000000;stroke-width:0.1mm;\" id=\""); + svg.append("visicut-part").append(partCounter).append("-"); + svg.append(title.replaceAll("[^a-zA-Z0-9]","_")); + svg.append("\">\n"); + } + + /** + * end a JobPart + */ + public void endPart() { + moveTo(0,0); // end path + if (partActive) { + partActive=false; + svg.append("</g>\n"); + } + } + + private void setLocation(int x, int y) { + xPrev=xNow; + yPrev=yNow; + double factor = 25.4/dpi; // convert units to millimeters + xNow=x*factor; + yNow=y*factor; + } + + /** + * move to somewhere with laser off + * @param x + * @param y + */ + void moveTo(int x, int y) { + setLocation(x,y); + if (vectorPathActive) { + // end the previous path + svg.append("\"/>\n"); + vectorPathActive=false; + } + } + + /** + * move to somewhere with laser on + * @param x + * @param y + */ + void lineTo(int x, int y) { + setLocation(x,y); + if (!partActive) { + throw new RuntimeException("lineTo called outside of a part!"); + } + if (!vectorPathActive) { + // start a new path + vectorPathActive=true; + svg.append("<path id=\"visicut-").append(idCounter).append("\" d=\"M "); + idCounter += 1; + svg.append(xPrev).append(",").append(yPrev).append(" "); + } + svg.append(xNow).append(",").append(yNow).append(" "); + } + + /** + * generate SVG output string and reset everything (delete all path data) + * @return + */ + private String getSVG() { + endPart(); + svg.insert(0,"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?> \n" + + "<!-- Created by VisiCut Debug output -->\n" + + "<svg xmlns:svg=\"http://www.w3.org/2000/svg\" " + + "xmlns=\"http://www.w3.org/2000/svg\" " + + "width=\"" + cutter.getBedWidth() + "mm\" " + + "height=\"" + cutter.getBedHeight() + "mm\" " + + "viewBox=\"0 0 " + cutter.getBedWidth() + " " + cutter.getBedHeight() + "\" " + + "version=\"1.1\" id=\"svg\"> \n"); + svg.append("</svg>\n"); + String result=svg.toString(); + svg = new StringBuilder(); + idCounter=0; + return result; + } + + /** + * store a String into a file + * @param path the filename + * @param str the content to be stored + */ + private void storeString(String path, String str) { + try { + FileWriter f = new FileWriter(path); + BufferedWriter b = new BufferedWriter(f); + b.write(str); + b.close(); + } catch (Exception e) { + System.out.println("Could not write debug SVG: Exception: " + e); + } + } + + /** + * store XHTML viewer to file + * @param path + * @param svgString + */ + void storeXHTML(String path, String svgString) { + BufferedReader br = null; + StringBuilder xhtml = new StringBuilder(); + try { + InputStream stream = new Dummy().getClass().getResourceAsStream("resources/visicut-svg-output-viewer.xhtml"); + StringBuilder s = new StringBuilder(); + br = new BufferedReader(new InputStreamReader(stream, "UTF-8")); + String line=""; + while ((line=br.readLine()) != null) { + if (line.contains("<!-- REPLACE THIS WITH SVG -->")) { + // insert svg, but skip first line with <?xml... + line=svgString.substring(svgString.indexOf("\n")); + } + xhtml.append(line).append("\n"); + } + storeString(path, xhtml.toString()); + } catch (Exception e) { + System.out.println("could not store debug XHTML: " + e); + } finally { + try { + br.close(); + } catch (IOException ex) { + System.out.println("could not close bufferedWriter when storing debug XHTML"); + } + } + } + + void store(String directory) { + if (directory == null || directory.isEmpty()) { + System.out.println("Not writing debug SVG - no output directory set (edit lasercutter settings to change)"); + } else { + String pathSVG=directory + "/visicut-debug.svg"; + System.out.println("storing SVG debug output to "+pathSVG); + String svgString=getSVG(); + storeString(pathSVG,svgString); + String pathXHTML=directory + "/visicut-svg-output-viewer.xhtml"; + System.out.println("storing SVG debug output (XHTML viewer) to "+pathXHTML); + storeXHTML(pathXHTML, svgString); + } + } + + } + @Override public String getModelName() { return "Dummy"; } + + @Override public void sendJob(LaserJob job, ProgressListener pl) throws IllegalJobException, Exception { pl.progressChanged(this, 0); - + BufferedOutputStream out; pl.taskChanged(this, "checking job"); checkJob(job); job.applyStartPoint(); pl.taskChanged(this, "sending"); pl.taskChanged(this, "sent."); + SVGWriter svg = new SVGWriter(this); // SVG debug output System.out.println("dummy-driver got LaserJob: "); //Â TODOÂ don't just print the parts and settins, but also the commands //Â TODO if you have too much time, also implement some preview output (svg animation???) - would be nice for testing optimisations for (JobPart p : job.getParts()) { + svg.startPart(p.getClass().getSimpleName(), p.getDPI()); if (p instanceof VectorPart) { System.out.println("VectorPart"); @@ -60,16 +244,25 @@ public class Dummy extends LaserCutter { { if (cmd.getType() == VectorCommand.CmdType.SETPROPERTY) { + if (!(cmd.getProperty() instanceof PowerSpeedFocusFrequencyProperty)) { throw new IllegalJobException("This driver expects Power,Speed,Frequency and Focus as settings"); } System.out.println(((PowerSpeedFocusFrequencyProperty) cmd.getProperty()).toString()); + } else if (cmd.getType() == VectorCommand.CmdType.LINETO) { + System.out.println("LINETO \t" + cmd.getX() + ", \t" + cmd.getY()); + svg.lineTo(cmd.getX(),cmd.getY()); + } else if (cmd.getType() == VectorCommand.CmdType.MOVETO) { + System.out.println("MOVETO \t" + cmd.getX() + ", \t" + cmd.getY()); + svg.moveTo(cmd.getX(),cmd.getY()); } } + } if (p instanceof RasterPart) { + // TODO add raster output for SVG debug output RasterPart rp = ((RasterPart) p); if (rp.getLaserProperty() != null && !(rp.getLaserProperty() instanceof PowerSpeedFocusProperty)) { @@ -89,6 +282,7 @@ public class Dummy extends LaserCutter { } } System.out.println("end of job."); + svg.store(svgOutdir); pl.progressChanged(this, 100); } @@ -161,10 +355,14 @@ public class Dummy extends LaserCutter { public void setBedHeight(double bedHeight) { this.bedHeight = bedHeight; } + + public String svgOutdir=""; + private static String[] settingAttributes = new String[]{ SETTING_BEDWIDTH, SETTING_BEDHEIGHT, - SETTING_RUNTIME + SETTING_RUNTIME, + SETTING_SVG_OUTDIR }; @Override @@ -180,6 +378,8 @@ public class Dummy extends LaserCutter { return this.getBedHeight(); } else if (SETTING_RUNTIME.equals(attribute)) { return this.fakeRunTime; + } else if (SETTING_SVG_OUTDIR.equals(attribute)) { + return this.svgOutdir; } return null; } @@ -192,6 +392,8 @@ public class Dummy extends LaserCutter { this.setBedHeight((Double) value); } else if (SETTING_RUNTIME.equals(attribute)) { this.fakeRunTime=Integer.parseInt(value.toString()); + } else if (SETTING_SVG_OUTDIR.equals(attribute)) { + this.svgOutdir=value.toString(); } } diff --git a/src/com/t_oster/liblasercut/drivers/resources/visicut-svg-output-viewer.xhtml b/src/com/t_oster/liblasercut/drivers/resources/visicut-svg-output-viewer.xhtml new file mode 100644 index 0000000000000000000000000000000000000000..938338cecc7a9e9e4cc25ddebd5e4919dc88b952 --- /dev/null +++ b/src/com/t_oster/liblasercut/drivers/resources/visicut-svg-output-viewer.xhtml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html +PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" +"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" +xmlns:svg="http://www.w3.org/2000/svg" +xmlns:xlink="http://www.w3.org/1999/xlink"> +<head> +<title>VisiCut SVG debug view</title> +</head> +<style> +.done{stroke:black;} +.todo{stroke:grey; fill:none;} +</style> +<script> +<![CDATA[ +var maximum=0; +function load(n) { + // get maximum + var i=0; + while (true) { + var obj=document.getElementById('visicut-'+i); + if (obj==null) { + // end of elements reached + break; + } + i++; + maximum=i; + } + document.getElementById('slider').max=maximum; +} +function showUntil(n) { + for (var i=0; i<n; i++) { + document.getElementById('visicut-'+i).setAttribute('class','done'); + } + var i=n; + while (true) { + var obj=document.getElementById('visicut-'+i); + if (obj==null) { + // end of elements reached + break; + } + obj.setAttribute('class','todo'); + i++; + } +} +]]> +</script> +<body onload="load()"> + +<h1>VisiCut debug output</h1> +<div style="display:block; width:100%;"> + Slide to see individual steps. Please use this with Chrome or another browser that supports <input type="range"> (not Firefox).<br/> +Start <input id="slider" type="range" value="0" min="0" max="0" onchange="showUntil(this.value)" style="width:60%"/> End +</div> + + +<!-- REPLACE THIS WITH SVG --> + +</body> +</html> \ No newline at end of file diff --git a/src/com/t_oster/liblasercut/platform/Rectangle.java b/src/com/t_oster/liblasercut/platform/Rectangle.java new file mode 100644 index 0000000000000000000000000000000000000000..0caba687a9febbf00f4ee7817df8316ab1f5c413 --- /dev/null +++ b/src/com/t_oster/liblasercut/platform/Rectangle.java @@ -0,0 +1,152 @@ +/** + * This file is part of VisiCut. + * Copyright (C) 2012 Max Gaukler <development@maxgaukler.de> + * + * VisiCut is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VisiCut is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VisiCut. If not, see <http://www.gnu.org/licenses/>. + **/ +package com.t_oster.liblasercut.platform; + +/** + * (not really compatible) replacement of java.awt.Rectangle, + * This Rectangle cannot be "empty" - at minimum it needs to have one point. + * + * @see Point + */ +public class Rectangle { + private int x1, x2, y1, y2; + + /** + * construct a rectangle with the corners (x1,y1) and (x2,y2) + */ + public Rectangle(int x1, int y1, int x2, int y2) + { + this.x1=Math.min(x1,x2); + this.x2=Math.max(x1,x2); + this.y1=Math.min(y1,y2); + this.y2=Math.max(y1,y2); + } + + /** + * Construct a rectangle with the corners p1 and p2 + */ + public Rectangle(Point p1, Point p2) { + this(p1.x,p1.y,p2.x,p2.y); + } + + /** + * construct a rectangle with only one point p + */ + public Rectangle(Point p) { + this(p,p); + } + + + /** + * add a point to the boundary of this rectangle + * use this iteratively to get the boundingBox + */ + public void add (int x, int y) { + if (x<x1) { + this.x1=x; + } else if (x>x2) { + this.x2=x; + } + + if (y<y1) { + this.y1=y; + } else if (y>y2) { + this.y2=y; + } + } + + public void add(Point p) { + if (p != null) { + add(p.x, p.y); + } + } + + /** + * smallest X coordinate + * @return int + */ + public int getXMin() { + return x1; + } + + /** + * greatest X coordinate + */ + public int getXMax() { + return x2; + } + + /** + * smallest Y coordinate + */ + public int getYMin() { + return y1; + } + + /** + * greatest Y coordinate + */ + public int getYMax() { + return y2; + } + + /** + * X interval from left to right + */ + public Interval getXInterval() { + return new Interval(x1,x2); + } + + /** + * Y interval from top to bottom + */ + public Interval getYInterval() { + return new Interval(y1,y2); + } + + @Override + public String toString() { + return "Rectangle(x1="+x1+",y1="+y1+",x2="+x2+",y2="+y2+")"; + } + + @Override + public Rectangle clone() + { + return new Rectangle(x1,y1,x2,y2); + } + + /** + * check if this is inside of (or equal) another rectangle + * @param other + * @return true if this rectangle is equal to or inside of the other one + */ + public boolean isInsideOf(Rectangle other) { + return this.getXInterval().isSubsetOf(other.getXInterval()) + && this.getYInterval().isSubsetOf(other.getYInterval()); + } + + /** + * check if the intersection of this rectangle with another one is not empty + * @param other + * @return true if rectangles have at least one point in common + */ + public boolean intersects(Rectangle other) { + return this.getXInterval().intersects(other.getXInterval()) + && this.getYInterval().intersects(other.getYInterval()); + } +} diff --git a/src/com/t_oster/liblasercut/utils/VectorOptimizer.java b/src/com/t_oster/liblasercut/utils/VectorOptimizer.java index e66a4510ad4e5da1e8fd195a1da3b5494fdb69e8..7a999994acd9f8c6facd15065877f8ddbd28a9b3 100644 --- a/src/com/t_oster/liblasercut/utils/VectorOptimizer.java +++ b/src/com/t_oster/liblasercut/utils/VectorOptimizer.java @@ -4,6 +4,9 @@ import com.t_oster.liblasercut.LaserProperty; import com.t_oster.liblasercut.VectorCommand; import com.t_oster.liblasercut.VectorPart; import com.t_oster.liblasercut.platform.Point; +import com.t_oster.liblasercut.platform.Rectangle; +import java.util.Collections; +import java.util.Comparator; import java.util.LinkedList; import java.util.List; @@ -16,8 +19,8 @@ public class VectorOptimizer public enum OrderStrategy { FILE, - //INNER_FIRST, - NEAREST + NEAREST, + INNER_FIRST } class Element @@ -39,11 +42,42 @@ public class VectorOptimizer moves = inv; } } + + + Point getEnd() { return moves.isEmpty() ? start : moves.get(moves.size()-1); } + + /** + * compute bounding box of moves, including start point + * @return Rectangle + */ + Rectangle boundingBox() { + if (start == null) { // TODO may this happen? + return null; + } + Rectangle bb=new Rectangle(start.x,start.y,start.x,start.y); + for (Point p: moves) { + bb.add(p); + } + return bb; + } + + /** + * test if this Element represents a closed path (polygon) + * @return true if start equals end, false otherwise + */ + boolean isClosedPath() { + if ((start == null) || moves.isEmpty()) { + return false; + } + return getEnd().equals(start); + } } + + private OrderStrategy strategy = OrderStrategy.FILE; @@ -166,6 +200,144 @@ public class VectorOptimizer } break; } + case INNER_FIRST: { + /** cut inside parts first, outside parts later + * this algorithm is very robust, it works even for unconnected paths that are split into individual lines (e.g. from some DXF imports) + * it is not completely perfect, as it only considers the bounding-box and not the individual path + * + * see below for documentation of the inner workings + */ + + // helper classes: + abstract class ElementValueComparator implements Comparator<Element> { + /** + * get one integer from the element + * order ascending by this integer + * inside objects should have the lowest values + */ + abstract int getValue(Element e); + + /** + * compare by getValue() + */ + @Override + public int compare(Element a, Element b) { + Integer av = new Integer(getValue(a)); + Integer bv = new Integer(getValue(b)); + return av.compareTo(bv); + } + + } + + class XMinComparator extends ElementValueComparator { + // compare by XMin a>b + @Override + int getValue(Element e) { + return -e.boundingBox().getXMin(); + } + } + + class YMinComparator extends ElementValueComparator { + // compare by YMin a>b + @Override + int getValue(Element e) { + return -e.boundingBox().getYMin(); + } + } + + class XMaxComparator extends ElementValueComparator { + // compare by XMax a<b + @Override + int getValue(Element e) { + return e.boundingBox().getXMax(); + } + } + + class YMaxComparator extends ElementValueComparator { + // compare by YMax a<b + @Override + int getValue(Element e) { + return e.boundingBox().getYMax(); + } + } + result.addAll(e); + /** + * HEURISTIC: + * this algorithm is based on the following observation: + * let I and O be rectangles, I inside O + * for explanations, assume that: + * - the X-axis goes from left to right + * - the Y-axis goes from bottom to top + * + * ---------------- O: outside rectangle + * | | + * | ---- | + * y axis | |in| I | + * ^ | ---- | + * | | | + * | ---------------- + * | + * ------> x axis + * + * look at each border: + * right border: I.getXMax() < O.getXMax() + * left border: I.getXMin() > O.getXMin() + * top border: I.getYMax() < O.getYMax() + * bottom border: I.getYMin() > O.getYMin() + * + * If we now SORT BY ymax ASCENDING, ymin DESCENDING, xmax ASCENDING, xmin DESCENDING + * (higher sorting priority listed first) + * we get the rectangles sorted inside-out: + * 1. I + * 2. O + * + * Because we sort by four values, this still works if + * the two rectangles start at the same corner and have the same width, + * but only differ in height. + * + * If each rectangle is split into four separate lines + * (e.g. because of a bad DXF import), + * this still mostly works: + * 1. O: bottom line + * 2. I: bottom + * 3. I: top, left, right (both have same YMax, but top has a higher YMin) + * 4: O: top, left, right (both have same YMax, but top has a higher YMin) + * + * TRADEOFFS AND LIMITATIONS: + * This algorithm does not work for paths that have the same bounding-box + * (e.g. a circle inscribed to a square) + * + * For concave polygons with the same bounding-box, + * many simple Polygon-inside-Polygon algorithms also fail + * (or have a useless definition of "inside" that matches the misbehaviour): + * Draw a concave polygon, remove one point at a concave edge. + * The resulting polygon is clearly outside the original, although every edge of it is inside the original! + * + * FUTURE WORK: + * It would also be nice to sort intersecting polygons, where one polygon + * is "90% inside" and "10% outside" the other. + * Real-world example:_A circular hole at the border of a rectangle. + * Due to rounding errors, it may appear slightly outside the rectangle. + * Mathematically, it is neither fully inside nor fully outside, but the + * user clearly wants it to be counted as "inside". + * + * POSSIBLE LIBRARIES: + * http://sourceforge.net/projects/geom-java/ + * http://sourceforge.net/projects/jts-topo-suite + * + * USEFUL METHODS: + * Element.isClosedPath() + */ + + // do the work: + Collections.sort(result,new XMinComparator()); + Collections.sort(result,new YMinComparator()); + Collections.sort(result,new XMaxComparator()); + Collections.sort(result,new YMaxComparator()); + + // the result is now mostly sorted + // TODO somehow sort by intersecting area + } } return result; }