/** * Origin : * http://javagraphics.java.net/jars/Clipper.jar * * Source are in BSD license. */ /* * @(#)Clipper.java * * $Date: 2012-07-03 01:10:05 -0500 (Tue, 03 Jul 2012) $ * * Copyright (c) 2011 by Jeremy Wood. * All rights reserved. * * The copyright of this software is owned by Jeremy Wood. * You may not use, copy or modify this software, except in * accordance with the license agreement you entered into with * Jeremy Wood. For details see accompanying license terms. * * This software is probably, but not necessarily, discussed here: * http://javagraphics.java.net/ * * That site should also contain the most recent official version * of this software. (See the SVN repository for more details.) */ package com.bric.geom; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.CubicCurve2D; import java.awt.geom.GeneralPath; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.Arrays; import java.util.Stack; import com.bric.util.FloatArrayFactory; /** This class lets you clip/intersect an arbitrary shape to a Rectangle2D. * */ public abstract class Clipper { private static final FloatArrayFactory floatFactory = new FloatArrayFactory(); /** This is the tolerance with which 2 numbers must * be similar to be considered "equal". * <P>This is necessary because as we much around with numbers * and equations, machine rounding will inevitably cause .5's * to become .49999's and other harmless changes. */ private static final float TOLERANCE = .0001f; /** This does 2 things: * 1. It collapses redundant line segments that fall on the same horizontal * or vertical line. This is very important, given how clipToRect() works. * And not only does it vastly simplify * your shape (lots of redundant lineTo's will be called), but if a shape * is properly collapsed it has a much better chance of return a truly * accurate result when you call getBounds() on it. * <P>Note that there are still some far fetched examples (involving discontinous * shapes) where getBounds() may be inaccurate, though. * 2. This can take a Function (either quadratic or cubic) and split it over * a smaller interval from an arbitrary [t0,t1]. */ static class ClippedPath { public final GeneralPath g; private Stack<float[]> uncommittedPoints = new Stack<float[]>(); private float initialX, initialY; public ClippedPath(int windingRule) { g = new GeneralPath(windingRule); } public void moveTo(float x,float y) { flush(); g.moveTo(x,y); initialX = x; initialY = y; } /** This makes a cubic curve to based on xf and yf that ranges from * [t0,t1]. So this takes a little subset of the curves if [t0,t1] * is smaller than [0,1]. */ public void curveTo(Function xf,Function yf,double t0,double t1) { flush(); //flush out lines double dt = (t1-t0); //I know I'm not explaining the math here, but you can derive //it with a little time and a few sheets of paper. The API for //the PathIterator shows the equations relating to bezier parametric //curves. From there you can calculate whatever you need: //it just might take a few pages of pen & paper. double dx0 = xf.getDerivative(t0)*dt; double dx1 = xf.getDerivative(t1)*dt; double dy0 = yf.getDerivative(t0)*dt; double dy1 = yf.getDerivative(t1)*dt; double x0 = xf.evaluate(t0); double x1 = xf.evaluate(t1); double y0 = yf.evaluate(t0); double y1 = yf.evaluate(t1); g.curveTo( (float)(x0+dx0/3), (float)(y0+dy0/3), (float)(x1-dx1/3), (float)(y1-dy1/3), (float)(x1), (float)(y1) ); } /** Adds a line to (x,y) * <P>This method doesn't actually commit a line until it's sure * that it isn't writing heavily redundant lines. That is the * points (0,0), (5,0) and (2,0) would be consolidated so only * the first and last point remained. * <P>However only horizontal/vertical lines are consolidated, * because this method is aimed at clipping to (non-rotated) rectangles. */ public void lineTo(float x,float y) { if(uncommittedPoints.size()>0) { float[] last = uncommittedPoints.peek(); //are we adding the same point? if(Math.abs(last[0]-x)<TOLERANCE && Math.abs(last[1]-y)<TOLERANCE) return; } float[] f = floatFactory.getArray(2); f[0] = x; f[1] = y; uncommittedPoints.push(f); } public void closePath() { lineTo(initialX,initialY); flush(); g.closePath(); } /** Flush out the stack of uncommitted points. */ public void flush() { while(uncommittedPoints.size()>0) { identifyLines : while(uncommittedPoints.size()>=3) { float[] first = uncommittedPoints.get(0); float[] middle = uncommittedPoints.get(1); float[] last = uncommittedPoints.get(2); if(Math.abs(first[0]-middle[0])<TOLERANCE && Math.abs(first[0]-last[0])<TOLERANCE) { //everything has the same x, so we have a vertical line float[] array = uncommittedPoints.remove(1); floatFactory.putArray(array); } else if(Math.abs(first[1]-middle[1])<TOLERANCE && Math.abs(first[1]-last[1])<TOLERANCE) { //everything has the same y, so we have a horizontal line float[] array = uncommittedPoints.remove(1); floatFactory.putArray(array); } else { break identifyLines; } } float[] point = uncommittedPoints.remove(0); g.lineTo( point[0], point[1]); floatFactory.putArray(point); } } } /** A function used to describe one of the 2 parametric equations * for a segment of a path. This can be thought of is f(t). */ static interface Function { /** evaluates this function at a given value */ public double evaluate(double t); /** Calculates all the t-values which will yield the result "f" * in this function. * * @param f the function result you're searching for * @param dest the array the results will be stored in * @param destOffset the offset at which data will be added to * the array * @return the number of solutions found. */ public int evaluateInverse(double f,double[] dest,int destOffset); /** Return the derivative (df/dt) for a given value of t */ public double getDerivative(double t); } /** A linear function */ static class LFunction implements Function { double slope, intercept; public LFunction() {} /** Defines this linear function. * * @param x1 at t = 0, x1 is the output of this function * @param x2 at t = 1, x2 is the output of this function */ public void define(double x1,double x2) { slope = (x2-x1); intercept = x1; } @Override public String toString() { return slope+"*t+"+intercept; } public double evaluate(double t) { return slope*t+intercept; } public int evaluateInverse(double x,double[] dest,int offset) { dest[offset] = (x-intercept)/slope; return 1; } public double getDerivative(double t) { return slope; } } /** A quadratic function */ static class QFunction implements Function { double a, b, c; public QFunction() {} @Override public String toString() { return a+"*t*t+"+b+"*t+"+c; } /** Use the 3 control points of a bezier quadratic */ public void define(double x0, double x1, double x2) { a = x0-2*x1+x2; b = -2*x0+2*x1; c = x0; } public double evaluate(double t) { return a*t*t+b*t+c; } public double getDerivative(double t) { return 2*a*t+b; } public int evaluateInverse(double x,double[] dest,int offset) { double C = c-x; double det = b*b-4*a*C; if(det<0) return 0; if(det==0) { dest[offset] = (-b)/(2*a); return 1; } det = Math.sqrt(det); dest[offset++] = (-b+det)/(2*a); dest[offset++] = (-b-det)/(2*a); return 2; } } /** A cubic function */ static class CFunction implements Function { double a, b, c, d; public CFunction() {} @Override public String toString() { return a+"*t*t*t+"+b+"*t*t+"+c+"*t+"+d; } public void define(double x0,double x1, double x2,double x3) { a = -x0+3*x1-3*x2+x3; b = 3*x0-6*x1+3*x2; c = -3*x0+3*x1; d = x0; } public double evaluate(double t) { return a*t*t*t+b*t*t+c*t+d; } public double getDerivative(double t) { return 3*a*t*t+2*b*t+c; } /** Recycle arrays here. * Remember this is possibly going to be 1 * object called hundreds of times, so reusing * the same arrays here will save us time & * memory allocation. In current setup there * is only 1 thread that will be using these * values. */ double[] t2; double[] eqn; public int evaluateInverse(double x,double[] dest,int offset) { if(eqn==null) eqn = new double[4]; eqn[0] = d-x; eqn[1] = c; eqn[2] = b; eqn[3] = a; if(offset==0) { int k = CubicCurve2D.solveCubic(eqn,dest); if(k<0) return 0; return k; } if(t2==null) t2 = new double[3]; int k = CubicCurve2D.solveCubic(eqn,t2); if(k<0) return 0; for(int i = 0; i<k; i++) { dest[offset+i] = t2[i]; } return k; } } /** This creates a <code>GeneralPath</code> representing <code>s</code> when * clipped to <code>r</code> * @param s a shape that you want clipped * @param r the rectangle to clip to * @return a <code>GeneralPath</code> enclosing the new shape. */ public static GeneralPath clipToRect(Shape s,Rectangle2D r) { return clipToRect(s,null,r); } /** This creates a <code>GeneralPath</code> representing <code>s</code> when * clipped to <code>r</code> * @param s a shape that you want clipped * @param t the transform to transform <code>s</code> by. * <P>This may be <code>null</code>, indicating that <code>s</code> should * not be transformed. * @param r the rectangle to clip to * @return a <code>GeneralPath</code> enclosing the new shape. */ public static GeneralPath clipToRect(Shape s,AffineTransform t,Rectangle2D r) { Clipper clipper = new RectangleClipper(r); return clipper.clip(s, t); } private static class RectangleClipper extends Clipper { final float rTop; final float rLeft; final float rRight; final float rBottom; private RectangleClipper(Rectangle2D rect) { rTop = (float)rect.getY(); rLeft = (float)rect.getX(); rRight = (float)(rect.getX()+rect.getWidth()); rBottom = (float)(rect.getY()+rect.getHeight()); } @Override boolean contains(float x,float y) { return (x>=rLeft && x<=rRight && y>=rTop && y<=rBottom); } @Override void cap(Point2D.Float p) { if(p.x<rLeft) p.x = rLeft; if(p.x>rRight) p.x = rRight; if(p.y<rTop) p.y = rTop; if(p.y>rBottom) p.y = rBottom; } @Override int collectIntersectionTimes(Function xf, Function yf, double[] intersectionTimes) { int sum = 0; sum += xf.evaluateInverse(rLeft,intersectionTimes,sum); sum += xf.evaluateInverse(rRight,intersectionTimes,sum); sum += yf.evaluateInverse(rTop,intersectionTimes,sum); sum += yf.evaluateInverse(rBottom,intersectionTimes,sum); return sum; } } abstract void cap(Point2D.Float p); abstract boolean contains(float x,float y); /** Calculates the t-values for which this shape intersects the parametric function * provided. The values in the array are not expected to be sorted. * * @param xf the x parametric curve * @param yf the y parametric curve * @param intersectionTimes the array to store the data in. * @return the number of values provided. */ abstract int collectIntersectionTimes(Function xf,Function yf,double[] intersectionTimes); GeneralPath clip(Shape incomingShape,AffineTransform transform) { PathIterator i = incomingShape.getPathIterator(transform); ClippedPath p = new ClippedPath(i.getWindingRule()); float initialX = 0; float initialY = 0; int k; float[] f = floatFactory.getArray(6); boolean shouldClose = false; float lastX = 0; float lastY = 0; boolean lastValueWasCapped, thisValueIsCapped, midValueInvalid; float x, y, x2, y2; //create 1 copy of objects and recycle them //to reduce memory allocation: LFunction lxf = new LFunction(); LFunction lyf = new LFunction(); QFunction qxf = new QFunction(); QFunction qyf = new QFunction(); CFunction cxf = new CFunction(); CFunction cyf = new CFunction(); Function xf = null; Function yf = null; Point2D.Float point = new Point2D.Float(); double[] intersectionTimes = new double[16]; int tCtr; while(i.isDone()==false) { k = i.currentSegment(f); if(k==PathIterator.SEG_MOVETO) { initialX = f[0]; initialY = f[1]; point.setLocation(f[0], f[1]); cap(point); p.moveTo(point.x, point.y); lastX = f[0]; lastY = f[1]; } else if(k==PathIterator.SEG_CLOSE) { f[0] = initialX; f[1] = initialY; k = PathIterator.SEG_LINETO; shouldClose = true; } xf = null; if(k==PathIterator.SEG_LINETO) { lxf.define(lastX,f[0]); lyf.define(lastY,f[1]); xf = lxf; yf = lyf; } else if(k==PathIterator.SEG_QUADTO) { qxf.define(lastX,f[0],f[2]); qyf.define(lastY,f[1],f[3]); xf = qxf; yf = qyf; } else if(k==PathIterator.SEG_CUBICTO) { cxf.define(lastX,f[0],f[2],f[4]); cyf.define(lastY,f[1],f[3],f[5]); xf = cxf; yf = cyf; } if(xf!=null) { //gather all the t values at which we might be //crossing the bounds of our rectangle: tCtr = collectIntersectionTimes(xf, yf, intersectionTimes); intersectionTimes[tCtr++] = 1; //we never actually calculate with 0, but we need to know it's in the list intersectionTimes[tCtr++] = 0; //put them in ascending order: Arrays.sort(intersectionTimes,0,tCtr); lastValueWasCapped = !contains(lastX, lastY); for(int a = 0; a<tCtr; a++) { if(a>0 && intersectionTimes[a]==intersectionTimes[a-1]) { //do nothing } else if(intersectionTimes[a]>0 && intersectionTimes[a]<=1) { //this is the magic: take 2 t values and see what we need to //do with them. //Remember we can make redundant horizontal/vertical lines //all we want to because the ClippedPath will clean up //the mess. x = (float)xf.evaluate(intersectionTimes[a]); y = (float)yf.evaluate(intersectionTimes[a]); point.setLocation(x, y); cap(point); thisValueIsCapped = !(Math.abs(x-point.x)<TOLERANCE && Math.abs(y-point.y)<TOLERANCE); x2 = (float)xf.evaluate((intersectionTimes[a]+intersectionTimes[a-1])/2); y2 = (float)yf.evaluate((intersectionTimes[a]+intersectionTimes[a-1])/2); midValueInvalid = !contains(x2, y2); if(( xf instanceof LFunction) || thisValueIsCapped || lastValueWasCapped || midValueInvalid ) { p.lineTo(point.x, point.y); } else if((xf instanceof QFunction) || (xf instanceof CFunction)) { p.curveTo(xf,yf,intersectionTimes[a-1],intersectionTimes[a]); } else { throw new RuntimeException("Unexpected condition."); } lastValueWasCapped = thisValueIsCapped; } } lastX = (float)xf.evaluate(1); lastY = (float)yf.evaluate(1); } if(shouldClose) { p.closePath(); shouldClose = false; } i.next(); } p.flush(); floatFactory.putArray(f); return p.g; } /** By default if a Graphics2D is asked to clip to a new shape, * it may resort to Area objects if either the current clipping * and the new clipping are not rectangles. * <P>This method with offer a slight improvement over this model: * if <i>either</i> the old clip or the new clip is a rectangle, * then this uses the <code>Clipper.clipToRect()</code> method. * This avoids the slow-but-accurate Area class. * <P>This should only be used to replace <code>Graphics2D.clip()</code>, * not <code>Graphics2D.setClip()</code>. * @param g the graphics2D to clip to * @param newClip the new clip */ public static void clip(Graphics2D g,Shape newClip) { Shape oldClip = g.getClip(); if(oldClip==null) { g.setClip(newClip); return; } Rectangle2D oldRect = RectangleReader.convert(oldClip); Rectangle2D newRect = RectangleReader.convert(newClip); if(oldRect!=null && newRect!=null) { Rectangle2D intersectedClip = oldRect.createIntersection(newRect); if(intersectedClip.getWidth()<0 || intersectedClip.getHeight()<0) { //a negative width or height indicates there's no real intersection intersectedClip.setFrame(intersectedClip.getX(), intersectedClip.getY(), 0, 0); } g.setClip( intersectedClip ); return; } if(newRect!=null && oldRect==null) { GeneralPath intersectedClip = Clipper.clipToRect(oldClip, newRect); g.setClip( intersectedClip); return; } if(newRect==null && oldRect!=null) { GeneralPath intersectedClip = Clipper.clipToRect(newClip, oldRect); g.setClip( intersectedClip ); return; } g.clip(newClip); } }