/** * This class stores a rectangular grid at the same grid resolution * as the RepRap machine's finest resolution using the Java BitSet class. * * There are two types of pixel: solid (or true), * and air (or false). * * There are Boolean operators implemented to allow unions, intersections, * and differences of two bitmaps, and complements of one. * * There are also functions to do ray-trace intersections, to find the parts * of lines that are solid, and outline following, to find the perimiters of * solid shapes as polygons. * * The class makes extensive use of lazy evaluation. * * @author Adrian Bowyer * */ package org.reprap.geometry.polygons; import org.reprap.Attributes; import org.reprap.Preferences; import java.util.ArrayList; import java.util.List; import java.util.BitSet; import org.reprap.utilities.Debug; public class BooleanGrid { // Various internal classes to make things work... /** * Integer 2D point * @author ensab * */ class iPoint { private int x, y; iPoint(int xa, int ya) { x = xa; y = ya; } /** * Copy constructor * @param a */ iPoint(iPoint a) { x = a.x; y = a.y; } /** * Convert real-world point to integer * @param a */ iPoint(Point2D a) { x = iScale(a.x()) - rec.swCorner.x; y = iScale(a.y()) - rec.swCorner.y; } /** * Generate the equivalent real-world point * @return */ Point2D realPoint() { return new Point2D(scale(rec.swCorner.x + x), scale(rec.swCorner.y + y)); } /** * Are two points the same? * @param b * @return */ boolean coincidesWith(iPoint b) { return x == b.x && y == b.y; } /** * Vector addition * @param b * @return */ iPoint add(iPoint b) { return new iPoint(x + b.x, y + b.y); } /** * Vector subtraction * @param b * @return */ iPoint sub(iPoint b) { return new iPoint(x - b.x, y - b.y); } /** * Opposite direction * @return */ iPoint neg() { return new iPoint(-x, -y); } /** * Absolute value * @return */ iPoint abs() { return new iPoint(Math.abs(x), Math.abs(y)); } /** * Squared length * @return */ long magnitude2() { return x*x + y*y; } /** * Scalar product * @param a * @return */ long scalarProduct(iPoint a) { return x*a.x + y*a.y; } /** * For printing */ public String toString() { return ": " + x + ", " + y + " :"; } } /** * Small class to hold rectangles represented by the sw point and * the size. * @author ensab * */ class iRectangle { public iPoint swCorner; public iPoint size; /** * Construct from the corner points * @param min * @param max */ public iRectangle(iPoint min, iPoint max) { swCorner = new iPoint(min); size = max.sub(min); size.x++; size.y++; } /** * Copy constructor * @param r */ public iRectangle(iRectangle r) { swCorner = new iPoint(r.swCorner); size = new iPoint(r.size); } /** * Useful to have a single-pixel at the origin * */ private iRectangle() { swCorner = new iPoint(0, 0); size = new iPoint(1, 1); } /** * Are two rectangles the same? * @param b * @return */ public boolean coincidesWith(iRectangle b) { return swCorner.coincidesWith(b.swCorner) && size.coincidesWith(b.size); } /** * This rectangle in the real world * @return */ public Rectangle realRectangle() { return new Rectangle(swCorner.realPoint(), new iPoint(swCorner.x + size.x - 1, swCorner.y + size.y - 1).realPoint()); } /** * Big rectangle containing the union of two. * @param b * @return */ public iRectangle union(iRectangle b) { iRectangle result = new iRectangle(this); result.swCorner.x = Math.min(result.swCorner.x, b.swCorner.x); result.swCorner.y = Math.min(result.swCorner.y, b.swCorner.y); int sx = result.swCorner.x + result.size.x - 1; sx = Math.max(sx, b.swCorner.x + b.size.x - 1) - result.swCorner.x + 1; int sy = result.swCorner.y + result.size.y - 1; sy = Math.max(sy, b.swCorner.y + b.size.y - 1) - result.swCorner.y + 1; result.size = new iPoint(sx, sy); return result; } /** * Rectangle containing the intersection of two. * @param b * @return */ public iRectangle intersection(iRectangle b) { iRectangle result = new iRectangle(this); result.swCorner.x = Math.max(result.swCorner.x, b.swCorner.x); result.swCorner.y = Math.max(result.swCorner.y, b.swCorner.y); int sx = result.swCorner.x + result.size.x - 1; sx = Math.min(sx, b.swCorner.x + b.size.x - 1) - result.swCorner.x + 1; int sy = result.swCorner.y + result.size.y - 1; sy = Math.min(sy, b.swCorner.y + b.size.y - 1) - result.swCorner.y + 1; result.size = new iPoint(sx, sy); return result; } /** * Grow (dist +ve) or shrink (dist -ve). * @param dist * @return */ public iRectangle offset(int dist) { iRectangle result = new iRectangle(this); result.swCorner.x = result.swCorner.x - dist; result.swCorner.y = result.swCorner.y - dist; result.size.x = result.size.x + 2*dist; result.size.y = result.size.y + 2*dist; return result; } /** * Anything there? * @return */ public boolean isEmpty() { return size.x < 0 | size.y < 0; } } /** * Integer-point polygon * @author ensab * */ class iPolygon { /** * Auto-extending list of points */ private List<iPoint> points = null; /** * Does the polygon loop back on itself? */ private boolean closed; public iPolygon(boolean c) { points = new ArrayList<iPoint>(); closed = c; } /** * Deep copy * @param a */ public iPolygon(iPolygon a) { points = new ArrayList<iPoint>(); for(int i = 0; i < a.size(); i++) add(a.point(i)); closed = a.closed; } /** * Return the point at a given index * @param i * @return */ public iPoint point(int i) { return points.get(i); } /** * How many points? * @return */ public int size() { return points.size(); } /** * Add a new point on the end * @param p */ public void add(iPoint p) { points.add(p); } /** * Add a whole polygon on the end * @param a */ public void add(iPolygon a) { for(int i = 0; i < a.size(); i++) add(a.point(i)); } /** * Delete a point and close the resulting gap * @param i */ public void remove(int i) { points.remove(i); } /** * Find the index of the point in the polygon nearest to another point as long as * it's less than tooFar2. Set that to Long.MAX_VALUE for a complete search. * @param a * @param tooFar2 * @return */ public int nearest(iPoint a, long tooFar2) { int i = 0; int j = -1; long d0 = tooFar2; while(i < size()) { long d1 = point(i).sub(a).magnitude2(); if(d1 < d0) { j = i; d0 = d1; } i++; } return j; } /** * Negate (i.e. reverse cyclic order) * @return reversed polygon object */ public iPolygon negate() { iPolygon result = new iPolygon(closed); for(int i = size() - 1; i >= 0; i--) result.add(point(i)); return result; } /** * Transtate by vector t * @param t * @return */ public iPolygon translate(iPoint t) { iPolygon result = new iPolygon(closed); for(int i = 0; i < size(); i++) result.add(point(i).add(t)); return result; } /** * Find the furthest point from point v1 on the polygon such that the polygon between * the two can be approximated by a DDA straight line from v1. * @param v1 * @return */ private int findAngleStart(int v1) { int top = size() - 1; int bottom = v1; iPoint p1 = point(v1); int offCount = 0; while(top - bottom > 1) { int middle = (bottom + top)/2; DDA line = new DDA(p1, point(middle)); iPoint n = line.next(); offCount = 0; int j = v1; while(j <= middle && n != null && offCount < 2) { if(point(j).coincidesWith(n)) offCount = 0; else offCount++; n = line.next(); j++; } if(offCount < 2) bottom = middle; else top = middle; } if(offCount < 2) return top; else return bottom; } /** * Generate an equivalent polygon with fewer vertices by removing chains of points * that lie in straight lines. * @return */ public iPolygon simplify() { if(size() <= 3) return new iPolygon(this); iPolygon r = new iPolygon(closed); int v = 0; do { r.add(point(v)); v = findAngleStart(v); }while(v < size() - 1); r.add(point(v)); return r; } /** * Convert the polygon into a polygon in the real world. * @param a * @return */ public Polygon realPolygon(Attributes a) { Polygon result = new Polygon(a, closed); for(int i = 0; i < size(); i++) result.add(point(i).realPoint()); return result; } } /** * A list of polygons * @author ensab * */ class iPolygonList { private List<iPolygon> polygons = null; // protected void finalize() throws Throwable // { // polygons = null; // super.finalize(); // } public iPolygonList() { polygons = new ArrayList<iPolygon>(); } /** * Return the ith polygon * @param i * @return */ public iPolygon polygon(int i) { return polygons.get(i); } /** * How many polygons are there in the list? * @return */ public int size() { return polygons.size(); } /** * Add a polygon on the end * @param p */ public void add(iPolygon p) { polygons.add(p); } /** * Replace a polygon in the list * @param i * @param p */ public void set(int i, iPolygon p) { polygons.set(i, p); } /** * Get rid of a polygon from the list * @param i */ public void remove(int i) { polygons.remove(i); } /** * Add another list of polygons on the end * @param a */ public void add(iPolygonList a) { for(int i = 0; i < a.size(); i++) add(a.polygon(i)); } /** * Transtate by vector t * @param t * @return */ public iPolygonList translate(iPoint t) { iPolygonList result = new iPolygonList(); for(int i = 0; i < size(); i++) result.add(polygon(i).translate(t)); return result; } /** * Turn all the polygons into real-world polygons * @param a * @return */ public PolygonList realPolygons(Attributes a) { PolygonList result = new PolygonList(); for(int i = 0; i < size(); i++) result.add(polygon(i).realPolygon(a)); return result; } /** * Simplify all the polygons * @return */ public iPolygonList simplify() { iPolygonList result = new iPolygonList(); for(int i = 0; i < size(); i++) result.add(polygon(i).simplify()); return result; } } /** * Straight-line DDA * @author ensab * */ class DDA { private iPoint delta, count, p; private int steps, taken; private boolean xPlus, yPlus, finished; // protected void finalize() throws Throwable // { // delta = null; // count = null; // p = null; // super.finalize(); // } /** * Set up the DDA between a start and an end point * @param s * @param e */ DDA(iPoint s, iPoint e) { delta = e.sub(s).abs(); steps = Math.max(delta.x, delta.y); taken = 0; xPlus = e.x >= s.x; yPlus = e.y >= s.y; count = new iPoint(-steps/2, -steps/2); p = new iPoint(s); finished = false; } /** * Return the next point along the line, or null * if the last point returned was the final one. * @return */ iPoint next() { if(finished) return null; iPoint result = new iPoint(p); finished = taken >= steps; if(!finished) { taken++; count = count.add(delta); if (count.x > 0) { count.x -= steps; if (xPlus) p.x++; else p.x--; } if (count.y > 0) { count.y -= steps; if (yPlus) p.y++; else p.y--; } } return result; } } /** * Little class to hold the ends of hatching patterns. Snakes are a combination of the hatching * lines that infill a shape plus the bits of boundary that join their ends to make a zig-zag pattern. * @author ensab * */ class SnakeEnd { public iPolygon track; public int hitPlaneIndex; // protected void finalize() throws Throwable // { // track = null; // super.finalize(); // } public SnakeEnd(iPolygon t, int h) { track = t; hitPlaneIndex = h; } } //************************************************************************************************** // Start of BooleanGrid proper /** * The resolution of the RepRap machine */ private static final double pixSize = Preferences.machineResolution()*0.6; private static final double realResolution = pixSize*1.5; private static final double rSwell = 0.5; // mm by which to swell rectangles to give margins round stuff //private static final int searchDepth = 3; /** * How simple does a CSG expression have to be to not be worth pruning further? */ private static final int simpleEnough = 3; private static final BooleanGrid nothingThere = new BooleanGrid(); /** * Run round the eight neighbours of a pixel anticlockwise from bottom left */ private final iPoint[] neighbour = { new iPoint(-1, -1), //0 / new iPoint(0, -1), //1 V new iPoint(1, -1), //2 \ new iPoint(1, 0), //3 -> new iPoint(1, 1), //4 / new iPoint(0, 1), //5 ^ new iPoint(-1, 1), //6 \ new iPoint(-1, 0) //7 < }; // Marching squares directions. 2x2 grid bits: // // 0 1 // // 2 3 private final int[] march = { -1, // 0 5, // 1 3, // 2 3, // 3 7, // 4 5, // 5 7, // 6 3, // 7 1, // 8 5, // 9 1, // 10 1, // 11 7, // 12 5, // 13 7, // 14 -1 // 15 }; /** * Lookup table for whether to cull points forming thin bridges. The index into this is the byte bitpattern * implied by the neighbour array. This table is generated interactively by the program * org.reprap.utilities.FilterGenerator. */ private static final boolean[] thinFilter = { true, true, true, false, true, true, false, true, true, true, true, true, false, true, false, false, true, true, true, true, true, true, true, true, false, true, true, true, true, true, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, true, false, true, true, true, true, true, true, true, false, true, true, true, false, true, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, true, false, false, false, true, false, false, false, false, true, true, true, true, false, false, false, true, false, false, false, true, false, false, false, true, true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, true, false, true, false, true, true, true, false, true, true, true, false, true, true, true, false, true, true, true, true, true, false, false, false, true, false, false, false, true, false, false, false, true, true, true, true, true, false, false, false, true, false, false, false, true, false, false, false, true, true, true, true, true, false, false, false, false, false, false, false, true, false, false, false, false, true, true, false, true, true, true, false, true, true, true, true, true, true, true, false, true, true, true, true, true, false, false, false, true, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, false, false, true, false, true, false, false, false, false, false, false, false, false, false, false, false }; /** * Lookup table behaves like scalar product for two neighbours i and j; get it by neighbourProduct[Math.abs(j - i)] */ private final int[] neighbourProduct = {2, 1, 0, -1, -2, -1, 0, 1}; /** * The pixel map */ private BitSet bits; /** * Flags for visited pixels during searches */ private BitSet visited; /** * The rectangle the pixelmap covers */ private iRectangle rec; /** * The attributes */ private Attributes att; private Boolean isThin = false; //************************************************************************************************** // Debugging and timing private static String stack[] = new String[20]; private static int sp = -1; private static boolean debug = false; private void push(String s) { if(!debug) return; sp++; stack[sp] = s; String str = ""; for(int i = 0; i < sp; i++) str += " "; Debug.a("{ " + str + s); } private void pop() { if(!debug) return; String str = ""; for(int i = 0; i < sp; i++) str += " "; Debug.a("} " + str + stack[sp]); sp--; } //************************************************************************************************** // Constructors and administration /** * Back and forth from real to pixel/integer coordinates * @param i * @return */ static double scale(int i) { return i*pixSize; } static int iScale(double d) { return (int)Math.round(d/pixSize); } /** * Build the grid from a CSG expression * @param csgP */ public BooleanGrid(CSG2D csgExp, Rectangle rectangle, Attributes a) { att = a; isThin = false; Rectangle ri = rectangle.offset(rSwell); rec = new iRectangle(new iPoint(0, 0), new iPoint(1, 1)); // Set the origin to (0, 0)... rec.swCorner = new iPoint(ri.sw()); // That then gets subtracted by the iPoint constructor to give the true origin rec.size = new iPoint(ri.ne()); // The true origin is now automatically subtracted. bits = new BitSet(rec.size.x*rec.size.y); visited = null; push("Build quad tree... "); //Debug.e("Quad start."); generateQuadTree(new iPoint(0, 0), new iPoint(rec.size.x - 1, rec.size.y - 1), csgExp); //Debug.e("Quad end."); pop(); deWhisker(); } /** * Copy constructor * N.B. attributes are _not_ deep copied * @param bg */ public BooleanGrid(BooleanGrid bg) { att = bg.att; visited = null; isThin = bg.isThin; rec= new iRectangle(bg.rec); bits = (BitSet)bg.bits.clone(); } /** * Copy constructor with new rectangle * N.B. attributes are _not_ deep copied * @param bg */ public BooleanGrid(BooleanGrid bg, iRectangle newRec) { att = bg.att; visited = null; isThin = bg.isThin; rec= new iRectangle(newRec); bits = new BitSet(rec.size.x*rec.size.y); iRectangle recScan = rec.intersection(bg.rec); int offxOut = recScan.swCorner.x - rec.swCorner.x; int offyOut = recScan.swCorner.y - rec.swCorner.y; int offxIn = recScan.swCorner.x - bg.rec.swCorner.x; int offyIn = recScan.swCorner.y - bg.rec.swCorner.y; for(int x = 0; x < recScan.size.x; x++) for(int y = 0; y < recScan.size.y; y++) bits.set(pixI(x + offxOut, y + offyOut), bg.bits.get(bg.pixI(x + offxIn, y + offyIn))); } /** * The empty grid */ private BooleanGrid() { att = new Attributes(null, null, null, null); rec = new iRectangle(); bits = new BitSet(1); isThin = false; visited = null; } /** * The empty set * @return */ public static BooleanGrid nullBooleanGrid() { return nothingThere; } /** * Overwrite the attributes * Only to be used if you know what you're doing... * @param a */ public void forceAttribute(Attributes a) { att = a; } /** * The index of a pixel in the 1D bit array. * @param x * @param y * @return */ private int pixI(int x, int y) { return x*rec.size.y + y; } /** * The index of a pixel in the 1D bit array. * @param p * @return */ private int pixI(iPoint p) { return pixI(p.x, p.y); } /** * The pixel corresponding to an index into the bit array * @param i * @return */ private iPoint pixel(int i) { return new iPoint(i/rec.size.y, i%rec.size.y); } /** * Return the attributes * @return */ public Attributes attribute() { return att; } /** * Bitmap of thin lines? * @return */ public Boolean isThin() { return isThin; } public void setThin(Boolean t) { isThin = t; } /** * Any pixels set? * @return */ public boolean isEmpty() { return bits.isEmpty(); } /** * Is a point inside the image? * @param p * @return */ private boolean inside(iPoint p) { if(p.x < 0) return false; if(p.y < 0) return false; if(p.x >= rec.size.x) return false; if(p.y >= rec.size.y) return false; return true; } /** * Set pixel p to value v * @param p * @param v */ public void set(iPoint p, boolean v) { if(!inside(p)) { Debug.e("BoolenGrid.set(): attempt to set pixel beyond boundary!"); return; } bits.set(pixI(p), v); } /** * Fill a disc centre c radius r with v * @param c * @param r * @param v */ public void disc(iPoint c, int r, boolean v) { for(int x = -r; x <= r; x++) { int xp = c.x + x; if(xp > 0 && xp < rec.size.x) { int y = (int)Math.round(Math.sqrt((double)(r*r - x*x))); int yp0 = c.y - y; int yp1 = c.y + y; yp0 = Math.max(yp0, 0); yp1 = Math.min(yp1, rec.size.y - 1); if(yp0 <= yp1) bits.set(pixI(xp, yp0), pixI(xp, yp1) + 1, v); } } } /** * Fill a disc centre c radius r with v * @param c * @param r * @param v */ public void disc(Point2D c, double r, boolean v) { disc(new iPoint(c), iScale(r), v); } /** * Fill a rectangle with centreline running from p0 to p1 of width 2r with v * @param p0 * @param p1 * @param r * @param v */ public void rectangle(iPoint p0, iPoint p1, int r, boolean v) { r = Math.abs(r); Point2D rp0 = new Point2D(p0.x, p0.y); Point2D rp1 = new Point2D(p1.x, p1.y); HalfPlane[] h = new HalfPlane[4]; h[0] = new HalfPlane(rp0, rp1); h[2] = h[0].offset(r); h[0] = h[0].offset(-r).complement(); h[1] = new HalfPlane(rp0, Point2D.add(rp0, h[2].normal())); h[3] = new HalfPlane(rp1, Point2D.add(rp1, h[0].normal())); double xMin = Double.MAX_VALUE; double xMax = Double.MIN_VALUE; Point2D p = null; for(int i = 0; i < 4; i++) { try { p = h[i].cross_point(h[(i+1)%4]); } catch (Exception e) {} xMin = Math.min(xMin, p.x()); xMax = Math.max(xMax, p.x()); } int iXMin = (int)Math.round(xMin); iXMin = Math.max(iXMin, 0); int iXMax = (int)Math.round(xMax); iXMax = Math.min(iXMax, rec.size.x - 1); for(int x = iXMin; x <= iXMax; x++) { Line yLine = new Line(new Point2D(x, 0), new Point2D(x, 1)); Interval iv = Interval.bigInterval(); for(int i = 0; i < 4; i++) iv = h[i].wipe(yLine, iv); if(!iv.empty()) { int yLow = (int)Math.round(yLine.point(iv.low()).y()); int yHigh = (int)Math.round(yLine.point(iv.high()).y()); yLow = Math.max(yLow, 0); yHigh = Math.min(yHigh, rec.size.y - 1); if(yLow <= yHigh) bits.set(pixI(x, yLow), pixI(x, yHigh) + 1, v); } } } /** * Fill a rectangle with centreline running from p0 to p1 of width 2r with v * @param p0 * @param p1 * @param r * @param v */ public void rectangle(Point2D p0, Point2D p1, double r, boolean v) { rectangle(new iPoint(p0), new iPoint(p1), iScale(r), v); } /** * Set a whole rectangle to one value * @param ipsw * @param ipne * @param v */ private void homogeneous(iPoint ipsw, iPoint ipne, boolean v) { for(int x = ipsw.x; x <= ipne.x; x++) bits.set(pixI(x, ipsw.y), pixI(x, ipne.y) + 1, v); } /** * Set a whole rectangle to one value * @param ipsw * @param ipne * @param v */ public void homogeneous(Point2D ipsw, Point2D ipne, boolean v) { homogeneous(new iPoint(ipsw), new iPoint(ipne), v); } /** * Set a whole rectangle to the right values for a CSG expression * @param ipsw * @param ipne * @param v */ private void heterogeneous(iPoint ipsw, iPoint ipne, CSG2D csgExpression) { for(int x = ipsw.x; x <= ipne.x; x++) for(int y = ipsw.y; y <= ipne.y; y++) bits.set(pixI(x, y), csgExpression.value(new iPoint(x, y).realPoint()) <= 0); } /** * The rectangle surrounding the set pixels in real coordinates. * @return */ public Rectangle box() { return new Rectangle(new iPoint(0, 0).realPoint(), new iPoint(rec.size.x - 1, rec.size.y - 1).realPoint()); } /** * The value at a point. * @param p * @return */ public boolean get(iPoint p) { if(!inside(p)) return false; return bits.get(pixI(p)); } /** * Get the value at the point corresponding to somewhere in the real world * That is, membership test. * @param p * @return */ public boolean get(Point2D p) { return get(new iPoint(p)); } /** * Set a point as visited * @param p * @param v */ private void vSet(iPoint p, boolean v) { if(!inside(p)) { Debug.e("BoolenGrid.vSet(): attempt to set pixel beyond boundary!"); return; } if(visited == null) visited = new BitSet(rec.size.x*rec.size.y); visited.set(pixI(p), v); } /** * Has this point been visited? * @param p * @return */ private boolean vGet(iPoint p) { if(visited == null) return false; if(!inside(p)) return false; return visited.get(pixI(p)); } public long pixelCount() { return bits.cardinality(); } /** * Find a set point * @return */ private iPoint findSeed_i() { for(int x = 0; x < rec.size.x; x++) for(int y = 0; y < rec.size.y; y++) { iPoint p = new iPoint(x, y); if(get(p)) return p; } return null; } /** * Find a set point * @return */ public Point2D findSeed() { iPoint p = findSeed_i(); if(p == null) return null; else return p.realPoint(); } /** * Find the centroid of the shape(s) * @return */ private iPoint findCentroid_i() { iPoint sum = new iPoint(0,0); int points = 0; for(int x = 0; x < rec.size.x; x++) for(int y = 0; y < rec.size.y; y++) { iPoint p = new iPoint(x, y); if(get(p)) { sum = sum.add(p); points++; } } if(points == 0) return null; return new iPoint(sum.x/points, sum.y/points); } /** * Find the centroid of the shape(s) * @return */ public Point2D findCentroid() { iPoint p = findCentroid_i(); if(p == null) return null; else return p.realPoint(); } /** * Generate the entire image from a CSG experession recursively * using a quad tree. * @param ipsw * @param ipne * @param csg */ private void generateQuadTree(iPoint ipsw, iPoint ipne, CSG2D csgExpression) { Point2D inc = new Point2D(pixSize*0.5, pixSize*0.5); Point2D p0 = ipsw.realPoint(); // Single pixel? if(ipsw.coincidesWith(ipne)) { set(ipsw, csgExpression.value(p0) <= 0); return; } // Uniform rectangle? Point2D p1 = ipne.realPoint(); Interval i = csgExpression.value(new Rectangle(Point2D.sub(p0, inc), Point2D.add(p1, inc))); if(!i.zero()) { homogeneous(ipsw, ipne, i.high() <= 0); return; } // Non-uniform, but simple, rectangle if(csgExpression.complexity() <= simpleEnough) { heterogeneous(ipsw, ipne, csgExpression); return; } // Divide this rectangle into four (roughly) congruent quads. // Work out the corner coordinates. int x0 = ipsw.x; int y0 = ipsw.y; int x1 = ipne.x; int y1 = ipne.y; int xd = (x1 - x0 + 1); int yd = (y1 - y0 + 1); int xm = x0 + xd/2; if(xd == 2) xm--; int ym = y0 + yd/2; if(yd == 2) ym--; iPoint sw, ne; // Special case - a single vertical line of pixels if(xd <= 1) { if(yd <= 1) Debug.e("BooleanGrid.generateQuadTree: attempt to divide single pixel!"); sw = new iPoint(x0, y0); ne = new iPoint(x0, ym); generateQuadTree(sw, ne, csgExpression.prune(new Rectangle(Point2D.sub(sw.realPoint(), inc), Point2D.add(ne.realPoint(), inc)))); sw = new iPoint(x0, ym+1); ne = new iPoint(x0, y1); generateQuadTree(sw, ne, csgExpression.prune(new Rectangle(Point2D.sub(sw.realPoint(), inc), Point2D.add(ne.realPoint(), inc)))); return; } // Special case - a single horizontal line of pixels if(yd <= 1) { sw = new iPoint(x0, y0); ne = new iPoint(xm, y0); generateQuadTree(sw, ne, csgExpression.prune(new Rectangle(Point2D.sub(sw.realPoint(), inc), Point2D.add(ne.realPoint(), inc)))); sw = new iPoint(xm+1, y0); ne = new iPoint(x1, y0); generateQuadTree(sw, ne, csgExpression.prune(new Rectangle(Point2D.sub(sw.realPoint(), inc), Point2D.add(ne.realPoint(), inc)))); return; } // General case - 4 quads. sw = new iPoint(x0, y0); ne = new iPoint(xm, ym); generateQuadTree(sw, ne, csgExpression.prune(new Rectangle(Point2D.sub(sw.realPoint(), inc), Point2D.add(ne.realPoint(), inc)))); sw = new iPoint(x0, ym + 1); ne = new iPoint(xm, y1); generateQuadTree(sw, ne, csgExpression.prune(new Rectangle(Point2D.sub(sw.realPoint(), inc), Point2D.add(ne.realPoint(), inc)))); sw = new iPoint(xm+1, ym + 1); ne = new iPoint(x1, y1); generateQuadTree(sw, ne, csgExpression.prune(new Rectangle(Point2D.sub(sw.realPoint(), inc), Point2D.add(ne.realPoint(), inc)))); sw = new iPoint(xm+1, y0); ne = new iPoint(x1, ym); generateQuadTree(sw, ne, csgExpression.prune(new Rectangle(Point2D.sub(sw.realPoint(), inc), Point2D.add(ne.realPoint(), inc)))); } //************************************************************************************* /** * Reset all the visited flags for the entire image * */ public void resetVisited() { if(visited != null) visited.clear(); } /** * Is a pixel on an edge? * If it is solid and there is air at at least one * of north, south, east, or west, then yes; otherwise * no. * @param a * @return */ private boolean isEdgePixel(iPoint a) { if(!get(a)) return false; for(int i = 1; i < 8; i+=2) if(!get(a.add(neighbour[i]))) return true; return false; } // /** // * Sloppy edge-test for a pixel. // * If it is solid and there is air at at least one // * neighbour then yes; otherwise no. // * @param a // * @return // */ // private boolean isSloppyEdgePixel(iPoint a) // { // if(!get(a)) // return false; // // for(int i = 0; i < 8; i++) // if(!get(a.add(neighbour[i]))) // return true; // // return false; // } /** * Find the index in the bitmap of the next unvisited edge pixel after-and-including start. * Return -1 if there isn't one. * @param start * @return */ // private int findUnvisitedEdgeIndex(int start) // { //// if(visited == null) //// { //// int i = bits.nextSetBit(start); //// if(i < 0) //// return -1; //// return i; //// } // // for(int i=bits.nextSetBit(start); i>=0; i=bits.nextSetBit(i+1)) // { // if(visited == null) // { // if(isEdgePixel(pixel(i))) // return i; // } else // if(!visited.get(i)) // if(isEdgePixel(pixel(i))) // return i; // } // return -1; // } /** * Remove whiskers (single threads of pixels) and similar nasties. * TODO: also need to do the same for cracks? * */ private void deWhisker() { push("deWhisker... "); for(int i=bits.nextSetBit(0); i>=0; i=bits.nextSetBit(i+1)) { iPoint here = pixel(i); if(neighbourCount(here) < 3) set(here, false); } for(int x = 0; x < rec.size.x - 1; x++) for(int y = 0; y < rec.size.y - 1; y++) { iPoint start = new iPoint(x, y); int m = marchPattern(start); if(m == 6 || m == 9) { if(poll(start, 3) > 0.5) { set(start, true); set(start.add(neighbour[1]), true); set(start.add(neighbour[2]), true); set(start.add(neighbour[2]), true); } else { set(start, false); set(start.add(neighbour[1]), false); set(start.add(neighbour[2]), false); set(start.add(neighbour[2]), false); } } } // int n; // for(int passes = 0; passes < 2; passes++) // { // int i = findUnvisitedEdgeIndex(0); // while(i >= 0) // { // iPoint p = pixel(i); // int filterIndex = 0; // for(n = 0; n < 8; n++) // if(get(p.add(neighbour[n]))) // filterIndex = filterIndex | (1<<n); // if(thinFilter[filterIndex]) // { // //printNearby(p, 3); // set(p, false); // //printNearby(p, 3); // } else // i++; // //// for(n = 0; n < 8; n++) //// blockSize[n] = 0; //// boolean last = get(p.add(neighbour[7])); //// boolean here; //// int nCount = 0; //// int blockCount = 0; //// for(n = 0; n < 8; n++) //// { //// here = get(p.add(neighbour[n])); //// if(here) //// nCount++; //// if(here && !last) //// blockCount++; //// if(here && last) //// blockSize[n]++; //// last = here; //// } //// //// if(blockCount > 1 || nCount < 2) //// { //// printNearby(p, 3); //// set(p, false); //// printNearby(p, 3); //// } // i = findUnvisitedEdgeIndex(i); // } // //System.out.println("end pass " + passes); // } pop(); } /** * Look-up table to find the index of a neighbour point, n, from the point. * @param n * @return */ private int neighbourIndex(iPoint n) { switch((n.y + 1)*3 + n.x + 1) { case 0: return 0; case 1: return 1; case 2: return 2; case 3: return 7; case 5: return 3; case 6: return 6; case 7: return 5; case 8: return 4; default: Debug.e("BooleanGrid.neighbourIndex(): not a neighbour point!" + n.toString()); } return 0; } /** * Count the solid neighbours of this point * @param p * @return */ private int neighbourCount(iPoint p) { int result = 0; for(int i = 0; i < 8; i++) if(get(p.add(neighbour[i]))) result++; return result; } /** * Find the index of the neighbouring point that's closest to a given real direction. * @param p * @return */ private int directionToNeighbour(Point2D p) { double score = Double.NEGATIVE_INFINITY; int result = -1; for(int i = 0; i < 8; i++) { // Can't use neighbour.realPoint as that adds swCorner... // We have to normailze neighbour, to get answers proportional to cosines double s = Point2D.mul(p, new Point2D(neighbour[i].x, neighbour[i].y).norm()); if(s > score) { result = i; score = s; } } if(result < 0) Debug.e("BooleanGrid.directionToNeighbour(): scalar product error!" + p.toString()); return result; } /** * Find a neighbour of a pixel that has not yet been visited, that is on an edge, and * that is nearest to a given neighbour direction, nd. If nd < 0 the first unvisited * neighbour is returned. If no valid neighbour exists, null is returned. This prefers to * visit valid pixels with few neighbours, and only after that tries to head in direction nd. * @param a * @param direction * @return */ private iPoint findUnvisitedNeighbourOnEdgeInDirection(iPoint a, int nd) { iPoint result = null; int directionScore = -5; int neighbourScore = 9; for(int i = 0; i < 8; i++) { iPoint b = a.add(neighbour[i]); if(isEdgePixel(b)) if(!vGet(b)) { if(nd < 0) return b; int ns = neighbourCount(b); if(ns <= neighbourScore) { neighbourScore = ns; int s = neighbourProduct[Math.abs(nd - i)]; if(s > directionScore) { directionScore = s; result = b; } } } } return result; } /** * Useful debugging function * @param p * @param b */ private String printNearby(iPoint p, int b) { String op = new String(); for(int y = p.y + b; y >= p.y - b; y--) { for(int x = p.x - b; x <= p.x + b; x++) { iPoint q = new iPoint(x, y); if(q.coincidesWith(p)) { if(get(p)) op += " +"; else op += " o"; } else if(get(q)) { if(visited != null) { if(vGet(q)) op += " v"; else op += " 1"; } else op += " 1"; } else op += " ."; } op += "\n"; } return op; } // /** // * Recursive flood-fill of solid pixels from p to return a BooleanGrid of // * just the shape connected to that pixel. // * @param p // * @return // */ // private void floodCopy_r(iPoint p, BooleanGrid newGrid) // { // if(!this.get(p) || newGrid.get(p)) // return; // // newGrid.set(p, true); // // floodCopy_r(p.add(neighbour[1]), newGrid); // floodCopy_r(p.add(neighbour[3]), newGrid); // floodCopy_r(p.add(neighbour[5]), newGrid); // floodCopy_r(p.add(neighbour[7]), newGrid); // } /** * Recursive flood-fill of solid pixels from p to return a BooleanGrid of * just the shape connected to that pixel. * @param p * @return */ public BooleanGrid floodCopy(Point2D pp) { iPoint p = new iPoint(pp); if(!this.inside(p) || !this.get(p)) return nothingThere; BooleanGrid result = new BooleanGrid(); result.att = this.att; result.visited = null; result.rec= new iRectangle(this.rec); result.bits = new BitSet(result.rec.size.x*result.rec.size.y); // We implement our own floodfill stack, rather than using recursion to // avoid having to specify a big Java stack just for this one function. int top = 200000; iPoint[] stack = new iPoint[top]; int sp = 0; stack[sp] = p; iPoint q; while(sp > -1) { p = stack[sp]; sp--; result.set(p, true); for(int i = 1; i < 8; i = i+2) { q = p.add(neighbour[i]); if(this.get(q) && !result.get(q)) { sp++; if(sp >= top) { Debug.e("BooleanGrid.floodCopy(): stack overflow!"); return result; } stack[sp] = q; } } } return result; } /** * Calculate the 4-bit marching squares value for a point * @param ip * @return */ private int marchPattern(iPoint ip) { int result = 0; if(get(ip)) result |= 1; if(get(ip.add(neighbour[3]))) result |= 2; if(get(ip.add(neighbour[1]))) result |= 4; if(get(ip.add(neighbour[2]))) result |= 8; return result; } //******************************************************************************** // Return geometrical constructions based on the pattern /** * Return all the outlines of all the solid areas as polygons consisting of * all the pixels that make up the outlines. * @return */ private iPolygonList iAllPerimitersRaw() { return marchAll(); } /** * Return all the outlines of all the solid areas as polygons in * their simplest form. * @return */ private iPolygonList iAllPerimiters() { return iAllPerimitersRaw().simplify(); } /** * Return all the outlines of all the solid areas as * real-world polygons with attributes a * @param a * @return */ public PolygonList allPerimiters(Attributes a) { PolygonList r = iAllPerimiters().realPolygons(a); r = r.simplify(realResolution); return r; } private double poll(iPoint p, int b) { int result = 0; iPoint q; for(int y = p.y + b; y >= p.y - b; y--) for(int x = p.x - b; x <= p.x + b; x++) { q = new iPoint(x, y); if(get(q)) result++; } b++; return (double)result/(double)(b*b); } /** * Run marching squares round the polygon starting with the 2x2 march pattern at start * @param start * @return */ private iPolygon marchRound(iPoint start) { iPolygon result = new iPolygon(true); iPoint here = new iPoint(start); iPoint pix; int m; boolean step = true; do { m = marchPattern(here); //pix = new iPoint(here); switch(m) { case 1: if(!vGet(here)) { result.add(here); vSet(here,true); } break; case 2: pix = here.add(neighbour[3]); if(!vGet(pix)) { result.add(pix); vSet(pix,true); } break; case 3: if(!vGet(here)) { result.add(here); vSet(here,true); } pix = here.add(neighbour[3]); if(!vGet(pix)) { result.add(pix); vSet(pix,true); } break; case 4: pix = here.add(neighbour[1]); if(!vGet(pix)) { result.add(pix); vSet(pix,true); } break; case 5: if(!vGet(here)) { result.add(here); vSet(here,true); } pix = here.add(neighbour[1]); if(!vGet(pix)) { result.add(pix); vSet(pix,true); } break; case 6: Debug.e("BooleanGrid.marchRound() - dud 2x2 grid: " + m + " at " + here.toString() + "\n" + printNearby(here,4) + "\n\n"); step = false; pix = here.add(neighbour[3]); set(pix, false); vSet(pix, false); pix = here.add(neighbour[1]); set(pix, false); vSet(pix, false); here = result.point(result.size() - 1); if(!get(here)) { if(result.size() > 1) { result.remove(result.size() - 1); here = result.point(result.size() - 1); if(!get(here)) { Debug.e("BooleanGrid.marchRound() - backtracked to an unfilled point!" + printNearby(here,4) + "\n\n"); result.remove(result.size() - 1); here = result.point(result.size() - 1); } } else { here = start; } } break; case 7: pix = here.add(neighbour[1]); if(!vGet(pix)) { result.add(pix); vSet(pix,true); } pix = here.add(neighbour[3]); if(!vGet(pix)) { result.add(pix); vSet(pix,true); } break; case 8: pix = here.add(neighbour[2]); if(!vGet(pix)) { result.add(pix); vSet(pix,true); } break; case 9: Debug.e("BooleanGrid.marchRound() - dud 2x2 grid: " + m + " at " + here.toString() + "\n" + printNearby(here,4) + "\n\n"); step = false; set(here, false); vSet(here, false); pix = here.add(neighbour[2]); set(pix, false); vSet(pix, false); here = result.point(result.size() - 1); if(!get(here)) { if(result.size() > 1) { result.remove(result.size() - 1); here = result.point(result.size() - 1); if(!get(here)) { Debug.e("BooleanGrid.marchRound() - backtracked to an unfilled point!" + printNearby(here,4) + "\n\n"); result.remove(result.size() - 1); here = result.point(result.size() - 1); } }else { here = start; } } break; case 10: pix = here.add(neighbour[3]); if(!vGet(pix)) { result.add(pix); vSet(pix,true); } pix = here.add(neighbour[2]); if(!vGet(pix)) { result.add(pix); vSet(pix,true); } break; case 11: if(!vGet(here)) { result.add(here); vSet(here,true); } pix = here.add(neighbour[2]); if(!vGet(pix)) { result.add(pix); vSet(pix,true); } break; case 12: pix = here.add(neighbour[2]); if(!vGet(pix)) { result.add(pix); vSet(pix,true); } pix = here.add(neighbour[1]); if(!vGet(pix)) { result.add(pix); vSet(pix,true); } break; case 13: pix = here.add(neighbour[2]); if(!vGet(pix)) { result.add(pix); vSet(pix,true); } if(!vGet(here)) { result.add(here); vSet(here,true); } break; case 14: pix = here.add(neighbour[3]); if(!vGet(pix)) { result.add(pix); vSet(pix,true); } pix = here.add(neighbour[1]); if(!vGet(pix)) { result.add(pix); vSet(pix,true); } break; default: Debug.e("BooleanGrid.marchRound() - dud 2x2 grid: " + m + " at " + here.toString() + "\n" + printNearby(here,4) + "\n\n"); return result; } if(step) here = here.add(neighbour[march[m]]); step = true; } while(!here.coincidesWith(start)); return result; } /** * Run marching squares round all polygons in the pattern, returning a list of them all * @return */ private iPolygonList marchAll() { //if(isThin) // return marchLines(); // XXXXXXXXXXXXXXXXXXXXXXX iPolygonList result = new iPolygonList(); if(isEmpty()) return result; iPoint start; iPolygon p; int m; for(int x = 0; x < rec.size.x - 1; x++) for(int y = 0; y < rec.size.y - 1; y++) { start = new iPoint(x, y); m = marchPattern(start); if(m != 0 && m != 15) { if( !( vGet(start) || vGet(start.add(neighbour[1])) || vGet(start.add(neighbour[2])) || vGet(start.add(neighbour[3])) ) ) { p = marchRound(start); if(p.size() > 2) result.add(p); } } } resetVisited(); return result; } /** * Generate a sequence of point-pairs where the line h enters * and leaves solid areas. The point pairs are stored in a * polygon, which should consequently have an even number of points * in it on return. * @param h * @return */ private iPolygon hatch(HalfPlane h) { iPolygon result = new iPolygon(false); Interval se = box().wipe(h.pLine(), Interval.bigInterval()); if(se.empty()) return result; iPoint s = new iPoint(h.pLine().point(se.low())); iPoint e = new iPoint(h.pLine().point(se.high())); if(get(s)) Debug.e("BooleanGrid.hatch(): start point is in solid!"); DDA dda = new DDA(s, e); iPoint n = dda.next(); iPoint nOld = n; boolean v; boolean vs = false; while(n != null) { v = get(n); if(v != vs) { if(v) result.add(n); else result.add(nOld); } vs = v; nOld = n; n = dda.next(); } if(get(e)) { Debug.e("BooleanGrid.hatch(): end point is in solid!"); result.add(e); } if(result.size()%2 != 0) Debug.e("BooleanGrid.hatch(): odd number of crossings: " + result.size()); return result; } /** * Find the bit of polygon edge between start/originPlane and targetPlane * TODO: origin == target!!! * @param start * @param hatches * @param originP * @param targetP * @return polygon edge between start/originaPlane and targetPlane */ private SnakeEnd goToPlane(iPoint start, List<HalfPlane> hatches, int originP, int targetP) { iPolygon track = new iPolygon(false); HalfPlane originPlane = hatches.get(originP); HalfPlane targetPlane = hatches.get(targetP); int dir = directionToNeighbour(originPlane.normal()); if(originPlane.value(targetPlane.pLine().origin()) < 0) dir = neighbourIndex(neighbour[dir].neg()); if(!get(start)) { Debug.e("BooleanGrid.goToPlane(): start is not solid!"); return null; } double vTarget = targetPlane.value(start.realPoint()); vSet(start, true); iPoint p = findUnvisitedNeighbourOnEdgeInDirection(start, dir); if(p == null) return null; iPoint pNew; double vOrigin = originPlane.value(p.realPoint()); boolean notCrossedOriginPlane = originPlane.value(p.realPoint())*vOrigin >= 0; boolean notCrossedTargetPlane = targetPlane.value(p.realPoint())*vTarget >= 0; while(p != null && notCrossedOriginPlane && notCrossedTargetPlane) { track.add(p); vSet(p, true); pNew = findUnvisitedNeighbourOnEdgeInDirection(p, dir); if(pNew == null) { for(int i = 0; i < track.size(); i++) vSet(track.point(i), false); return null; } dir = neighbourIndex(pNew.sub(p)); p = pNew; notCrossedOriginPlane = originPlane.value(p.realPoint())*vOrigin >= 0; notCrossedTargetPlane = targetPlane.value(p.realPoint())*vTarget >= 0; } if(notCrossedOriginPlane) return(new SnakeEnd(track, targetP)); if(notCrossedTargetPlane) return(new SnakeEnd(track, originP)); Debug.e("BooleanGrid.goToPlane(): invalid ending!"); return null; } /** * Find the piece of edge between start and end (if there is one). * @param start * @param end * @return */ private iPolygon goToPoint(iPoint start, iPoint end, HalfPlane hatch, double tooFar) { iPolygon track = new iPolygon(false); iPoint diff = end.sub(start); if(diff.x == 0 && diff.y == 0) { track.add(start); return track; } int dir = directionToNeighbour(new Point2D(diff.x, diff.y)); if(!get(start)) { Debug.e("BooleanGrid.goToPlane(): start is not solid!"); return null; } vSet(start, true); iPoint p = findUnvisitedNeighbourOnEdgeInDirection(start, dir); if(p == null) return null; while(true) { track.add(p); vSet(p, true); p = findUnvisitedNeighbourOnEdgeInDirection(p, dir); boolean lost = p == null; if(!lost) lost = Math.abs(hatch.value(p.realPoint())) > tooFar; if(lost) { for(int i = 0; i < track.size(); i++) vSet(track.point(i), false); vSet(start, false); return null; } diff = end.sub(p); if(diff.magnitude2() < 3) return track; dir = directionToNeighbour(new Point2D(diff.x, diff.y)); } } /** * Take a list of hatch point pairs from hatch (above) and the corresponding lines * that created them, and stitch them together to make a weaving snake-like hatching * pattern for infill. * @param ipl * @param hatches * @param thisHatch * @param thisPt * @return */ private iPolygon snakeGrow(iPolygonList ipl, List<HalfPlane> hatches, int thisHatch, int thisPt) { iPolygon result = new iPolygon(false); iPolygon thisPolygon = ipl.polygon(thisHatch); iPoint pt = thisPolygon.point(thisPt); result.add(pt); SnakeEnd jump; do { thisPolygon.remove(thisPt); if(thisPt%2 != 0) thisPt--; pt = thisPolygon.point(thisPt); result.add(pt); thisHatch++; if(thisHatch < hatches.size()) jump = goToPlane(pt, hatches, thisHatch - 1, thisHatch); else jump = null; thisPolygon.remove(thisPt); if(jump != null) { result.add(jump.track); thisHatch = jump.hitPlaneIndex; thisPolygon = ipl.polygon(thisHatch); thisPt = thisPolygon.nearest(jump.track.point(jump.track.size() - 1), 10); } } while(jump != null && thisPt >= 0); return result; } /** * Fine the nearest plane in the hatch to a given point * @param p * @param hatches * @return */ HalfPlane hPlane(iPoint p, List<HalfPlane> hatches) { int bot = 0; int top = hatches.size() - 1; Point2D rp = p.realPoint(); double dbot = Math.abs(hatches.get(bot).value(rp)); double dtop = Math.abs(hatches.get(top).value(rp)); while(top - bot > 1) { int mid = (top + bot)/2; if(dbot < dtop) { top = mid; dtop = Math.abs(hatches.get(top).value(rp)); } else { bot = mid; dbot = Math.abs(hatches.get(bot).value(rp)); } } if(dtop < dbot) return hatches.get(top); else return hatches.get(bot); } /** * Run through the snakes, trying to join them up to make longer snakes * @param snakes * @param hatches * @param gap */ void joinUpSnakes(iPolygonList snakes, List<HalfPlane> hatches, double gap) { int i = 0; if(hatches.size() <= 0) return; Point2D n = hatches.get(0).normal(); iPolygon track; while(i < snakes.size()) { iPoint iStart = snakes.polygon(i).point(0); iPoint iEnd = snakes.polygon(i).point(snakes.polygon(i).size() - 1); double d; int j = i+1; boolean incrementI = true; while(j < snakes.size()) { iPoint jStart = snakes.polygon(j).point(0); iPoint jEnd = snakes.polygon(j).point(snakes.polygon(j).size() - 1); incrementI = true; Point2D diff = Point2D.sub(jStart.realPoint(), iStart.realPoint()); d = Point2D.mul(diff, n); if(Math.abs(d) < 1.5*gap) { track = goToPoint(iStart, jStart, hPlane(iStart, hatches), gap); if(track != null) { iPolygon p = snakes.polygon(i).negate(); p.add(track); p.add(snakes.polygon(j)); snakes.set(i, p); snakes.remove(j); incrementI = false; break; } } diff = Point2D.sub(jEnd.realPoint(), iStart.realPoint()); d = Point2D.mul(diff, n); if(Math.abs(d) < 1.5*gap) { track = goToPoint(iStart, jEnd, hPlane(iStart, hatches), gap); if(track != null) { iPolygon p = snakes.polygon(j); p.add(track.negate()); p.add(snakes.polygon(i)); snakes.set(i, p); snakes.remove(j); incrementI = false; break; } } diff = Point2D.sub(jStart.realPoint(), iEnd.realPoint()); d = Point2D.mul(diff, n); if(Math.abs(d) < 1.5*gap) { track = goToPoint(iEnd, jStart, hPlane(iEnd, hatches), gap); if(track != null) { iPolygon p = snakes.polygon(i); p.add(track); p.add(snakes.polygon(j)); snakes.set(i, p); snakes.remove(j); incrementI = false; break; } } diff = Point2D.sub(jEnd.realPoint(), iEnd.realPoint()); d = Point2D.mul(diff, n); if(Math.abs(d) < 1.5*gap) { track = goToPoint(iEnd, jEnd, hPlane(iEnd, hatches), gap); if(track != null) { iPolygon p = snakes.polygon(i); p.add(track); p.add(snakes.polygon(j).negate()); snakes.set(i, p); snakes.remove(j); incrementI = false; break; } } j++; } if(incrementI) i++; } } /** * Hatch all the polygons parallel to line hp with increment gap * @param hp * @param gap * @param a * @return a polygon list of hatch lines as the result with attributes a */ public PolygonList hatch(HalfPlane hp, double gap, Attributes a) //, Rr2Point startNearHere) { //push("Computing hatching... "); if(gap <= 0) // Means the user has turned infill off for this; return an empty list. return new PolygonList(); Rectangle big = box().scale(1.1); double d = Math.sqrt(big.dSquared()); Point2D orth = hp.normal(); int quadPointing = (int)(2 + 2*Math.atan2(orth.y(), orth.x())/Math.PI); Point2D org = big.ne(); switch(quadPointing) { case 1: org = big.nw(); break; case 2: org = big.sw(); break; case 3: org = big.se(); break; case 0: default: break; } double dist = Point2D.mul(org, orth)/gap; dist = (1 + (long)dist)*gap; HalfPlane hatcher = new HalfPlane(hp); hatcher = hatcher.offset(dist); //HalfPlane hatcher = new // HalfPlane(org, Point2D.add(org, hp.pLine().direction())); List<HalfPlane> hatches = new ArrayList<HalfPlane>(); iPolygonList iHatches = new iPolygonList(); double g = 0; while (g < d) { iPolygon ip = hatch(hatcher); if(ip.size() > 0) { hatches.add(hatcher); iHatches.add(ip); } hatcher = hatcher.offset(gap); g += gap; } // Now we have the individual hatch lines, join them up iPolygonList snakes = new iPolygonList(); int segment; do { segment = -1; for(int i = 0; i < iHatches.size(); i++) { if((iHatches.polygon(i)).size() > 0) { segment = i; break; } } if(segment >= 0) { snakes.add(snakeGrow(iHatches, hatches, segment, 0)); } } while(segment >= 0); try { if(Preferences.loadGlobalBool("PathOptimise")) joinUpSnakes(snakes, hatches, gap); } catch (Exception e) {} resetVisited(); PolygonList result = snakes.realPolygons(a).simplify(realResolution); //result = result.nearEnds(startNearHere); //pop(); return result; } /** * This assumes that shrunk is this bitmap offset by dist from a previous calculation. * It grows shrunk by -dist, then subtracts that from itself. The result is a bitmap of all the * thin lines in this pattern that were discarded by the original offset (plus some noise at places * square convex corners that have grown back rounded). * @param shrunk * @param dist * @return */ public BooleanGrid lines(BooleanGrid shrunk, double dist) { if(dist >=0) { Debug.e("BooleanGrid.lines() called with non-negative offset: " + dist); return new BooleanGrid(); } return difference(this, shrunk.offset(-dist)); } /** * Offset the pattern by a given real-world distance. If the distance is * negative the pattern is shrunk; if it is positive it is grown; * @param dist * @return */ public BooleanGrid offset(double dist) { int r = iScale(dist); BooleanGrid result = new BooleanGrid(this, rec.offset(r)); if(r == 0) return result; iPolygonList polygons = iAllPerimiters().translate(rec.swCorner.sub(result.rec.swCorner)); if(polygons.size() <= 0) { iRectangle newRec = new iRectangle(result.rec); newRec.size.x = 1; newRec.size.y = 1; return new BooleanGrid(CSG2D.nothing(), newRec.realRectangle(), att); } for(int p = 0; p < polygons.size(); p++) { iPolygon ip = polygons.polygon(p); for(int e = 0; e < ip.size(); e++) { iPoint p0 = ip.point(e); iPoint p1 = ip.point((e+1)%ip.size()); result.rectangle(p0, p1, Math.abs(r), r > 0); result.disc(p1, Math.abs(r), r > 0); } } if(result.isEmpty()) return nothingThere; //if(dist < 0) result.deWhisker(); return result; } //********************************************************************************************************* // Boolean operators on the bitmap /** * Complement a grid * N.B. the grid doesn't get bigger, even though the expression * it contains may now fill the whole of space. * @return */ public BooleanGrid complement() { BooleanGrid result = new BooleanGrid(this); result.bits.flip(0, result.rec.size.x*result.rec.size.y - 1); //result.deWhisker(); return result; } /** * Compute the union of two bit patterns, forcing attribute a on the result. * @param d * @param e * @param a * @return */ public static BooleanGrid union(BooleanGrid d, BooleanGrid e, Attributes a) { BooleanGrid result; if(d == nothingThere) { if(e == nothingThere) return nothingThere; if(e.att == a) return e; result = new BooleanGrid(e); result.forceAttribute(a); return result; } if(e == nothingThere) { if(d.att == a) return d; result = new BooleanGrid(d); result.forceAttribute(a); return result; } if(d.rec.coincidesWith(e.rec)) { result = new BooleanGrid(d); result.bits.or(e.bits); } else { iRectangle u = d.rec.union(e.rec); result = new BooleanGrid(d, u); BooleanGrid temp = new BooleanGrid(e, u); result.bits.or(temp.bits); } //result.deWhisker(); result.forceAttribute(a); return result; } /** * Compute the union of two bit patterns * @param d * @param e * @return */ public static BooleanGrid union(BooleanGrid d, BooleanGrid e) { BooleanGrid result = union(d, e, d.att); if(result != nothingThere && d.att != e.att) Debug.e("BooleanGrid.union(): attempt to union two bitmaps of different materials: " + d.attribute().getMaterial() + " and " + e.attribute().getMaterial() ); return result; } /** * Compute the intersection of two bit patterns * @param d * @param e * @return */ public static BooleanGrid intersection(BooleanGrid d, BooleanGrid e, Attributes a) { BooleanGrid result; if(d == nothingThere || e == nothingThere) return nothingThere; if(d.rec.coincidesWith(e.rec)) { result = new BooleanGrid(d); result.bits.and(e.bits); } else { iRectangle u = d.rec.intersection(e.rec); if(u.isEmpty()) return nothingThere; result = new BooleanGrid(d, u); BooleanGrid temp = new BooleanGrid(e, u); result.bits.and(temp.bits); } if(result.isEmpty()) return nothingThere; result.deWhisker(); result.forceAttribute(a); return result; } /** * Compute the intersection of two bit patterns * @param d * @param e * @return */ public static BooleanGrid intersection(BooleanGrid d, BooleanGrid e) { BooleanGrid result = intersection(d, e, d.att); if(result != nothingThere && d.att != e.att) Debug.e("BooleanGrid.intersection(): attempt to intersect two bitmaps of different materials: " + d.attribute().getMaterial() + " and " + e.attribute().getMaterial() ); return result; } /** * Grid d - grid e, forcing attribute a on the result * d's rectangle is presumed to contain the result. * TODO: write a function to compute the rectangle from the bitmap * @param d * @param e * @param a * @return */ public static BooleanGrid difference(BooleanGrid d, BooleanGrid e, Attributes a) { if(d == nothingThere) return nothingThere; BooleanGrid result; if(e == nothingThere) { if(d.att == a) return d; result = new BooleanGrid(d); result.forceAttribute(a); return result; } result = new BooleanGrid(d); BooleanGrid temp; if(d.rec.coincidesWith(e.rec)) temp = e; else temp = new BooleanGrid(e, result.rec); result.bits.andNot(temp.bits); if(result.isEmpty()) return nothingThere; result.deWhisker(); result.forceAttribute(a); return result; } /** * Grid d - grid e * d's rectangle is presumed to contain the result. * TODO: write a function to compute the rectangle from the bitmap * @param d * @param e * @return */ public static BooleanGrid difference(BooleanGrid d, BooleanGrid e) { BooleanGrid result = difference(d, e, d.att); if(result != nothingThere && d.att != e.att) Debug.e("BooleanGrid.difference(): attempt to subtract two bitmaps of different materials: " + d.attribute().getMaterial() + " and " + e.attribute().getMaterial() ); return result; } }