/** * This file is part of LibLaserCut. * Copyright (C) 2011 - 2013 Thomas Oster <thomas.oster@rwth-aachen.de> * RWTH Aachen University - 52062 Aachen, Germany * * LibLaserCut 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. * * LibLaserCut 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 LibLaserCut. If not, see <http://www.gnu.org/licenses/>. **/ package com.t_oster.liblasercut.utils; 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; /** * * @author Thomas Oster <thomas.oster@rwth-aachen.de> */ public class VectorOptimizer { public enum OrderStrategy { FILE, NEAREST, INNER_FIRST, SMALLEST_FIRST } class Element { LaserProperty prop; Point start; List<Point> moves = new LinkedList<Point>(); void invert() { if (!moves.isEmpty()) { moves.add(0, start); start = moves.remove(moves.size()-1); List<Point> inv = new LinkedList<Point>(); while (!moves.isEmpty()) { inv.add(moves.remove(moves.size()-1)); } 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; public VectorOptimizer(OrderStrategy s) { this.strategy = s; } private List<Element> divide(VectorPart vp) { List<Element> result = new LinkedList<Element>(); Element cur = null; Point lastMove = null; LaserProperty lastProp = null; boolean stop = false; for (VectorCommand cmd : vp.getCommandList()) { switch (cmd.getType()) { case MOVETO: { lastMove = new Point(cmd.getX(), cmd.getY()); stop = true; break; } case LINETO: { if (stop) { stop = false; if (cur != null) { result.add(cur); } cur = new Element(); cur.start = lastMove; cur.prop = lastProp; } cur.moves.add(new Point(cmd.getX(), cmd.getY())); break; } case SETPROPERTY: { lastProp = cmd.getProperty(); stop = true; break; } } } if (cur != null) { result.add(cur); } return result; } private double dist(Point a, Point b) { return Math.sqrt((a.y-b.y)*(a.y-b.y)+(a.x-b.x)*(a.x-b.x)); } // helper classes: private 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); } } private List<Element> sort(List<Element> e) { List<Element> result = new LinkedList<Element>(); if (e.isEmpty()) { return result; } switch (strategy) { case FILE: { result.addAll(e); break; } case NEAREST: { result.add(e.remove(0)); while (!e.isEmpty()) { Point end = result.get(result.size()-1).getEnd(); //find nearest element int next = 0; //invert element direction if endpoint is nearer boolean invert = false; double dst = -1; for (int i = 1; i < e.size(); i++) { //check distance to startpoint double nd = dist(e.get(i).start, end); if (nd < dst || dst == -1) { next = i; dst = nd; invert = false; } if (!e.get(i).start.equals(e.get(i).getEnd())) { //check distance to endpoint nd = dist(e.get(i).getEnd(), end); if (nd < dst || dst == -1) { next = i; dst = nd; invert = true; } } } //add next if (invert) { Element m = e.remove(next); m.invert(); result.add(m); } else { result.add(e.remove(next)); } } 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 */ 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 break; } case SMALLEST_FIRST: { /** cut smaller parts first, bigger parts later Heuristic is explained below... */ class SmallerComparator extends ElementValueComparator { // compare by XMin a>b @Override int getValue(Element e) { return (e.boundingBox().getXMax()-e.boundingBox().getXMin()) * (e.boundingBox().getYMax()-e.boundingBox().getYMin()); } } result.addAll(e); /** * HEURISTIC: * this algorithm is based on the following observation: * let S and B be rectangles, S smaller than B * for explanations, assume that: * - the X-axis goes from left to right * - the Y-axis goes from bottom to top * * ---------------- B: bigger rectangle * | | * | ---- | * y axis | | S| | * ^ | ---- | * | | | * | ---------------- * | * ------> x axis * * we get the rectangles sorted by size * 1. S * 2. B */ // do the work: Collections.sort(result,new SmallerComparator()); // the result is now mostly sorted break; } } return result; } public VectorPart optimize(VectorPart vp) { List<Element> opt = this.sort(this.divide(vp)); LaserProperty cp = opt.isEmpty() ? vp.getCurrentCuttingProperty() : opt.get(0).prop; VectorPart result = new VectorPart(cp, vp.getDPI()); for (Element e : opt) { if (!e.prop.equals(cp)) { result.setProperty(e.prop); cp = e.prop; } result.moveto(e.start.x, e.start.y); for (Point p : e.moves) { result.lineto(p.x, p.y); } } return result; } }