Skip to content
Snippets Groups Projects
Commit 2f1cf6c9 authored by Thomas Oster's avatar Thomas Oster
Browse files

Merge pull request #3 from mgmax/develop

inside-first vector sorting, improved dummy driver
parents bec871df 01eed576
No related branches found
No related tags found
No related merge requests found
......@@ -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();
}
}
......
<?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 &lt;input type=&quot;range&quot;&gt; (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
/**
* 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());
}
}
......@@ -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;
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment