From 4b9be83ad2f4936ed7ea8370ce9e8aae11788545 Mon Sep 17 00:00:00 2001 From: codeanticode Date: Tue, 15 May 2012 21:40:04 +0000 Subject: [PATCH] Checking-in the new processing.opengl.geom including the linear path stroker. --- .../src/processing/opengl/geom/LinePath.java | 561 ++++++++++++++ .../src/processing/opengl/geom/LineSink.java | 85 +++ .../processing/opengl/geom/LineStroker.java | 714 ++++++++++++++++++ .../src/processing/opengl/geom/LinePath.java | 561 ++++++++++++++ .../src/processing/opengl/geom/LineSink.java | 85 +++ .../processing/opengl/geom/LineStroker.java | 714 ++++++++++++++++++ 6 files changed, 2720 insertions(+) create mode 100644 android/core/src/processing/opengl/geom/LinePath.java create mode 100644 android/core/src/processing/opengl/geom/LineSink.java create mode 100644 android/core/src/processing/opengl/geom/LineStroker.java create mode 100644 java/libraries/opengl/src/processing/opengl/geom/LinePath.java create mode 100644 java/libraries/opengl/src/processing/opengl/geom/LineSink.java create mode 100644 java/libraries/opengl/src/processing/opengl/geom/LineStroker.java diff --git a/android/core/src/processing/opengl/geom/LinePath.java b/android/core/src/processing/opengl/geom/LinePath.java new file mode 100644 index 000000000..b58b48bf1 --- /dev/null +++ b/android/core/src/processing/opengl/geom/LinePath.java @@ -0,0 +1,561 @@ +/* + * Copyright 2006 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ + +package processing.opengl.geom; + +import processing.core.PMatrix2D; + +/** + * The {@code LinePath} class allows to represent polygonal paths, + * potentially composed by several disjoint polygonal segments. + * It can be iterated by the {@link PathIterator} class including all + * of its segment types and winding rules + * + */ +public class LinePath { + /** + * The winding rule constant for specifying an even-odd rule + * for determining the interior of a path. + * The even-odd rule specifies that a point lies inside the + * path if a ray drawn in any direction from that point to + * infinity is crossed by path segments an odd number of times. + */ + public static final int WIND_EVEN_ODD = 0; + + /** + * The winding rule constant for specifying a non-zero rule + * for determining the interior of a path. + * The non-zero rule specifies that a point lies inside the + * path if a ray drawn in any direction from that point to + * infinity is crossed by path segments a different number + * of times in the counter-clockwise direction than the + * clockwise direction. + */ + public static final int WIND_NON_ZERO = 1; + + /** + * Starts segment at a given position. + */ + public static final byte SEG_MOVETO = 0; + + /** + * Extends segment by adding a line to a given position. + */ + public static final byte SEG_LINETO = 1; + + /** + * Closes segment at current position. + */ + public static final byte SEG_CLOSE = 2; + + /** + * Joins path segments by extending their outside edges until they meet. + */ + public final static int JOIN_MITER = 0; + + /** + * Joins path segments by rounding off the corner at a radius of half the line + * width. + */ + public final static int JOIN_ROUND = 1; + + /** + * Joins path segments by connecting the outer corners of their wide outlines + * with a straight segment. + */ + public final static int JOIN_BEVEL = 2; + + /** + * Ends unclosed subpaths and dash segments with no added decoration. + */ + public final static int CAP_BUTT = 0; + + /** + * Ends unclosed subpaths and dash segments with a round decoration that has a + * radius equal to half of the width of the pen. + */ + public final static int CAP_ROUND = 1; + + /** + * Ends unclosed subpaths and dash segments with a square projection that + * extends beyond the end of the segment to a distance equal to half of the + * line width. + */ + public final static int CAP_SQUARE = 2; + + private static PMatrix2D identity = new PMatrix2D(); + + private static float defaultMiterlimit = 10.0f; + + static final int INIT_SIZE = 20; + + static final int EXPAND_MAX = 500; + + protected byte[] pointTypes; + + protected float[] floatCoords; + + protected int numTypes; + + protected int numCoords; + + protected int windingRule; + + + /** + * Constructs a new empty single precision {@code LinePath} object with a + * default winding rule of {@link #WIND_NON_ZERO}. + */ + public LinePath() { + this(WIND_NON_ZERO, INIT_SIZE); + } + + + /** + * Constructs a new empty single precision {@code LinePath} object with the + * specified winding rule to control operations that require the interior of + * the path to be defined. + * + * @param rule + * the winding rule + * @see #WIND_EVEN_ODD + * @see #WIND_NON_ZERO + */ + public LinePath(int rule) { + this(rule, INIT_SIZE); + } + + + /** + * Constructs a new {@code LinePath} object from the given specified initial + * values. This method is only intended for internal use and should not be + * made public if the other constructors for this class are ever exposed. + * + * @param rule + * the winding rule + * @param initialTypes + * the size to make the initial array to store the path segment types + */ + public LinePath(int rule, int initialCapacity) { + setWindingRule(rule); + this.pointTypes = new byte[initialCapacity]; + floatCoords = new float[initialCapacity * 2]; + } + + + void needRoom(boolean needMove, int newCoords) { + if (needMove && numTypes == 0) { + throw new RuntimeException("missing initial moveto " + + "in path definition"); + } + int size = pointTypes.length; + if (numTypes >= size) { + int grow = size; + if (grow > EXPAND_MAX) { + grow = EXPAND_MAX; + } + pointTypes = copyOf(pointTypes, size + grow); + } + size = floatCoords.length; + if (numCoords + newCoords > size) { + int grow = size; + if (grow > EXPAND_MAX * 2) { + grow = EXPAND_MAX * 2; + } + if (grow < newCoords) { + grow = newCoords; + } + floatCoords = copyOf(floatCoords, size + grow); + } + } + + + /** + * Adds a point to the path by moving to the specified coordinates specified + * in float precision. + *

+ * This method provides a single precision variant of the double precision + * {@code moveTo()} method on the base {@code LinePath} class. + * + * @param x + * the specified X coordinate + * @param y + * the specified Y coordinate + * @see LinePath#moveTo + */ + public final void moveTo(float x, float y) { + if (numTypes > 0 && pointTypes[numTypes - 1] == SEG_MOVETO) { + floatCoords[numCoords - 2] = x; + floatCoords[numCoords - 1] = y; + } else { + needRoom(false, 2); + pointTypes[numTypes++] = SEG_MOVETO; + floatCoords[numCoords++] = x; + floatCoords[numCoords++] = y; + } + } + + + /** + * Adds a point to the path by drawing a straight line from the current + * coordinates to the new specified coordinates specified in float precision. + *

+ * This method provides a single precision variant of the double precision + * {@code lineTo()} method on the base {@code LinePath} class. + * + * @param x + * the specified X coordinate + * @param y + * the specified Y coordinate + * @see LinePath#lineTo + */ + public final void lineTo(float x, float y) { + needRoom(true, 2); + pointTypes[numTypes++] = SEG_LINETO; + floatCoords[numCoords++] = x; + floatCoords[numCoords++] = y; + } + + + /** + * The iterator for this class is not multi-threaded safe, which means that + * the {@code LinePath} class does not guarantee that modifications to the + * geometry of this {@code LinePath} object do not affect any iterations of that + * geometry that are already in process. + */ + public PathIterator getPathIterator() { + return new PathIterator(this); + } + + + /** + * Closes the current subpath by drawing a straight line back to the + * coordinates of the last {@code moveTo}. If the path is already closed then + * this method has no effect. + */ + public final void closePath() { + if (numTypes == 0 || pointTypes[numTypes - 1] != SEG_CLOSE) { + needRoom(false, 0); + pointTypes[numTypes++] = SEG_CLOSE; + } + } + + + /** + * Returns the fill style winding rule. + * + * @return an integer representing the current winding rule. + * @see #WIND_EVEN_ODD + * @see #WIND_NON_ZERO + * @see #setWindingRule + */ + public final int getWindingRule() { + return windingRule; + } + + + /** + * Sets the winding rule for this path to the specified value. + * + * @param rule + * an integer representing the specified winding rule + * @exception IllegalArgumentException + * if {@code rule} is not either {@link #WIND_EVEN_ODD} or + * {@link #WIND_NON_ZERO} + * @see #getWindingRule + */ + public final void setWindingRule(int rule) { + if (rule != WIND_EVEN_ODD && rule != WIND_NON_ZERO) { + throw new IllegalArgumentException("winding rule must be " + + "WIND_EVEN_ODD or " + "WIND_NON_ZERO"); + } + windingRule = rule; + } + + + /** + * Resets the path to empty. The append position is set back to the beginning + * of the path and all coordinates and point types are forgotten. + */ + public final void reset() { + numTypes = numCoords = 0; + } + + + static public class PathIterator { + float floatCoords[]; + + int typeIdx; + + int pointIdx; + + LinePath path; + + static final int curvecoords[] = { 2, 2, 0 }; + + PathIterator(LinePath p2df) { + this.path = p2df; + this.floatCoords = p2df.floatCoords; + } + + public int currentSegment(float[] coords) { + int type = path.pointTypes[typeIdx]; + int numCoords = curvecoords[type]; + if (numCoords > 0) { + System.arraycopy(floatCoords, pointIdx, coords, 0, numCoords); + } + return type; + } + + public int currentSegment(double[] coords) { + int type = path.pointTypes[typeIdx]; + int numCoords = curvecoords[type]; + if (numCoords > 0) { + for (int i = 0; i < numCoords; i++) { + coords[i] = floatCoords[pointIdx + i]; + } + } + return type; + } + + public int getWindingRule() { + return path.getWindingRule(); + } + + public boolean isDone() { + return (typeIdx >= path.numTypes); + } + + public void next() { + int type = path.pointTypes[typeIdx++]; + pointIdx += curvecoords[type]; + } + } + + + ///////////////////////////////////////////////////////////////////////////// + // + // Stroked path methods + + + static public LinePath createStrokedPath(LinePath src, float weight, + int caps, int join) { + return createStrokedPath(src, weight, caps, join, defaultMiterlimit, null); + } + + + static public LinePath createStrokedPath(LinePath src, float weight, + int caps, int join, float miterlimit) { + return createStrokedPath(src, weight, caps, join, miterlimit, null); + } + + + /** + * Constructs a solid LinePath with the specified attributes. + * + * @param src + * the original path to be stroked + * @param weight + * the weight of the stroked path + * @param cap + * the decoration of the ends of the segments in the path + * @param join + * the decoration applied where path segments meet + * @param miterlimit + * @param transform + * + */ + static public LinePath createStrokedPath(LinePath src, float weight, + int caps, int join, + float miterlimit, PMatrix2D transform) { + final LinePath dest = new LinePath(); + + strokeTo(src, weight, caps, join, miterlimit, transform, new LineSink() { + public void moveTo(int x0, int y0) { + dest.moveTo(S15_16ToFloat(x0), S15_16ToFloat(y0)); + } + + public void lineJoin() { + } + + public void lineTo(int x1, int y1) { + dest.lineTo(S15_16ToFloat(x1), S15_16ToFloat(y1)); + } + + public void close() { + dest.closePath(); + } + + public void end() { + } + }); + + return dest; + } + + + private static void strokeTo(LinePath src, float width, int caps, int join, + float miterlimit, PMatrix2D transform, + LineSink lsink) { + lsink = new LineStroker(lsink, FloatToS15_16(width), caps, join, + FloatToS15_16(miterlimit), + transform == null ? identity : transform); + + PathIterator pi = src.getPathIterator(); + pathTo(pi, lsink); + } + + + private static void pathTo(PathIterator pi, LineSink lsink) { + float coords[] = new float[2]; + while (!pi.isDone()) { + switch (pi.currentSegment(coords)) { + case SEG_MOVETO: + lsink.moveTo(FloatToS15_16(coords[0]), FloatToS15_16(coords[1])); + break; + case SEG_LINETO: + lsink.lineJoin(); + lsink.lineTo(FloatToS15_16(coords[0]), FloatToS15_16(coords[1])); + break; + case SEG_CLOSE: + lsink.lineJoin(); + lsink.close(); + break; + default: + throw new InternalError("unknown flattened segment type"); + } + pi.next(); + } + lsink.end(); + } + + + ///////////////////////////////////////////////////////////////////////////// + // + // Utility methods + + + public static float[] copyOf(float[] source, int length) { + float[] target = new float[length]; + for (int i = 0; i < target.length; i++) { + if (i > source.length - 1) + target[i] = 0f; + else + target[i] = source[i]; + } + return target; + } + + + public static byte[] copyOf(byte[] source, int length) { + byte[] target = new byte[length]; + for (int i = 0; i < target.length; i++) { + if (i > source.length - 1) + target[i] = 0; + else + target[i] = source[i]; + } + return target; + } + + + // From Ken Turkowski, _Fixed-Point Square Root_, In Graphics Gems V + public static int isqrt(int x) { + int fracbits = 16; + + int root = 0; + int remHi = 0; + int remLo = x; + int count = 15 + fracbits / 2; + + do { + remHi = (remHi << 2) | (remLo >>> 30); // N.B. - unsigned shift R + remLo <<= 2; + root <<= 1; + int testdiv = (root << 1) + 1; + if (remHi >= testdiv) { + remHi -= testdiv; + root++; + } + } while (count-- != 0); + + return root; + } + + + public static long lsqrt(long x) { + int fracbits = 16; + + long root = 0; + long remHi = 0; + long remLo = x; + int count = 31 + fracbits / 2; + + do { + remHi = (remHi << 2) | (remLo >>> 62); // N.B. - unsigned shift R + remLo <<= 2; + root <<= 1; + long testDiv = (root << 1) + 1; + if (remHi >= testDiv) { + remHi -= testDiv; + root++; + } + } while (count-- != 0); + + return root; + } + + + public static double hypot(double x, double y) { + return Math.sqrt(x * x + y * y); + } + + + public static int hypot(int x, int y) { + return (int) ((lsqrt((long) x * x + (long) y * y) + 128) >> 8); + } + + + public static long hypot(long x, long y) { + return (lsqrt(x * x + y * y) + 128) >> 8; + } + + + static int FloatToS15_16(float flt) { + flt = flt * 65536f + 0.5f; + if (flt <= -(65536f * 65536f)) { + return Integer.MIN_VALUE; + } else if (flt >= (65536f * 65536f)) { + return Integer.MAX_VALUE; + } else { + return (int) Math.floor(flt); + } + } + + + static float S15_16ToFloat(int fix) { + return (fix / 65536f); + } +} diff --git a/android/core/src/processing/opengl/geom/LineSink.java b/android/core/src/processing/opengl/geom/LineSink.java new file mode 100644 index 000000000..2e57143cd --- /dev/null +++ b/android/core/src/processing/opengl/geom/LineSink.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2007, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package processing.opengl.geom; + +/** + * The LineSink interface accepts a series of line + * drawing commands: moveTo, lineTo, + * close (equivalent to a lineTo command + * with an argument equal to the argument of the last + * moveTo command), and end. + * + */ +public abstract class LineSink { + /** + * Moves the current drawing position to the point (x0, + * y0). + * + * @param x0 the X coordinate in S15.16 format + * @param y0 the Y coordinate in S15.16 format + */ + public abstract void moveTo(int x0, int y0); + + /** + * Provides a hint that the current segment should be joined to + * the following segment using an explicit miter or round join if + * required. + * + *

An application-generated path will generally have no need + * to contain calls to this method; they are typically introduced + * by a Flattener to mark segment divisions that + * appear in its input, and consumed by a Stroker + * that is responsible for emitting the miter or round join + * segments. + * + *

Other LineSink classes should simply pass this + * hint to their output sink as needed. + */ + public abstract void lineJoin(); + + /** + * Draws a line from the current drawing position to the point + * (x1, y1) and sets the current drawing position to + * (x1, y1). + * + * @param x1 the X coordinate in S15.16 format + * @param y1 the Y coordinate in S15.16 format + */ + public abstract void lineTo(int x1, int y1); + + /** + * Closes the current path by drawing a line from the current + * drawing position to the point specified by the moset recent + * moveTo command. + */ + public abstract void close(); + + /** + * Ends the current path. It may be necessary to end a path in + * order to allow end caps to be drawn. + */ + public abstract void end(); +} diff --git a/android/core/src/processing/opengl/geom/LineStroker.java b/android/core/src/processing/opengl/geom/LineStroker.java new file mode 100644 index 000000000..85d035e2b --- /dev/null +++ b/android/core/src/processing/opengl/geom/LineStroker.java @@ -0,0 +1,714 @@ +/* + * Copyright (c) 2007, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package processing.opengl.geom; + +import processing.core.PMatrix2D; + +public class LineStroker extends LineSink { + LineSink output; + + int lineWidth; + + int capStyle; + + int joinStyle; + + int miterLimit; + + int m00, m01; + + int m10, m11; + + int lineWidth2; + + long scaledLineWidth2; + + // For any pen offset (pen_dx, pen_dy) that does not depend on + // the line orientation, the pen should be transformed so that: + // + // pen_dx' = m00*pen_dx + m01*pen_dy + // pen_dy' = m10*pen_dx + m11*pen_dy + // + // For a round pen, this means: + // + // pen_dx(r, theta) = r*cos(theta) + // pen_dy(r, theta) = r*sin(theta) + // + // pen_dx'(r, theta) = r*(m00*cos(theta) + m01*sin(theta)) + // pen_dy'(r, theta) = r*(m10*cos(theta) + m11*sin(theta)) + int numPenSegments; + + int[] pen_dx; + + int[] pen_dy; + + boolean[] penIncluded; + + int[] join; + + int[] offset = new int[2]; + + int[] reverse = new int[100]; + + int[] miter = new int[2]; + + long miterLimitSq; + + int prev; + + int rindex; + + boolean started; + + boolean lineToOrigin; + + boolean joinToOrigin; + + int sx0, sy0, sx1, sy1, x0, y0, x1, y1; + + int mx0, my0, mx1, my1, omx, omy; + + int lx0, ly0, lx1, ly1, lx0p, ly0p, px0, py0; + + double m00_2_m01_2; + + double m10_2_m11_2; + + double m00_m10_m01_m11; + + /** + * Empty constructor. setOutput and setParameters + * must be called prior to calling any other methods. + */ + public LineStroker() { + } + + /** + * Constructs a LineStroker. + * + * @param output + * an output LineSink. + * @param lineWidth + * the desired line width in pixels, in S15.16 format. + * @param capStyle + * the desired end cap style, one of CAP_BUTT, + * CAP_ROUND or CAP_SQUARE. + * @param joinStyle + * the desired line join style, one of JOIN_MITER, + * JOIN_ROUND or JOIN_BEVEL. + * @param miterLimit + * the desired miter limit, in S15.16 format. + * @param transform + * a Transform4 object indicating the transform that has + * been previously applied to all incoming coordinates. This is + * required in order to produce consistently shaped end caps and + * joins. + */ + public LineStroker(LineSink output, int lineWidth, int capStyle, int joinStyle, + int miterLimit, PMatrix2D transform) { + setOutput(output); + setParameters(lineWidth, capStyle, joinStyle, miterLimit, transform); + } + + /** + * Sets the output LineSink of this LineStroker. + * + * @param output + * an output LineSink. + */ + public void setOutput(LineSink output) { + this.output = output; + } + + /** + * Sets the parameters of this LineStroker. + * + * @param lineWidth + * the desired line width in pixels, in S15.16 format. + * @param capStyle + * the desired end cap style, one of CAP_BUTT, + * CAP_ROUND or CAP_SQUARE. + * @param joinStyle + * the desired line join style, one of JOIN_MITER, + * JOIN_ROUND or JOIN_BEVEL. + * @param miterLimit + * the desired miter limit, in S15.16 format. + * @param transform + * a Transform4 object indicating the transform that has + * been previously applied to all incoming coordinates. This is + * required in order to produce consistently shaped end caps and + * joins. + */ + public void setParameters(int lineWidth, int capStyle, int joinStyle, + int miterLimit, PMatrix2D transform) { + this.m00 = LinePath.FloatToS15_16(transform.m00); + this.m01 = LinePath.FloatToS15_16(transform.m01); + this.m10 = LinePath.FloatToS15_16(transform.m10); + this.m11 = LinePath.FloatToS15_16(transform.m11); + + this.lineWidth = lineWidth; + this.lineWidth2 = lineWidth >> 1; + this.scaledLineWidth2 = ((long) m00 * lineWidth2) >> 16; + this.capStyle = capStyle; + this.joinStyle = joinStyle; + this.miterLimit = miterLimit; + + this.m00_2_m01_2 = (double) m00 * m00 + (double) m01 * m01; + this.m10_2_m11_2 = (double) m10 * m10 + (double) m11 * m11; + this.m00_m10_m01_m11 = (double) m00 * m10 + (double) m01 * m11; + + double dm00 = m00 / 65536.0; + double dm01 = m01 / 65536.0; + double dm10 = m10 / 65536.0; + double dm11 = m11 / 65536.0; + double determinant = dm00 * dm11 - dm01 * dm10; + + if (joinStyle == LinePath.JOIN_MITER) { + double limit = (miterLimit / 65536.0) * (lineWidth2 / 65536.0) + * determinant; + double limitSq = limit * limit; + this.miterLimitSq = (long) (limitSq * 65536.0 * 65536.0); + } + + this.numPenSegments = (int) (3.14159f * lineWidth / 65536.0f); + if (pen_dx == null || pen_dx.length < numPenSegments) { + this.pen_dx = new int[numPenSegments]; + this.pen_dy = new int[numPenSegments]; + this.penIncluded = new boolean[numPenSegments]; + this.join = new int[2 * numPenSegments]; + } + + for (int i = 0; i < numPenSegments; i++) { + double r = lineWidth / 2.0; + double theta = (double) i * 2.0 * Math.PI / numPenSegments; + + double cos = Math.cos(theta); + double sin = Math.sin(theta); + pen_dx[i] = (int) (r * (dm00 * cos + dm01 * sin)); + pen_dy[i] = (int) (r * (dm10 * cos + dm11 * sin)); + } + + prev = LinePath.SEG_CLOSE; + rindex = 0; + started = false; + lineToOrigin = false; + } + + private void computeOffset(int x0, int y0, int x1, int y1, int[] m) { + long lx = (long) x1 - (long) x0; + long ly = (long) y1 - (long) y0; + + int dx, dy; + if (m00 > 0 && m00 == m11 && m01 == 0 & m10 == 0) { + long ilen = LinePath.hypot(lx, ly); + if (ilen == 0) { + dx = dy = 0; + } else { + dx = (int) ((ly * scaledLineWidth2) / ilen); + dy = (int) (-(lx * scaledLineWidth2) / ilen); + } + } else { + double dlx = x1 - x0; + double dly = y1 - y0; + double det = (double) m00 * m11 - (double) m01 * m10; + int sdet = (det > 0) ? 1 : -1; + double a = dly * m00 - dlx * m10; + double b = dly * m01 - dlx * m11; + double dh = LinePath.hypot(a, b); + double div = sdet * lineWidth2 / (65536.0 * dh); + double ddx = dly * m00_2_m01_2 - dlx * m00_m10_m01_m11; + double ddy = dly * m00_m10_m01_m11 - dlx * m10_2_m11_2; + dx = (int) (ddx * div); + dy = (int) (ddy * div); + } + + m[0] = dx; + m[1] = dy; + } + + private void ensureCapacity(int newrindex) { + if (reverse.length < newrindex) { + int[] tmp = new int[Math.max(newrindex, 6 * reverse.length / 5)]; + System.arraycopy(reverse, 0, tmp, 0, rindex); + this.reverse = tmp; + } + } + + private boolean isCCW(int x0, int y0, int x1, int y1, int x2, int y2) { + int dx0 = x1 - x0; + int dy0 = y1 - y0; + int dx1 = x2 - x1; + int dy1 = y2 - y1; + return (long) dx0 * dy1 < (long) dy0 * dx1; + } + + private boolean side(int x, int y, int x0, int y0, int x1, int y1) { + long lx = x; + long ly = y; + long lx0 = x0; + long ly0 = y0; + long lx1 = x1; + long ly1 = y1; + + return (ly0 - ly1) * lx + (lx1 - lx0) * ly + (lx0 * ly1 - lx1 * ly0) > 0; + } + + private int computeRoundJoin(int cx, int cy, int xa, int ya, int xb, int yb, + int side, boolean flip, int[] join) { + int px, py; + int ncoords = 0; + + boolean centerSide; + if (side == 0) { + centerSide = side(cx, cy, xa, ya, xb, yb); + } else { + centerSide = (side == 1) ? true : false; + } + for (int i = 0; i < numPenSegments; i++) { + px = cx + pen_dx[i]; + py = cy + pen_dy[i]; + + boolean penSide = side(px, py, xa, ya, xb, yb); + if (penSide != centerSide) { + penIncluded[i] = true; + } else { + penIncluded[i] = false; + } + } + + int start = -1, end = -1; + for (int i = 0; i < numPenSegments; i++) { + if (penIncluded[i] + && !penIncluded[(i + numPenSegments - 1) % numPenSegments]) { + start = i; + } + if (penIncluded[i] && !penIncluded[(i + 1) % numPenSegments]) { + end = i; + } + } + + if (end < start) { + end += numPenSegments; + } + + if (start != -1 && end != -1) { + long dxa = cx + pen_dx[start] - xa; + long dya = cy + pen_dy[start] - ya; + long dxb = cx + pen_dx[start] - xb; + long dyb = cy + pen_dy[start] - yb; + + boolean rev = (dxa * dxa + dya * dya > dxb * dxb + dyb * dyb); + int i = rev ? end : start; + int incr = rev ? -1 : 1; + while (true) { + int idx = i % numPenSegments; + px = cx + pen_dx[idx]; + py = cy + pen_dy[idx]; + join[ncoords++] = px; + join[ncoords++] = py; + if (i == (rev ? start : end)) { + break; + } + i += incr; + } + } + + return ncoords / 2; + } + + private static final long ROUND_JOIN_THRESHOLD = 1000L; + + private static final long ROUND_JOIN_INTERNAL_THRESHOLD = 1000000000L; + + private void drawRoundJoin(int x, int y, int omx, int omy, int mx, int my, + int side, boolean flip, boolean rev, long threshold) { + if ((omx == 0 && omy == 0) || (mx == 0 && my == 0)) { + return; + } + + long domx = (long) omx - mx; + long domy = (long) omy - my; + long len = domx * domx + domy * domy; + if (len < threshold) { + return; + } + + if (rev) { + omx = -omx; + omy = -omy; + mx = -mx; + my = -my; + } + + int bx0 = x + omx; + int by0 = y + omy; + int bx1 = x + mx; + int by1 = y + my; + + int npoints = computeRoundJoin(x, y, bx0, by0, bx1, by1, side, flip, join); + for (int i = 0; i < npoints; i++) { + emitLineTo(join[2 * i], join[2 * i + 1], rev); + } + } + + // Return the intersection point of the lines (ix0, iy0) -> (ix1, iy1) + // and (ix0p, iy0p) -> (ix1p, iy1p) in m[0] and m[1] + private void computeMiter(int ix0, int iy0, int ix1, int iy1, int ix0p, + int iy0p, int ix1p, int iy1p, int[] m) { + long x0 = ix0; + long y0 = iy0; + long x1 = ix1; + long y1 = iy1; + + long x0p = ix0p; + long y0p = iy0p; + long x1p = ix1p; + long y1p = iy1p; + + long x10 = x1 - x0; + long y10 = y1 - y0; + long x10p = x1p - x0p; + long y10p = y1p - y0p; + + long den = (x10 * y10p - x10p * y10) >> 16; + if (den == 0) { + m[0] = ix0; + m[1] = iy0; + return; + } + + long t = (x1p * (y0 - y0p) - x0 * y10p + x0p * (y1p - y0)) >> 16; + m[0] = (int) (x0 + (t * x10) / den); + m[1] = (int) (y0 + (t * y10) / den); + } + + private void drawMiter(int px0, int py0, int x0, int y0, int x1, int y1, + int omx, int omy, int mx, int my, boolean rev) { + if (mx == omx && my == omy) { + return; + } + if (px0 == x0 && py0 == y0) { + return; + } + if (x0 == x1 && y0 == y1) { + return; + } + + if (rev) { + omx = -omx; + omy = -omy; + mx = -mx; + my = -my; + } + + computeMiter(px0 + omx, py0 + omy, x0 + omx, y0 + omy, x0 + mx, y0 + my, x1 + + mx, y1 + my, miter); + + // Compute miter length in untransformed coordinates + long dx = (long) miter[0] - x0; + long dy = (long) miter[1] - y0; + long a = (dy * m00 - dx * m10) >> 16; + long b = (dy * m01 - dx * m11) >> 16; + long lenSq = a * a + b * b; + + if (lenSq < miterLimitSq) { + emitLineTo(miter[0], miter[1], rev); + } + } + + public void moveTo(int x0, int y0) { + // System.out.println("LineStroker.moveTo(" + x0/65536.0 + ", " + y0/65536.0 + ")"); + + if (lineToOrigin) { + // not closing the path, do the previous lineTo + lineToImpl(sx0, sy0, joinToOrigin); + lineToOrigin = false; + } + + if (prev == LinePath.SEG_LINETO) { + finish(); + } + + this.sx0 = this.x0 = x0; + this.sy0 = this.y0 = y0; + this.rindex = 0; + this.started = false; + this.joinSegment = false; + this.prev = LinePath.SEG_MOVETO; + } + + boolean joinSegment = false; + + public void lineJoin() { + // System.out.println("LineStroker.lineJoin()"); + this.joinSegment = true; + } + + public void lineTo(int x1, int y1) { + // System.out.println("LineStroker.lineTo(" + x1/65536.0 + ", " + y1/65536.0 + ")"); + + if (lineToOrigin) { + if (x1 == sx0 && y1 == sy0) { + // staying in the starting point + return; + } + + // not closing the path, do the previous lineTo + lineToImpl(sx0, sy0, joinToOrigin); + lineToOrigin = false; + } else if (x1 == x0 && y1 == y0) { + return; + } else if (x1 == sx0 && y1 == sy0) { + lineToOrigin = true; + joinToOrigin = joinSegment; + joinSegment = false; + return; + } + + lineToImpl(x1, y1, joinSegment); + joinSegment = false; + } + + private void lineToImpl(int x1, int y1, boolean joinSegment) { + computeOffset(x0, y0, x1, y1, offset); + int mx = offset[0]; + int my = offset[1]; + + if (!started) { + emitMoveTo(x0 + mx, y0 + my); + this.sx1 = x1; + this.sy1 = y1; + this.mx0 = mx; + this.my0 = my; + started = true; + } else { + boolean ccw = isCCW(px0, py0, x0, y0, x1, y1); + if (joinSegment) { + if (joinStyle == LinePath.JOIN_MITER) { + drawMiter(px0, py0, x0, y0, x1, y1, omx, omy, mx, my, ccw); + } else if (joinStyle == LinePath.JOIN_ROUND) { + drawRoundJoin(x0, y0, omx, omy, mx, my, 0, false, ccw, + ROUND_JOIN_THRESHOLD); + } + } else { + // Draw internal joins as round + drawRoundJoin(x0, y0, omx, omy, mx, my, 0, false, ccw, + ROUND_JOIN_INTERNAL_THRESHOLD); + } + + emitLineTo(x0, y0, !ccw); + } + + emitLineTo(x0 + mx, y0 + my, false); + emitLineTo(x1 + mx, y1 + my, false); + + emitLineTo(x0 - mx, y0 - my, true); + emitLineTo(x1 - mx, y1 - my, true); + + lx0 = x1 + mx; + ly0 = y1 + my; + lx0p = x1 - mx; + ly0p = y1 - my; + lx1 = x1; + ly1 = y1; + + this.omx = mx; + this.omy = my; + this.px0 = x0; + this.py0 = y0; + this.x0 = x1; + this.y0 = y1; + this.prev = LinePath.SEG_LINETO; + } + + public void close() { + // System.out.println("LineStroker.close()"); + + if (lineToOrigin) { + // ignore the previous lineTo + lineToOrigin = false; + } + + if (!started) { + finish(); + return; + } + + computeOffset(x0, y0, sx0, sy0, offset); + int mx = offset[0]; + int my = offset[1]; + + // Draw penultimate join + boolean ccw = isCCW(px0, py0, x0, y0, sx0, sy0); + if (joinSegment) { + if (joinStyle == LinePath.JOIN_MITER) { + drawMiter(px0, py0, x0, y0, sx0, sy0, omx, omy, mx, my, ccw); + } else if (joinStyle == LinePath.JOIN_ROUND) { + drawRoundJoin(x0, y0, omx, omy, mx, my, 0, false, ccw, + ROUND_JOIN_THRESHOLD); + } + } else { + // Draw internal joins as round + drawRoundJoin(x0, y0, omx, omy, mx, my, 0, false, ccw, + ROUND_JOIN_INTERNAL_THRESHOLD); + } + + emitLineTo(x0 + mx, y0 + my); + emitLineTo(sx0 + mx, sy0 + my); + + ccw = isCCW(x0, y0, sx0, sy0, sx1, sy1); + + // Draw final join on the outside + if (!ccw) { + if (joinStyle == LinePath.JOIN_MITER) { + drawMiter(x0, y0, sx0, sy0, sx1, sy1, mx, my, mx0, my0, false); + } else if (joinStyle == LinePath.JOIN_ROUND) { + drawRoundJoin(sx0, sy0, mx, my, mx0, my0, 0, false, false, + ROUND_JOIN_THRESHOLD); + } + } + + emitLineTo(sx0 + mx0, sy0 + my0); + emitLineTo(sx0 - mx0, sy0 - my0); // same as reverse[0], reverse[1] + + // Draw final join on the inside + if (ccw) { + if (joinStyle == LinePath.JOIN_MITER) { + drawMiter(x0, y0, sx0, sy0, sx1, sy1, -mx, -my, -mx0, -my0, false); + } else if (joinStyle == LinePath.JOIN_ROUND) { + drawRoundJoin(sx0, sy0, -mx, -my, -mx0, -my0, 0, true, false, + ROUND_JOIN_THRESHOLD); + } + } + + emitLineTo(sx0 - mx, sy0 - my); + emitLineTo(x0 - mx, y0 - my); + for (int i = rindex - 2; i >= 0; i -= 2) { + emitLineTo(reverse[i], reverse[i + 1]); + } + + this.x0 = this.sx0; + this.y0 = this.sy0; + this.rindex = 0; + this.started = false; + this.joinSegment = false; + this.prev = LinePath.SEG_CLOSE; + emitClose(); + } + + public void end() { + // System.out.println("LineStroker.end()"); + + if (lineToOrigin) { + // not closing the path, do the previous lineTo + lineToImpl(sx0, sy0, joinToOrigin); + lineToOrigin = false; + } + + if (prev == LinePath.SEG_LINETO) { + finish(); + } + + output.end(); + this.joinSegment = false; + this.prev = LinePath.SEG_MOVETO; + } + + long lineLength(long ldx, long ldy) { + long ldet = ((long) m00 * m11 - (long) m01 * m10) >> 16; + long la = ((long) ldy * m00 - (long) ldx * m10) / ldet; + long lb = ((long) ldy * m01 - (long) ldx * m11) / ldet; + long llen = (int) LinePath.hypot(la, lb); + return llen; + } + + private void finish() { + if (capStyle == LinePath.CAP_ROUND) { + drawRoundJoin(x0, y0, omx, omy, -omx, -omy, 1, false, false, + ROUND_JOIN_THRESHOLD); + } else if (capStyle == LinePath.CAP_SQUARE) { + long ldx = (long) (px0 - x0); + long ldy = (long) (py0 - y0); + long llen = lineLength(ldx, ldy); + long s = (long) lineWidth2 * 65536 / llen; + + int capx = x0 - (int) (ldx * s >> 16); + int capy = y0 - (int) (ldy * s >> 16); + + emitLineTo(capx + omx, capy + omy); + emitLineTo(capx - omx, capy - omy); + } + + for (int i = rindex - 2; i >= 0; i -= 2) { + emitLineTo(reverse[i], reverse[i + 1]); + } + this.rindex = 0; + + if (capStyle == LinePath.CAP_ROUND) { + drawRoundJoin(sx0, sy0, -mx0, -my0, mx0, my0, 1, false, false, + ROUND_JOIN_THRESHOLD); + } else if (capStyle == LinePath.CAP_SQUARE) { + long ldx = (long) (sx1 - sx0); + long ldy = (long) (sy1 - sy0); + long llen = lineLength(ldx, ldy); + long s = (long) lineWidth2 * 65536 / llen; + + int capx = sx0 - (int) (ldx * s >> 16); + int capy = sy0 - (int) (ldy * s >> 16); + + emitLineTo(capx - mx0, capy - my0); + emitLineTo(capx + mx0, capy + my0); + } + + emitClose(); + this.joinSegment = false; + } + + private void emitMoveTo(int x0, int y0) { + // System.out.println("LineStroker.emitMoveTo(" + x0/65536.0 + ", " + y0/65536.0 + ")"); + output.moveTo(x0, y0); + } + + private void emitLineTo(int x1, int y1) { + // System.out.println("LineStroker.emitLineTo(" + x0/65536.0 + ", " + y0/65536.0 + ")"); + output.lineTo(x1, y1); + } + + private void emitLineTo(int x1, int y1, boolean rev) { + if (rev) { + ensureCapacity(rindex + 2); + reverse[rindex++] = x1; + reverse[rindex++] = y1; + } else { + emitLineTo(x1, y1); + } + } + + private void emitClose() { + // System.out.println("LineStroker.emitClose()"); + output.close(); + } +} diff --git a/java/libraries/opengl/src/processing/opengl/geom/LinePath.java b/java/libraries/opengl/src/processing/opengl/geom/LinePath.java new file mode 100644 index 000000000..b58b48bf1 --- /dev/null +++ b/java/libraries/opengl/src/processing/opengl/geom/LinePath.java @@ -0,0 +1,561 @@ +/* + * Copyright 2006 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ + +package processing.opengl.geom; + +import processing.core.PMatrix2D; + +/** + * The {@code LinePath} class allows to represent polygonal paths, + * potentially composed by several disjoint polygonal segments. + * It can be iterated by the {@link PathIterator} class including all + * of its segment types and winding rules + * + */ +public class LinePath { + /** + * The winding rule constant for specifying an even-odd rule + * for determining the interior of a path. + * The even-odd rule specifies that a point lies inside the + * path if a ray drawn in any direction from that point to + * infinity is crossed by path segments an odd number of times. + */ + public static final int WIND_EVEN_ODD = 0; + + /** + * The winding rule constant for specifying a non-zero rule + * for determining the interior of a path. + * The non-zero rule specifies that a point lies inside the + * path if a ray drawn in any direction from that point to + * infinity is crossed by path segments a different number + * of times in the counter-clockwise direction than the + * clockwise direction. + */ + public static final int WIND_NON_ZERO = 1; + + /** + * Starts segment at a given position. + */ + public static final byte SEG_MOVETO = 0; + + /** + * Extends segment by adding a line to a given position. + */ + public static final byte SEG_LINETO = 1; + + /** + * Closes segment at current position. + */ + public static final byte SEG_CLOSE = 2; + + /** + * Joins path segments by extending their outside edges until they meet. + */ + public final static int JOIN_MITER = 0; + + /** + * Joins path segments by rounding off the corner at a radius of half the line + * width. + */ + public final static int JOIN_ROUND = 1; + + /** + * Joins path segments by connecting the outer corners of their wide outlines + * with a straight segment. + */ + public final static int JOIN_BEVEL = 2; + + /** + * Ends unclosed subpaths and dash segments with no added decoration. + */ + public final static int CAP_BUTT = 0; + + /** + * Ends unclosed subpaths and dash segments with a round decoration that has a + * radius equal to half of the width of the pen. + */ + public final static int CAP_ROUND = 1; + + /** + * Ends unclosed subpaths and dash segments with a square projection that + * extends beyond the end of the segment to a distance equal to half of the + * line width. + */ + public final static int CAP_SQUARE = 2; + + private static PMatrix2D identity = new PMatrix2D(); + + private static float defaultMiterlimit = 10.0f; + + static final int INIT_SIZE = 20; + + static final int EXPAND_MAX = 500; + + protected byte[] pointTypes; + + protected float[] floatCoords; + + protected int numTypes; + + protected int numCoords; + + protected int windingRule; + + + /** + * Constructs a new empty single precision {@code LinePath} object with a + * default winding rule of {@link #WIND_NON_ZERO}. + */ + public LinePath() { + this(WIND_NON_ZERO, INIT_SIZE); + } + + + /** + * Constructs a new empty single precision {@code LinePath} object with the + * specified winding rule to control operations that require the interior of + * the path to be defined. + * + * @param rule + * the winding rule + * @see #WIND_EVEN_ODD + * @see #WIND_NON_ZERO + */ + public LinePath(int rule) { + this(rule, INIT_SIZE); + } + + + /** + * Constructs a new {@code LinePath} object from the given specified initial + * values. This method is only intended for internal use and should not be + * made public if the other constructors for this class are ever exposed. + * + * @param rule + * the winding rule + * @param initialTypes + * the size to make the initial array to store the path segment types + */ + public LinePath(int rule, int initialCapacity) { + setWindingRule(rule); + this.pointTypes = new byte[initialCapacity]; + floatCoords = new float[initialCapacity * 2]; + } + + + void needRoom(boolean needMove, int newCoords) { + if (needMove && numTypes == 0) { + throw new RuntimeException("missing initial moveto " + + "in path definition"); + } + int size = pointTypes.length; + if (numTypes >= size) { + int grow = size; + if (grow > EXPAND_MAX) { + grow = EXPAND_MAX; + } + pointTypes = copyOf(pointTypes, size + grow); + } + size = floatCoords.length; + if (numCoords + newCoords > size) { + int grow = size; + if (grow > EXPAND_MAX * 2) { + grow = EXPAND_MAX * 2; + } + if (grow < newCoords) { + grow = newCoords; + } + floatCoords = copyOf(floatCoords, size + grow); + } + } + + + /** + * Adds a point to the path by moving to the specified coordinates specified + * in float precision. + *

