Skip to content
Snippets Groups Projects
VectorOptimizer.java 12.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • /**
     * 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/>.
     **/
    
    Thomas Oster's avatar
    Thomas Oster committed
    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;
    
    Thomas Oster's avatar
    Thomas Oster committed
    import java.util.LinkedList;
    import java.util.List;
    
    /**
     *
     * @author Thomas Oster <thomas.oster@rwth-aachen.de>
     */
    public class VectorOptimizer
    {
      public enum OrderStrategy
      {
        FILE,
    
    Thomas Oster's avatar
    Thomas Oster committed
      }
    
    Thomas Oster's avatar
    Thomas Oster committed
      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;
          }
        }
    
    Thomas Oster's avatar
    Thomas Oster committed
        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);
        }
    
    Thomas Oster's avatar
    Thomas Oster committed
      }
    
    Thomas Oster's avatar
    Thomas Oster committed
      private OrderStrategy strategy = OrderStrategy.FILE;
    
    Thomas Oster's avatar
    Thomas Oster committed
      public VectorOptimizer(OrderStrategy s)
      {
        this.strategy = s;
      }
    
    Thomas Oster's avatar
    Thomas Oster committed
      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;
      }
    
    Thomas Oster's avatar
    Thomas Oster committed
      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);
            }
            
        }
        
    
        
        
    
    Thomas Oster's avatar
    Thomas Oster committed
      private List<Element> sort(List<Element> e)
      {
        List<Element> result = new LinkedList<Element>();
    
        if (e.isEmpty())
        {
          return result;
        }
    
    Thomas Oster's avatar
    Thomas Oster committed
        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
    
    Thomas Oster's avatar
    Thomas Oster committed
              int next = 0;
    
              //invert element direction if endpoint is nearer
              boolean invert = false;
    
    Thomas Oster's avatar
    Thomas Oster committed
              double dst = -1;
              for (int i = 1; i < e.size(); i++)
              {
    
                //check distance to startpoint
                double nd = dist(e.get(i).start, end);
    
    Thomas Oster's avatar
    Thomas Oster committed
                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;
                  }
    
    Thomas Oster's avatar
    Thomas Oster committed
                }
              }
              //add next
    
              if (invert)
              {
                Element m = e.remove(next);
                m.invert();
                result.add(m);
              }
              else
              {
                result.add(e.remove(next));
              }
    
    Thomas Oster's avatar
    Thomas Oster committed
            }
            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
    
            /** 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
    
    Thomas Oster's avatar
    Thomas Oster committed
        }
        return result;
      }
    
    Thomas Oster's avatar
    Thomas Oster committed
      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());
    
    Thomas Oster's avatar
    Thomas Oster committed
        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;
      }
    
    Thomas Oster's avatar
    Thomas Oster committed
    }