/* * @(#)Clipper.java * * $Date: 2010-03-19 18:53:03 -0500 (Fri, 19 Mar 2010) $ * * Copyright (c) 2009 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: * BSD License * * This software is probably, but not necessarily, discussed here: * http://javagraphics.blogspot.com/ * * And the latest version should be available here: * https://javagraphics.dev.java.net/ */ package org.geogebra.common.euclidian.clipping; import java.util.Arrays; import java.util.Stack; import org.geogebra.common.awt.GAffineTransform; import org.geogebra.common.awt.GGeneralPath; import org.geogebra.common.awt.GPathIterator; import org.geogebra.common.awt.GRectangle2D; import org.geogebra.common.awt.GShape; import org.geogebra.common.factories.AwtFactory; import org.geogebra.common.kernel.EquationSolver; /** * This class lets you clip/intersect an arbitrary shape to a Rectangle2D. * */ public class ClipShape { // private static final DoubleArrayFactoryImpl doubleFactory = new // DoubleArrayFactoryImpl(); /** Factory for double arrays */ static final DoubleArrayFactory doubleFactory = new DoubleArrayFactory(); /** * 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 double TOLERANCE = 1E-10; /** * 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 arbitary [t0,t1]. */ static class ClippedPath { /** path */ final GGeneralPath g; private Stack<double[]> uncommittedPoints = new Stack<double[]>(); private double initialX, initialY; /** * @param windingRule * winding rule */ public ClippedPath(int windingRule) { g = AwtFactory.getPrototype().newGeneralPath(windingRule); } /** * @param x * x coordinate * @param y * y coordinate */ public void moveTo(double x, double 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((x0 + dx0 / 3), (y0 + dy0 / 3), (x1 - dx1 / 3), (y1 - dy1 / 3), (x1), (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. * * @param x * x coordinate * @param y * y coordinate * */ public void lineTo(double x, double y) { if (uncommittedPoints.size() > 0) { double[] last = uncommittedPoints.peek(); // are we adding the same point? if (Math.abs(last[0] - x) < TOLERANCE && Math.abs(last[1] - y) < TOLERANCE) { return; } } double[] f = doubleFactory.getArray(2); f[0] = x; f[1] = y; uncommittedPoints.push(f); } /** * Close the path */ 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) { double[] first = uncommittedPoints.get(0); double[] middle = uncommittedPoints.get(1); double[] 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 double[] array = uncommittedPoints.remove(1); doubleFactory.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 double[] array = uncommittedPoints.remove(1); doubleFactory.putArray(array); } else { break identifyLines; } } double[] point = uncommittedPoints.remove(0); g.lineTo(point[0], point[1]); doubleFactory.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 * * @param t * parameter * @return function 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 * * @param t * parameter value * @return derivative */ public double getDerivative(double t); } /** A linear function */ static class LFunction implements Function { private double slope, intercept; /** * 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; } @Override public double evaluate(double t) { return slope * t + intercept; } @Override public int evaluateInverse(double x, double[] dest, int offset) { dest[offset] = (x - intercept) / slope; return 1; } @Override public double getDerivative(double t) { return slope; } } /** A quadratic function */ static class QFunction implements Function { private double a, b, c; @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; } @Override public double evaluate(double t) { return a * t * t + b * t + c; } @Override public double getDerivative(double t) { return 2 * a * t + b; } @Override 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 + 1] = (-b - det) / (2 * a); return 2; } } /** A cubic function */ static class CFunction implements Function { private double a, b, c, d; @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; } @Override public double evaluate(double t) { return a * t * t * t + b * t * t + c * t + d; } @Override 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. */ private double[] t2; private double[] eqn; @Override 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 = java.awt.geom.CubicCurve2D.solveCubic(eqn,dest); int k = EquationSolver.solveCubicS(eqn, dest, 1E-8); if (k < 0) { return 0; } return k; } if (t2 == null) { t2 = new double[3]; } // int k = java.awt.geom.CubicCurve2D.solveCubic(eqn,t2); int k = EquationSolver.solveCubicS(eqn, t2, 1E-8); if (k < 0) { return 0; } for (int i = 0; i < k; i++) { dest[offset + i] = t2[i]; } return k; } } /** * @param result * @param s * @param t * @param r * @return */ // public static geogebra.common.awt.Shape // clipToRect(geogebra.common.awt.Shape result,Shape s,AffineTransform // t,Rectangle2D r) { // if(result==null) // return new geogebra.awt.GeneralPath(clipToRect(s,t,r)); // ((geogebra.awt.GenericShape)result).setImpl(clipToRect(s,t,r)); // return result; // } // // public static geogebra.common.awt.GeneralPath clipToRect( // geogebra.common.awt.Shape s, geogebra.common.awt.AffineTransform t, // geogebra.common.awt.Rectangle2D r){ // return new geogebra.awt.GeneralPath(clipToRect( // geogebra.awt.GenericShape.getAwtShape(s), // geogebra.awt.AffineTransform.getAwtAffineTransform(t), // geogebra.awt.GenericRectangle2D.getAWTRectangle2D(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 GGeneralPath clipToRect(GShape s, GAffineTransform t, GRectangle2D r) { GPathIterator i = s.getPathIterator(t); ClippedPath p = new ClippedPath(i.getWindingRule()); double initialX = 0; double initialY = 0; int k; double[] f = doubleFactory.getArray(6); double rTop = r.getY(); double rLeft = r.getX(); double rRight = (r.getX() + r.getWidth()); double rBottom = (r.getY() + r.getHeight()); boolean shouldClose = false; double lastX = 0; double lastY = 0; boolean lastValueWasCapped, thisValueIsCapped, midValueInvalid; double cappedX, cappedY, x, y, x2, y2; // create 1 copy of all our possible functions, // and recycle these objects constantly // this way we avoid 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; double[] interestingTimes = new double[16]; int tCtr; while (!i.isDone()) { k = i.currentSegment(f); if (k == GPathIterator.SEG_MOVETO) { initialX = f[0]; initialY = f[1]; cappedX = f[0]; cappedY = f[1]; if (cappedX < rLeft) { cappedX = rLeft; } if (cappedX > rRight) { cappedX = rRight; } if (cappedY < rTop) { cappedY = rTop; } if (cappedY > rBottom) { cappedY = rBottom; } p.moveTo(cappedX, cappedY); lastX = f[0]; lastY = f[1]; } else if (k == GPathIterator.SEG_CLOSE) { f[0] = initialX; f[1] = initialY; k = GPathIterator.SEG_LINETO; shouldClose = true; } xf = null; if (k == GPathIterator.SEG_LINETO) { lxf.define(lastX, f[0]); lyf.define(lastY, f[1]); xf = lxf; yf = lyf; } else if (k == GPathIterator.SEG_QUADTO) { qxf.define(lastX, f[0], f[2]); qyf.define(lastY, f[1], f[3]); xf = qxf; yf = qyf; } else if (k == GPathIterator.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 = 0; tCtr += xf.evaluateInverse(rLeft, interestingTimes, tCtr); tCtr += xf.evaluateInverse(rRight, interestingTimes, tCtr); tCtr += yf.evaluateInverse(rTop, interestingTimes, tCtr); tCtr += yf.evaluateInverse(rBottom, interestingTimes, tCtr); interestingTimes[tCtr++] = 1; // we never actually calculate with 0, but we need to know it's // in the list interestingTimes[tCtr++] = 0; // put them in ascending order: Arrays.sort(interestingTimes, 0, tCtr); lastValueWasCapped = !(lastX >= rLeft && lastX <= rRight && lastY >= rTop && lastY <= rBottom); for (int a = 0; a < tCtr; a++) { if (a > 0 && interestingTimes[a] == interestingTimes[a - 1]) { // do nothing } else if (interestingTimes[a] > 0 && interestingTimes[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 = xf.evaluate(interestingTimes[a]); y = yf.evaluate(interestingTimes[a]); cappedX = x; cappedY = y; if (cappedX < rLeft) { cappedX = rLeft; } else if (cappedX > rRight) { cappedX = rRight; } if (cappedY < rTop) { cappedY = rTop; } else if (cappedY > rBottom) { cappedY = rBottom; } thisValueIsCapped = !(Math.abs(x - cappedX) < TOLERANCE && Math.abs(y - cappedY) < TOLERANCE); x2 = xf.evaluate( (interestingTimes[a] + interestingTimes[a - 1]) / 2); y2 = yf.evaluate( (interestingTimes[a] + interestingTimes[a - 1]) / 2); midValueInvalid = !(rLeft <= x2 && x2 <= rRight && rTop <= y2 && y2 <= rBottom); if ((xf instanceof LFunction) || thisValueIsCapped || lastValueWasCapped || midValueInvalid) { p.lineTo(cappedX, cappedY); } else if ((xf instanceof QFunction) || (xf instanceof CFunction)) { p.curveTo(xf, yf, interestingTimes[a - 1], interestingTimes[a]); } else { throw new RuntimeException("Unexpected condition."); } lastValueWasCapped = thisValueIsCapped; } } lastX = xf.evaluate(1); lastY = yf.evaluate(1); } if (shouldClose) { p.closePath(); shouldClose = false; } i.next(); } p.flush(); doubleFactory.putArray(f); return p.g; } }