+ * This method provides a single precision variant of the double precision + * {@code moveTo()} method on the base {@code LinePath} class. + * + * @param x + * the specified X coordinate + * @param y + * the specified Y coordinate + * @see LinePath#moveTo + */ + public final void moveTo(float x, float y) { + if (numTypes > 0 && pointTypes[numTypes - 1] == SEG_MOVETO) { + floatCoords[numCoords - 2] = x; + floatCoords[numCoords - 1] = y; + } else { + needRoom(false, 2); + pointTypes[numTypes++] = SEG_MOVETO; + floatCoords[numCoords++] = x; + floatCoords[numCoords++] = y; + } + } + + + /** + * Adds a point to the path by drawing a straight line from the current + * coordinates to the new specified coordinates specified in float precision. + *

+ * This method provides a single precision variant of the double precision + * {@code lineTo()} method on the base {@code LinePath} class. + * + * @param x + * the specified X coordinate + * @param y + * the specified Y coordinate + * @see LinePath#lineTo + */ + public final void lineTo(float x, float y) { + needRoom(true, 2); + pointTypes[numTypes++] = SEG_LINETO; + floatCoords[numCoords++] = x; + floatCoords[numCoords++] = y; + } + + + /** + * The iterator for this class is not multi-threaded safe, which means that + * the {@code LinePath} class does not guarantee that modifications to the + * geometry of this {@code LinePath} object do not affect any iterations of that + * geometry that are already in process. + */ + public PathIterator getPathIterator() { + return new PathIterator(this); + } + + + /** + * Closes the current subpath by drawing a straight line back to the + * coordinates of the last {@code moveTo}. If the path is already closed then + * this method has no effect. + */ + public final void closePath() { + if (numTypes == 0 || pointTypes[numTypes - 1] != SEG_CLOSE) { + needRoom(false, 0); + pointTypes[numTypes++] = SEG_CLOSE; + } + } + + + /** + * Returns the fill style winding rule. + * + * @return an integer representing the current winding rule. + * @see #WIND_EVEN_ODD + * @see #WIND_NON_ZERO + * @see #setWindingRule + */ + public final int getWindingRule() { + return windingRule; + } + + + /** + * Sets the winding rule for this path to the specified value. + * + * @param rule + * an integer representing the specified winding rule + * @exception IllegalArgumentException + * if {@code rule} is not either {@link #WIND_EVEN_ODD} or + * {@link #WIND_NON_ZERO} + * @see #getWindingRule + */ + public final void setWindingRule(int rule) { + if (rule != WIND_EVEN_ODD && rule != WIND_NON_ZERO) { + throw new IllegalArgumentException("winding rule must be " + + "WIND_EVEN_ODD or " + "WIND_NON_ZERO"); + } + windingRule = rule; + } + + + /** + * Resets the path to empty. The append position is set back to the beginning + * of the path and all coordinates and point types are forgotten. + */ + public final void reset() { + numTypes = numCoords = 0; + } + + + static public class PathIterator { + float floatCoords[]; + + int typeIdx; + + int pointIdx; + + LinePath path; + + static final int curvecoords[] = { 2, 2, 0 }; + + PathIterator(LinePath p2df) { + this.path = p2df; + this.floatCoords = p2df.floatCoords; + } + + public int currentSegment(float[] coords) { + int type = path.pointTypes[typeIdx]; + int numCoords = curvecoords[type]; + if (numCoords > 0) { + System.arraycopy(floatCoords, pointIdx, coords, 0, numCoords); + } + return type; + } + + public int currentSegment(double[] coords) { + int type = path.pointTypes[typeIdx]; + int numCoords = curvecoords[type]; + if (numCoords > 0) { + for (int i = 0; i < numCoords; i++) { + coords[i] = floatCoords[pointIdx + i]; + } + } + return type; + } + + public int getWindingRule() { + return path.getWindingRule(); + } + + public boolean isDone() { + return (typeIdx >= path.numTypes); + } + + public void next() { + int type = path.pointTypes[typeIdx++]; + pointIdx += curvecoords[type]; + } + } + + + ///////////////////////////////////////////////////////////////////////////// + // + // Stroked path methods + + + static public LinePath createStrokedPath(LinePath src, float weight, + int caps, int join) { + return createStrokedPath(src, weight, caps, join, defaultMiterlimit, null); + } + + + static public LinePath createStrokedPath(LinePath src, float weight, + int caps, int join, float miterlimit) { + return createStrokedPath(src, weight, caps, join, miterlimit, null); + } + + + /** + * Constructs a solid LinePath with the specified attributes. + * + * @param src + * the original path to be stroked + * @param weight + * the weight of the stroked path + * @param cap + * the decoration of the ends of the segments in the path + * @param join + * the decoration applied where path segments meet + * @param miterlimit + * @param transform + * + */ + static public LinePath createStrokedPath(LinePath src, float weight, + int caps, int join, + float miterlimit, PMatrix2D transform) { + final LinePath dest = new LinePath(); + + strokeTo(src, weight, caps, join, miterlimit, transform, new LineSink() { + public void moveTo(int x0, int y0) { + dest.moveTo(S15_16ToFloat(x0), S15_16ToFloat(y0)); + } + + public void lineJoin() { + } + + public void lineTo(int x1, int y1) { + dest.lineTo(S15_16ToFloat(x1), S15_16ToFloat(y1)); + } + + public void close() { + dest.closePath(); + } + + public void end() { + } + }); + + return dest; + } + + + private static void strokeTo(LinePath src, float width, int caps, int join, + float miterlimit, PMatrix2D transform, + LineSink lsink) { + lsink = new LineStroker(lsink, FloatToS15_16(width), caps, join, + FloatToS15_16(miterlimit), + transform == null ? identity : transform); + + PathIterator pi = src.getPathIterator(); + pathTo(pi, lsink); + } + + + private static void pathTo(PathIterator pi, LineSink lsink) { + float coords[] = new float[2]; + while (!pi.isDone()) { + switch (pi.currentSegment(coords)) { + case SEG_MOVETO: + lsink.moveTo(FloatToS15_16(coords[0]), FloatToS15_16(coords[1])); + break; + case SEG_LINETO: + lsink.lineJoin(); + lsink.lineTo(FloatToS15_16(coords[0]), FloatToS15_16(coords[1])); + break; + case SEG_CLOSE: + lsink.lineJoin(); + lsink.close(); + break; + default: + throw new InternalError("unknown flattened segment type"); + } + pi.next(); + } + lsink.end(); + } + + + ///////////////////////////////////////////////////////////////////////////// + // + // Utility methods + + + public static float[] copyOf(float[] source, int length) { + float[] target = new float[length]; + for (int i = 0; i < target.length; i++) { + if (i > source.length - 1) + target[i] = 0f; + else + target[i] = source[i]; + } + return target; + } + + + public static byte[] copyOf(byte[] source, int length) { + byte[] target = new byte[length]; + for (int i = 0; i < target.length; i++) { + if (i > source.length - 1) + target[i] = 0; + else + target[i] = source[i]; + } + return target; + } + + + // From Ken Turkowski, _Fixed-Point Square Root_, In Graphics Gems V + public static int isqrt(int x) { + int fracbits = 16; + + int root = 0; + int remHi = 0; + int remLo = x; + int count = 15 + fracbits / 2; + + do { + remHi = (remHi << 2) | (remLo >>> 30); // N.B. - unsigned shift R + remLo <<= 2; + root <<= 1; + int testdiv = (root << 1) + 1; + if (remHi >= testdiv) { + remHi -= testdiv; + root++; + } + } while (count-- != 0); + + return root; + } + + + public static long lsqrt(long x) { + int fracbits = 16; + + long root = 0; + long remHi = 0; + long remLo = x; + int count = 31 + fracbits / 2; + + do { + remHi = (remHi << 2) | (remLo >>> 62); // N.B. - unsigned shift R + remLo <<= 2; + root <<= 1; + long testDiv = (root << 1) + 1; + if (remHi >= testDiv) { + remHi -= testDiv; + root++; + } + } while (count-- != 0); + + return root; + } + + + public static double hypot(double x, double y) { + return Math.sqrt(x * x + y * y); + } + + + public static int hypot(int x, int y) { + return (int) ((lsqrt((long) x * x + (long) y * y) + 128) >> 8); + } + + + public static long hypot(long x, long y) { + return (lsqrt(x * x + y * y) + 128) >> 8; + } + + + static int FloatToS15_16(float flt) { + flt = flt * 65536f + 0.5f; + if (flt <= -(65536f * 65536f)) { + return Integer.MIN_VALUE; + } else if (flt >= (65536f * 65536f)) { + return Integer.MAX_VALUE; + } else { + return (int) Math.floor(flt); + } + } + + + static float S15_16ToFloat(int fix) { + return (fix / 65536f); + } +} diff --git a/java/libraries/opengl/src/processing/opengl/geom/LineSink.java b/java/libraries/opengl/src/processing/opengl/geom/LineSink.java new file mode 100644 index 000000000..2e57143cd --- /dev/null +++ b/java/libraries/opengl/src/processing/opengl/geom/LineSink.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2007, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package processing.opengl.geom; + +/** + * The LineSink interface accepts a series of line + * drawing commands: moveTo, lineTo, + * close (equivalent to a lineTo command + * with an argument equal to the argument of the last + * moveTo command), and end. + * + */ +public abstract class LineSink { + /** + * Moves the current drawing position to the point (x0, + * y0). + * + * @param x0 the X coordinate in S15.16 format + * @param y0 the Y coordinate in S15.16 format + */ + public abstract void moveTo(int x0, int y0); + + /** + * Provides a hint that the current segment should be joined to + * the following segment using an explicit miter or round join if + * required. + * + *

An application-generated path will generally have no need + * to contain calls to this method; they are typically introduced + * by a Flattener to mark segment divisions that + * appear in its input, and consumed by a Stroker + * that is responsible for emitting the miter or round join + * segments. + * + *

Other LineSink classes should simply pass this + * hint to their output sink as needed. + */ + public abstract void lineJoin(); + + /** + * Draws a line from the current drawing position to the point + * (x1, y1) and sets the current drawing position to + * (x1, y1). + * + * @param x1 the X coordinate in S15.16 format + * @param y1 the Y coordinate in S15.16 format + */ + public abstract void lineTo(int x1, int y1); + + /** + * Closes the current path by drawing a line from the current + * drawing position to the point specified by the moset recent + * moveTo command. + */ + public abstract void close(); + + /** + * Ends the current path. It may be necessary to end a path in + * order to allow end caps to be drawn. + */ + public abstract void end(); +} diff --git a/java/libraries/opengl/src/processing/opengl/geom/LineStroker.java b/java/libraries/opengl/src/processing/opengl/geom/LineStroker.java new file mode 100644 index 000000000..85d035e2b --- /dev/null +++ b/java/libraries/opengl/src/processing/opengl/geom/LineStroker.java @@ -0,0 +1,714 @@ +/* + * Copyright (c) 2007, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package processing.opengl.geom; + +import processing.core.PMatrix2D; + +public class LineStroker extends LineSink { + LineSink output; + + int lineWidth; + + int capStyle; + + int joinStyle; + + int miterLimit; + + int m00, m01; + + int m10, m11; + + int lineWidth2; + + long scaledLineWidth2; + + // For any pen offset (pen_dx, pen_dy) that does not depend on + // the line orientation, the pen should be transformed so that: + // + // pen_dx' = m00*pen_dx + m01*pen_dy + // pen_dy' = m10*pen_dx + m11*pen_dy + // + // For a round pen, this means: + // + // pen_dx(r, theta) = r*cos(theta) + // pen_dy(r, theta) = r*sin(theta) + // + // pen_dx'(r, theta) = r*(m00*cos(theta) + m01*sin(theta)) + // pen_dy'(r, theta) = r*(m10*cos(theta) + m11*sin(theta)) + int numPenSegments; + + int[] pen_dx; + + int[] pen_dy; + + boolean[] penIncluded; + + int[] join; + + int[] offset = new int[2]; + + int[] reverse = new int[100]; + + int[] miter = new int[2]; + + long miterLimitSq; + + int prev; + + int rindex; + + boolean started; + + boolean lineToOrigin; + + boolean joinToOrigin; + + int sx0, sy0, sx1, sy1, x0, y0, x1, y1; + + int mx0, my0, mx1, my1, omx, omy; + + int lx0, ly0, lx1, ly1, lx0p, ly0p, px0, py0; + + double m00_2_m01_2; + + double m10_2_m11_2; + + double m00_m10_m01_m11; + + /** + * Empty constructor. setOutput and setParameters + * must be called prior to calling any other methods. + */ + public LineStroker() { + } + + /** + * Constructs a LineStroker. + * + * @param output + * an output LineSink. + * @param lineWidth + * the desired line width in pixels, in S15.16 format. + * @param capStyle + * the desired end cap style, one of CAP_BUTT, + * CAP_ROUND or CAP_SQUARE. + * @param joinStyle + * the desired line join style, one of JOIN_MITER, + * JOIN_ROUND or JOIN_BEVEL. + * @param miterLimit + * the desired miter limit, in S15.16 format. + * @param transform + * a Transform4 object indicating the transform that has + * been previously applied to all incoming coordinates. This is + * required in order to produce consistently shaped end caps and + * joins. + */ + public LineStroker(LineSink output, int lineWidth, int capStyle, int joinStyle, + int miterLimit, PMatrix2D transform) { + setOutput(output); + setParameters(lineWidth, capStyle, joinStyle, miterLimit, transform); + } + + /** + * Sets the output LineSink of this LineStroker. + * + * @param output + * an output LineSink. + */ + public void setOutput(LineSink output) { + this.output = output; + } + + /** + * Sets the parameters of this LineStroker. + * + * @param lineWidth + * the desired line width in pixels, in S15.16 format. + * @param capStyle + * the desired end cap style, one of CAP_BUTT, + * CAP_ROUND or CAP_SQUARE. + * @param joinStyle + * the desired line join style, one of JOIN_MITER, + * JOIN_ROUND or JOIN_BEVEL. + * @param miterLimit + * the desired miter limit, in S15.16 format. + * @param transform + * a Transform4 object indicating the transform that has + * been previously applied to all incoming coordinates. This is + * required in order to produce consistently shaped end caps and + * joins. + */ + public void setParameters(int lineWidth, int capStyle, int joinStyle, + int miterLimit, PMatrix2D transform) { + this.m00 = LinePath.FloatToS15_16(transform.m00); + this.m01 = LinePath.FloatToS15_16(transform.m01); + this.m10 = LinePath.FloatToS15_16(transform.m10); + this.m11 = LinePath.FloatToS15_16(transform.m11); + + this.lineWidth = lineWidth; + this.lineWidth2 = lineWidth >> 1; + this.scaledLineWidth2 = ((long) m00 * lineWidth2) >> 16; + this.capStyle = capStyle; + this.joinStyle = joinStyle; + this.miterLimit = miterLimit; + + this.m00_2_m01_2 = (double) m00 * m00 + (double) m01 * m01; + this.m10_2_m11_2 = (double) m10 * m10 + (double) m11 * m11; + this.m00_m10_m01_m11 = (double) m00 * m10 + (double) m01 * m11; + + double dm00 = m00 / 65536.0; + double dm01 = m01 / 65536.0; + double dm10 = m10 / 65536.0; + double dm11 = m11 / 65536.0; + double determinant = dm00 * dm11 - dm01 * dm10; + + if (joinStyle == LinePath.JOIN_MITER) { + double limit = (miterLimit / 65536.0) * (lineWidth2 / 65536.0) + * determinant; + double limitSq = limit * limit; + this.miterLimitSq = (long) (limitSq * 65536.0 * 65536.0); + } + + this.numPenSegments = (int) (3.14159f * lineWidth / 65536.0f); + if (pen_dx == null || pen_dx.length < numPenSegments) { + this.pen_dx = new int[numPenSegments]; + this.pen_dy = new int[numPenSegments]; + this.penIncluded = new boolean[numPenSegments]; + this.join = new int[2 * numPenSegments]; + } + + for (int i = 0; i < numPenSegments; i++) { + double r = lineWidth / 2.0; + double theta = (double) i * 2.0 * Math.PI / numPenSegments; + + double cos = Math.cos(theta); + double sin = Math.sin(theta); + pen_dx[i] = (int) (r * (dm00 * cos + dm01 * sin)); + pen_dy[i] = (int) (r * (dm10 * cos + dm11 * sin)); + } + + prev = LinePath.SEG_CLOSE; + rindex = 0; + started = false; + lineToOrigin = false; + } + + private void computeOffset(int x0, int y0, int x1, int y1, int[] m) { + long lx = (long) x1 - (long) x0; + long ly = (long) y1 - (long) y0; + + int dx, dy; + if (m00 > 0 && m00 == m11 && m01 == 0 & m10 == 0) { + long ilen = LinePath.hypot(lx, ly); + if (ilen == 0) { + dx = dy = 0; + } else { + dx = (int) ((ly * scaledLineWidth2) / ilen); + dy = (int) (-(lx * scaledLineWidth2) / ilen); + } + } else { + double dlx = x1 - x0; + double dly = y1 - y0; + double det = (double) m00 * m11 - (double) m01 * m10; + int sdet = (det > 0) ? 1 : -1; + double a = dly * m00 - dlx * m10; + double b = dly * m01 - dlx * m11; + double dh = LinePath.hypot(a, b); + double div = sdet * lineWidth2 / (65536.0 * dh); + double ddx = dly * m00_2_m01_2 - dlx * m00_m10_m01_m11; + double ddy = dly * m00_m10_m01_m11 - dlx * m10_2_m11_2; + dx = (int) (ddx * div); + dy = (int) (ddy * div); + } + + m[0] = dx; + m[1] = dy; + } + + private void ensureCapacity(int newrindex) { + if (reverse.length < newrindex) { + int[] tmp = new int[Math.max(newrindex, 6 * reverse.length / 5)]; + System.arraycopy(reverse, 0, tmp, 0, rindex); + this.reverse = tmp; + } + } + + private boolean isCCW(int x0, int y0, int x1, int y1, int x2, int y2) { + int dx0 = x1 - x0; + int dy0 = y1 - y0; + int dx1 = x2 - x1; + int dy1 = y2 - y1; + return (long) dx0 * dy1 < (long) dy0 * dx1; + } + + private boolean side(int x, int y, int x0, int y0, int x1, int y1) { + long lx = x; + long ly = y; + long lx0 = x0; + long ly0 = y0; + long lx1 = x1; + long ly1 = y1; + + return (ly0 - ly1) * lx + (lx1 - lx0) * ly + (lx0 * ly1 - lx1 * ly0) > 0; + } + + private int computeRoundJoin(int cx, int cy, int xa, int ya, int xb, int yb, + int side, boolean flip, int[] join) { + int px, py; + int ncoords = 0; + + boolean centerSide; + if (side == 0) { + centerSide = side(cx, cy, xa, ya, xb, yb); + } else { + centerSide = (side == 1) ? true : false; + } + for (int i = 0; i < numPenSegments; i++) { + px = cx + pen_dx[i]; + py = cy + pen_dy[i]; + + boolean penSide = side(px, py, xa, ya, xb, yb); + if (penSide != centerSide) { + penIncluded[i] = true; + } else { + penIncluded[i] = false; + } + } + + int start = -1, end = -1; + for (int i = 0; i < numPenSegments; i++) { + if (penIncluded[i] + && !penIncluded[(i + numPenSegments - 1) % numPenSegments]) { + start = i; + } + if (penIncluded[i] && !penIncluded[(i + 1) % numPenSegments]) { + end = i; + } + } + + if (end < start) { + end += numPenSegments; + } + + if (start != -1 && end != -1) { + long dxa = cx + pen_dx[start] - xa; + long dya = cy + pen_dy[start] - ya; + long dxb = cx + pen_dx[start] - xb; + long dyb = cy + pen_dy[start] - yb; + + boolean rev = (dxa * dxa + dya * dya > dxb * dxb + dyb * dyb); + int i = rev ? end : start; + int incr = rev ? -1 : 1; + while (true) { + int idx = i % numPenSegments; + px = cx + pen_dx[idx]; + py = cy + pen_dy[idx]; + join[ncoords++] = px; + join[ncoords++] = py; + if (i == (rev ? start : end)) { + break; + } + i += incr; + } + } + + return ncoords / 2; + } + + private static final long ROUND_JOIN_THRESHOLD = 1000L; + + private static final long ROUND_JOIN_INTERNAL_THRESHOLD = 1000000000L; + + private void drawRoundJoin(int x, int y, int omx, int omy, int mx, int my, + int side, boolean flip, boolean rev, long threshold) { + if ((omx == 0 && omy == 0) || (mx == 0 && my == 0)) { + return; + } + + long domx = (long) omx - mx; + long domy = (long) omy - my; + long len = domx * domx + domy * domy; + if (len < threshold) { + return; + } + + if (rev) { + omx = -omx; + omy = -omy; + mx = -mx; + my = -my; + } + + int bx0 = x + omx; + int by0 = y + omy; + int bx1 = x + mx; + int by1 = y + my; + + int npoints = computeRoundJoin(x, y, bx0, by0, bx1, by1, side, flip, join); + for (int i = 0; i < npoints; i++) { + emitLineTo(join[2 * i], join[2 * i + 1], rev); + } + } + + // Return the intersection point of the lines (ix0, iy0) -> (ix1, iy1) + // and (ix0p, iy0p) -> (ix1p, iy1p) in m[0] and m[1] + private void computeMiter(int ix0, int iy0, int ix1, int iy1, int ix0p, + int iy0p, int ix1p, int iy1p, int[] m) { + long x0 = ix0; + long y0 = iy0; + long x1 = ix1; + long y1 = iy1; + + long x0p = ix0p; + long y0p = iy0p; + long x1p = ix1p; + long y1p = iy1p; + + long x10 = x1 - x0; + long y10 = y1 - y0; + long x10p = x1p - x0p; + long y10p = y1p - y0p; + + long den = (x10 * y10p - x10p * y10) >> 16; + if (den == 0) { + m[0] = ix0; + m[1] = iy0; + return; + } + + long t = (x1p * (y0 - y0p) - x0 * y10p + x0p * (y1p - y0)) >> 16; + m[0] = (int) (x0 + (t * x10) / den); + m[1] = (int) (y0 + (t * y10) / den); + } + + private void drawMiter(int px0, int py0, int x0, int y0, int x1, int y1, + int omx, int omy, int mx, int my, boolean rev) { + if (mx == omx && my == omy) { + return; + } + if (px0 == x0 && py0 == y0) { + return; + } + if (x0 == x1 && y0 == y1) { + return; + } + + if (rev) { + omx = -omx; + omy = -omy; + mx = -mx; + my = -my; + } + + computeMiter(px0 + omx, py0 + omy, x0 + omx, y0 + omy, x0 + mx, y0 + my, x1 + + mx, y1 + my, miter); + + // Compute miter length in untransformed coordinates + long dx = (long) miter[0] - x0; + long dy = (long) miter[1] - y0; + long a = (dy * m00 - dx * m10) >> 16; + long b = (dy * m01 - dx * m11) >> 16; + long lenSq = a * a + b * b; + + if (lenSq < miterLimitSq) { + emitLineTo(miter[0], miter[1], rev); + } + } + + public void moveTo(int x0, int y0) { + // System.out.println("LineStroker.moveTo(" + x0/65536.0 + ", " + y0/65536.0 + ")"); + + if (lineToOrigin) { + // not closing the path, do the previous lineTo + lineToImpl(sx0, sy0, joinToOrigin); + lineToOrigin = false; + } + + if (prev == LinePath.SEG_LINETO) { + finish(); + } + + this.sx0 = this.x0 = x0; + this.sy0 = this.y0 = y0; + this.rindex = 0; + this.started = false; + this.joinSegment = false; + this.prev = LinePath.SEG_MOVETO; + } + + boolean joinSegment = false; + + public void lineJoin() { + // System.out.println("LineStroker.lineJoin()"); + this.joinSegment = true; + } + + public void lineTo(int x1, int y1) { + // System.out.println("LineStroker.lineTo(" + x1/65536.0 + ", " + y1/65536.0 + ")"); + + if (lineToOrigin) { + if (x1 == sx0 && y1 == sy0) { + // staying in the starting point + return; + } + + // not closing the path, do the previous lineTo + lineToImpl(sx0, sy0, joinToOrigin); + lineToOrigin = false; + } else if (x1 == x0 && y1 == y0) { + return; + } else if (x1 == sx0 && y1 == sy0) { + lineToOrigin = true; + joinToOrigin = joinSegment; + joinSegment = false; + return; + } + + lineToImpl(x1, y1, joinSegment); + joinSegment = false; + } + + private void lineToImpl(int x1, int y1, boolean joinSegment) { + computeOffset(x0, y0, x1, y1, offset); + int mx = offset[0]; + int my = offset[1]; + + if (!started) { + emitMoveTo(x0 + mx, y0 + my); + this.sx1 = x1; + this.sy1 = y1; + this.mx0 = mx; + this.my0 = my; + started = true; + } else { + boolean ccw = isCCW(px0, py0, x0, y0, x1, y1); + if (joinSegment) { + if (joinStyle == LinePath.JOIN_MITER) { + drawMiter(px0, py0, x0, y0, x1, y1, omx, omy, mx, my, ccw); + } else if (joinStyle == LinePath.JOIN_ROUND) { + drawRoundJoin(x0, y0, omx, omy, mx, my, 0, false, ccw, + ROUND_JOIN_THRESHOLD); + } + } else { + // Draw internal joins as round + drawRoundJoin(x0, y0, omx, omy, mx, my, 0, false, ccw, + ROUND_JOIN_INTERNAL_THRESHOLD); + } + + emitLineTo(x0, y0, !ccw); + } + + emitLineTo(x0 + mx, y0 + my, false); + emitLineTo(x1 + mx, y1 + my, false); + + emitLineTo(x0 - mx, y0 - my, true); + emitLineTo(x1 - mx, y1 - my, true); + + lx0 = x1 + mx; + ly0 = y1 + my; + lx0p = x1 - mx; + ly0p = y1 - my; + lx1 = x1; + ly1 = y1; + + this.omx = mx; + this.omy = my; + this.px0 = x0; + this.py0 = y0; + this.x0 = x1; + this.y0 = y1; + this.prev = LinePath.SEG_LINETO; + } + + public void close() { + // System.out.println("LineStroker.close()"); + + if (lineToOrigin) { + // ignore the previous lineTo + lineToOrigin = false; + } + + if (!started) { + finish(); + return; + } + + computeOffset(x0, y0, sx0, sy0, offset); + int mx = offset[0]; + int my = offset[1]; + + // Draw penultimate join + boolean ccw = isCCW(px0, py0, x0, y0, sx0, sy0); + if (joinSegment) { + if (joinStyle == LinePath.JOIN_MITER) { + drawMiter(px0, py0, x0, y0, sx0, sy0, omx, omy, mx, my, ccw); + } else if (joinStyle == LinePath.JOIN_ROUND) { + drawRoundJoin(x0, y0, omx, omy, mx, my, 0, false, ccw, + ROUND_JOIN_THRESHOLD); + } + } else { + // Draw internal joins as round + drawRoundJoin(x0, y0, omx, omy, mx, my, 0, false, ccw, + ROUND_JOIN_INTERNAL_THRESHOLD); + } + + emitLineTo(x0 + mx, y0 + my); + emitLineTo(sx0 + mx, sy0 + my); + + ccw = isCCW(x0, y0, sx0, sy0, sx1, sy1); + + // Draw final join on the outside + if (!ccw) { + if (joinStyle == LinePath.JOIN_MITER) { + drawMiter(x0, y0, sx0, sy0, sx1, sy1, mx, my, mx0, my0, false); + } else if (joinStyle == LinePath.JOIN_ROUND) { + drawRoundJoin(sx0, sy0, mx, my, mx0, my0, 0, false, false, + ROUND_JOIN_THRESHOLD); + } + } + + emitLineTo(sx0 + mx0, sy0 + my0); + emitLineTo(sx0 - mx0, sy0 - my0); // same as reverse[0], reverse[1] + + // Draw final join on the inside + if (ccw) { + if (joinStyle == LinePath.JOIN_MITER) { + drawMiter(x0, y0, sx0, sy0, sx1, sy1, -mx, -my, -mx0, -my0, false); + } else if (joinStyle == LinePath.JOIN_ROUND) { + drawRoundJoin(sx0, sy0, -mx, -my, -mx0, -my0, 0, true, false, + ROUND_JOIN_THRESHOLD); + } + } + + emitLineTo(sx0 - mx, sy0 - my); + emitLineTo(x0 - mx, y0 - my); + for (int i = rindex - 2; i >= 0; i -= 2) { + emitLineTo(reverse[i], reverse[i + 1]); + } + + this.x0 = this.sx0; + this.y0 = this.sy0; + this.rindex = 0; + this.started = false; + this.joinSegment = false; + this.prev = LinePath.SEG_CLOSE; + emitClose(); + } + + public void end() { + // System.out.println("LineStroker.end()"); + + if (lineToOrigin) { + // not closing the path, do the previous lineTo + lineToImpl(sx0, sy0, joinToOrigin); + lineToOrigin = false; + } + + if (prev == LinePath.SEG_LINETO) { + finish(); + } + + output.end(); + this.joinSegment = false; + this.prev = LinePath.SEG_MOVETO; + } + + long lineLength(long ldx, long ldy) { + long ldet = ((long) m00 * m11 - (long) m01 * m10) >> 16; + long la = ((long) ldy * m00 - (long) ldx * m10) / ldet; + long lb = ((long) ldy * m01 - (long) ldx * m11) / ldet; + long llen = (int) LinePath.hypot(la, lb); + return llen; + } + + private void finish() { + if (capStyle == LinePath.CAP_ROUND) { + drawRoundJoin(x0, y0, omx, omy, -omx, -omy, 1, false, false, + ROUND_JOIN_THRESHOLD); + } else if (capStyle == LinePath.CAP_SQUARE) { + long ldx = (long) (px0 - x0); + long ldy = (long) (py0 - y0); + long llen = lineLength(ldx, ldy); + long s = (long) lineWidth2 * 65536 / llen; + + int capx = x0 - (int) (ldx * s >> 16); + int capy = y0 - (int) (ldy * s >> 16); + + emitLineTo(capx + omx, capy + omy); + emitLineTo(capx - omx, capy - omy); + } + + for (int i = rindex - 2; i >= 0; i -= 2) { + emitLineTo(reverse[i], reverse[i + 1]); + } + this.rindex = 0; + + if (capStyle == LinePath.CAP_ROUND) { + drawRoundJoin(sx0, sy0, -mx0, -my0, mx0, my0, 1, false, false, + ROUND_JOIN_THRESHOLD); + } else if (capStyle == LinePath.CAP_SQUARE) { + long ldx = (long) (sx1 - sx0); + long ldy = (long) (sy1 - sy0); + long llen = lineLength(ldx, ldy); + long s = (long) lineWidth2 * 65536 / llen; + + int capx = sx0 - (int) (ldx * s >> 16); + int capy = sy0 - (int) (ldy * s >> 16); + + emitLineTo(capx - mx0, capy - my0); + emitLineTo(capx + mx0, capy + my0); + } + + emitClose(); + this.joinSegment = false; + } + + private void emitMoveTo(int x0, int y0) { + // System.out.println("LineStroker.emitMoveTo(" + x0/65536.0 + ", " + y0/65536.0 + ")"); + output.moveTo(x0, y0); + } + + private void emitLineTo(int x1, int y1) { + // System.out.println("LineStroker.emitLineTo(" + x0/65536.0 + ", " + y0/65536.0 + ")"); + output.lineTo(x1, y1); + } + + private void emitLineTo(int x1, int y1, boolean rev) { + if (rev) { + ensureCapacity(rindex + 2); + reverse[rindex++] = x1; + reverse[rindex++] = y1; + } else { + emitLineTo(x1, y1); + } + } + + private void emitClose() { + // System.out.println("LineStroker.emitClose()"); + output.close(); + } +}