package org.geogebra.ggbjdk.java.awt; import org.geogebra.common.awt.GBasicStroke; import org.geogebra.common.awt.GGeneralPath; import org.geogebra.common.awt.GPathIterator; import org.geogebra.common.awt.GShape; import org.geogebra.ggbjdk.java.awt.geom.GeneralPath; public class DefaultBasicStroke implements GBasicStroke { public static final int CAP_BUTT = 0; public static final int CAP_ROUND = 1; public static final int CAP_SQUARE = 2; public static final int JOIN_MITER = 0; public static final int JOIN_ROUND = 1; public static final int JOIN_BEVEL = 2; /** * Constants for calculating */ static final int MAX_LEVEL = 20; // Maximal deepness of curve subdivision static final double CURVE_DELTA = 2.0; // Width tolerance static final double CORNER_ANGLE = 4.0; // Minimum corner angel static final double CORNER_ZERO = 0.01; // Zero angle static final double CUBIC_ARC = 4.0 / 3.0 * (Math.sqrt(2.0) - 1); /** * Stroke width */ double width; /** * Stroke cap type */ int cap; /** * Stroke join type */ int join; /** * Stroke miter limit */ double miterLimit; /** * Stroke dashes array */ double dash[]; /** * The temporary pre-calculated values */ double curveDelta; double cornerDelta; double zeroDelta; double w2; double fmx, fmy; double scx, scy, smx, smy; double mx, my, cx, cy; /** * The temporary indicators */ boolean isMove; boolean isFirst; boolean checkMove; /** * The temporary and destination work paths */ BufferedPath dst, lp, rp, sp; /** * Stroke dasher class */ Dasher dasher; public DefaultBasicStroke() { this(1.0, CAP_SQUARE, JOIN_MITER, 10.0, null); } public DefaultBasicStroke(double width, int cap, int join, double miterLimit, double[] dash) { if (width < 0) { // awt.133=Negative width throw new IllegalArgumentException(("Negative width awt.133")); //$NON-NLS-1$ } if (cap != CAP_BUTT && cap != CAP_ROUND && cap != CAP_SQUARE) { // awt.134=Illegal cap throw new IllegalArgumentException(("Illegal cap awt.134")); //$NON-NLS-1$ } if (join != JOIN_MITER && join != JOIN_ROUND && join != JOIN_BEVEL) { // awt.135=Illegal join throw new IllegalArgumentException(("Illegal join awt.135")); //$NON-NLS-1$ } if (join == JOIN_MITER && miterLimit < 1.0) { // awt.136=miterLimit less than 1.0 throw new IllegalArgumentException( ("miterLimit less than 1.0 awt.136")); //$NON-NLS-1$ } if (dash != null) { if (dash.length == 0) { // awt.138=Zero dash length throw new IllegalArgumentException(("Zero dash length awt.138")); //$NON-NLS-1$ } ZERO: { for (int i = 0; i < dash.length; i++) { if (dash[i] < 0.0) { // awt.139=Negative dash[{0}] throw new IllegalArgumentException( ("Negative dash[{0}] awt.139")); //$NON-NLS-1$ } if (dash[i] > 0.0) { break ZERO; } } // awt.13A=All dash lengths zero throw new IllegalArgumentException( ("All dash lengths zero awt.13A")); //$NON-NLS-1$ } } this.width = width; this.cap = cap; this.join = join; this.miterLimit = miterLimit; this.dash = dash; } public DefaultBasicStroke(double width, int cap, int join, double miterLimit) { this(width, cap, join, miterLimit, null); } public DefaultBasicStroke(double width, int cap, int join) { this(width, cap, join, 10.0, null); } public DefaultBasicStroke(double width) { this(width, CAP_SQUARE, JOIN_MITER, 10, null); } @Override public double getLineWidth() { return width; } @Override public int getEndCap() { return cap; } @Override public int getLineJoin() { return join; } @Override public double getMiterLimit() { return miterLimit; } @Override public double[] getDashArray() { return dash; } /* * @Override public int hashCode() { HashCode hash = new HashCode(); * hash.append(width); hash.append(cap); hash.append(join); * hash.append(miterLimit); if (dash != null) { hash.append(dashPhase); for * (float element : dash) { hash.append(element); } } return * hash.hashCode(); } * * @Override public boolean equals(Object obj) { if (obj == this) { return * true; } if (obj instanceof BasicStroke) { BasicStroke bs = * (BasicStroke)obj; return bs.width == width && bs.cap == cap && bs.join == * join && bs.miterLimit == miterLimit && bs.dashPhase == dashPhase && * java.util.Arrays.equals(bs.dash, dash); } return false; } */ /** * Calculates allowable curve derivation */ static double getCurveDelta(double width) { double a = width + CURVE_DELTA; double cos = 1.0 - 2.0 * width * width / (a * a); double sin = Math.sqrt(1.0 - cos * cos); return Math.abs(sin / cos); } /** * Calculates value to detect small angle */ static double getCornerDelta(double width) { return width * width * Math.sin(Math.PI * CORNER_ANGLE / 180.0); } /** * Calculates value to detect zero angle */ static double getZeroDelta(double width) { return width * width * Math.sin(Math.PI * CORNER_ZERO / 180.0); } @Override public GShape createStrokedShape(GShape s) { w2 = width / 2.0; curveDelta = getCurveDelta(w2); cornerDelta = getCornerDelta(w2); zeroDelta = getZeroDelta(w2); dst = new BufferedPath(); lp = new BufferedPath(); rp = new BufferedPath(); if (dash == null) { createSolidShape(s.getPathIterator(null)); } else { createDashedShape(s.getPathIterator(null)); } return dst.createGeneralPath(); } /** * Generates solid stroked shape without dash * * @param pathIterator * - the PathIterator of source shape */ void createSolidShape(GPathIterator pathIterator) { double coords[] = new double[6]; mx = my = cx = cy = 0.0; isMove = false; isFirst = false; checkMove = true; boolean isClosed = true; while (!pathIterator.isDone()) { switch (pathIterator.currentSegment(coords)) { case GPathIterator.SEG_MOVETO: if (!isClosed) { closeSolidShape(); } rp.clean(); mx = cx = coords[0]; my = cy = coords[1]; isMove = true; isClosed = false; break; case GPathIterator.SEG_LINETO: addLine(cx, cy, cx = coords[0], cy = coords[1], true); break; case GPathIterator.SEG_QUADTO: addQuad(cx, cy, coords[0], coords[1], cx = coords[2], cy = coords[3]); break; case GPathIterator.SEG_CUBICTO: addCubic(cx, cy, coords[0], coords[1], coords[2], coords[3], cx = coords[4], cy = coords[5]); break; case GPathIterator.SEG_CLOSE: addLine(cx, cy, mx, my, false); addJoin(lp, mx, my, lp.xMove, lp.yMove, true); addJoin(rp, mx, my, rp.xMove, rp.yMove, false); lp.closePath(); rp.closePath(); lp.appendReverse(rp); isClosed = true; break; } pathIterator.next(); } if (!isClosed) { closeSolidShape(); } dst = lp; } /** * Closes solid shape path */ void closeSolidShape() { addCap(lp, cx, cy, rp.xLast, rp.yLast); lp.combine(rp); addCap(lp, mx, my, lp.xMove, lp.yMove); lp.closePath(); } /** * Generates dashed stroked shape * * @param pathIterator * - the PathIterator of source shape */ void createDashedShape(GPathIterator pathIterator) { double coords[] = new double[6]; mx = my = cx = cy = 0.0; smx = smy = scx = scy = 0.0; isMove = false; checkMove = false; boolean isClosed = true; while (!pathIterator.isDone()) { switch (pathIterator.currentSegment(coords)) { case GPathIterator.SEG_MOVETO: if (!isClosed) { closeDashedShape(); } dasher = new Dasher(dash, 0); lp.clean(); rp.clean(); sp = null; isFirst = true; isMove = true; isClosed = false; mx = cx = coords[0]; my = cy = coords[1]; break; case GPathIterator.SEG_LINETO: addDashLine(cx, cy, cx = coords[0], cy = coords[1]); break; case GPathIterator.SEG_QUADTO: addDashQuad(cx, cy, coords[0], coords[1], cx = coords[2], cy = coords[3]); break; case GPathIterator.SEG_CUBICTO: addDashCubic(cx, cy, coords[0], coords[1], coords[2], coords[3], cx = coords[4], cy = coords[5]); break; case GPathIterator.SEG_CLOSE: addDashLine(cx, cy, cx = mx, cy = my); if (dasher.isConnected() && sp != null) { // Connect current and head segments addJoin(lp, fmx, fmy, sp.xMove, sp.yMove, true); lp.join(sp); addJoin(lp, fmx, fmy, rp.xLast, rp.yLast, true); lp.combine(rp); addCap(lp, smx, smy, lp.xMove, lp.yMove); lp.closePath(); dst.append(lp); sp = null; } else { closeDashedShape(); } isClosed = true; break; } pathIterator.next(); } if (!isClosed) { closeDashedShape(); } } /** * Closes dashed shape path */ void closeDashedShape() { // Add head segment if (sp != null) { addCap(sp, fmx, fmy, sp.xMove, sp.yMove); sp.closePath(); dst.append(sp); } if (lp.typeSize > 0) { // Close current segment if (!dasher.isClosed()) { addCap(lp, scx, scy, rp.xLast, rp.yLast); lp.combine(rp); addCap(lp, smx, smy, lp.xMove, lp.yMove); lp.closePath(); } dst.append(lp); } } /** * Adds cap to the work path * * @param p * - the BufferedPath object of work path * @param x0 * - the x coordinate of the source path * @param y0 * - the y coordinate on the source path * @param x2 * - the x coordinate of the next point on the work path * @param y2 * - the y coordinate of the next point on the work path */ void addCap(BufferedPath p, double x0, double y0, double x2, double y2) { double x1 = p.xLast; double y1 = p.yLast; double x10 = x1 - x0; double y10 = y1 - y0; double x20 = x2 - x0; double y20 = y2 - y0; switch (cap) { case CAP_BUTT: p.lineTo(x2, y2); break; case CAP_ROUND: double mx = x10 * CUBIC_ARC; double my = y10 * CUBIC_ARC; double x3 = x0 + y10; double y3 = y0 - x10; x10 *= CUBIC_ARC; y10 *= CUBIC_ARC; x20 *= CUBIC_ARC; y20 *= CUBIC_ARC; p.cubicTo(x1 + y10, y1 - x10, x3 + mx, y3 + my, x3, y3); p.cubicTo(x3 - mx, y3 - my, x2 - y20, y2 + x20, x2, y2); break; case CAP_SQUARE: p.lineTo(x1 + y10, y1 - x10); p.lineTo(x2 - y20, y2 + x20); p.lineTo(x2, y2); break; } } /** * Adds bevel and miter join to the work path * * @param p * - the BufferedPath object of work path * @param x0 * - the x coordinate of the source path * @param y0 * - the y coordinate on the source path * @param x2 * - the x coordinate of the next point on the work path * @param y2 * - the y coordinate of the next point on the work path * @param isLeft * - the orientation of work path, true if work path lies to the * left from source path, false otherwise */ void addJoin(BufferedPath p, double x0, double y0, double x2, double y2, boolean isLeft) { double x1 = p.xLast; double y1 = p.yLast; double x10 = x1 - x0; double y10 = y1 - y0; double x20 = x2 - x0; double y20 = y2 - y0; double sin0 = x10 * y20 - y10 * x20; // Small corner if (-cornerDelta < sin0 && sin0 < cornerDelta) { double cos0 = x10 * x20 + y10 * y20; if (cos0 > 0.0) { // if zero corner do nothing if (-zeroDelta > sin0 || sin0 > zeroDelta) { double x3 = x0 + w2 * w2 * (y20 - y10) / sin0; double y3 = y0 + w2 * w2 * (x10 - x20) / sin0; p.setLast(x3, y3); } return; } // Zero corner if (-zeroDelta < sin0 && sin0 < zeroDelta) { p.lineTo(x2, y2); } return; } if (isLeft ^ (sin0 < 0.0)) { // Twisted corner p.lineTo(x0, y0); p.lineTo(x2, y2); } else { switch (join) { case JOIN_BEVEL: p.lineTo(x2, y2); break; case JOIN_MITER: double s1 = x1 * x10 + y1 * y10; double s2 = x2 * x20 + y2 * y20; double x3 = (s1 * y20 - s2 * y10) / sin0; double y3 = (s2 * x10 - s1 * x20) / sin0; double x30 = x3 - x0; double y30 = y3 - y0; double miterLength = Math.sqrt(x30 * x30 + y30 * y30); if (miterLength < miterLimit * w2) { p.lineTo(x3, y3); } p.lineTo(x2, y2); break; case JOIN_ROUND: addRoundJoin(p, x0, y0, x2, y2, isLeft); break; } } } /** * Adds round join to the work path * * @param p * - the BufferedPath object of work path * @param x0 * - the x coordinate of the source path * @param y0 * - the y coordinate on the source path * @param x2 * - the x coordinate of the next point on the work path * @param y2 * - the y coordinate of the next point on the work path * @param isLeft * - the orientation of work path, true if work path lies to the * left from source path, false otherwise */ void addRoundJoin(BufferedPath p, double x0, double y0, double x2, double y2, boolean isLeft) { double x1 = p.xLast; double y1 = p.yLast; double x10 = x1 - x0; double y10 = y1 - y0; double x20 = x2 - x0; double y20 = y2 - y0; double x30 = x10 + x20; double y30 = y10 + y20; double l30 = Math.sqrt(x30 * x30 + y30 * y30); if (l30 < 1E-5) { p.lineTo(x2, y2); return; } double w = w2 / l30; x30 *= w; y30 *= w; double x3 = x0 + x30; double y3 = y0 + y30; double cos = x10 * x20 + y10 * y20; double a = Math.acos(cos / (w2 * w2)); if (cos >= 0.0) { double k = 4.0 / 3.0 * Math.tan(a / 4.0); if (isLeft) { k = -k; } x10 *= k; y10 *= k; x20 *= k; y20 *= k; p.cubicTo(x1 - y10, y1 + x10, x2 + y20, y2 - x20, x2, y2); } else { double k = 4.0 / 3.0 * Math.tan(a / 8.0); if (isLeft) { k = -k; } x10 *= k; y10 *= k; x20 *= k; y20 *= k; x30 *= k; y30 *= k; p.cubicTo(x1 - y10, y1 + x10, x3 + y30, y3 - x30, x3, y3); p.cubicTo(x3 - y30, y3 + x30, x2 + y20, y2 - x20, x2, y2); } } /** * Adds solid line segment to the work path * * @param x1 * - the x coordinate of the start line point * @param y1 * - the y coordinate of the start line point * @param x2 * - the x coordinate of the end line point * @param y2 * - the y coordinate of the end line point * @param zero * - if true it's allowable to add zero length line segment */ void addLine(double x1, double y1, double x2, double y2, boolean zero) { double dx = x2 - x1; double dy = y2 - y1; if (dx == 0.0 && dy == 0.0) { if (!zero) { return; } dx = w2; dy = 0; } else { double w = w2 / Math.sqrt(dx * dx + dy * dy); dx *= w; dy *= w; } double lx1 = x1 - dy; double ly1 = y1 + dx; double rx1 = x1 + dy; double ry1 = y1 - dx; if (checkMove) { if (isMove) { isMove = false; lp.moveTo(lx1, ly1); rp.moveTo(rx1, ry1); } else { addJoin(lp, x1, y1, lx1, ly1, true); addJoin(rp, x1, y1, rx1, ry1, false); } } lp.lineTo(x2 - dy, y2 + dx); rp.lineTo(x2 + dy, y2 - dx); } /** * Adds solid quad segment to the work path * * @param x1 * - the x coordinate of the first control point * @param y1 * - the y coordinate of the first control point * @param x2 * - the x coordinate of the second control point * @param y2 * - the y coordinate of the second control point * @param x3 * - the x coordinate of the third control point * @param y3 * - the y coordinate of the third control point */ void addQuad(double x1, double y1, double x2, double y2, double x3, double y3) { double x21 = x2 - x1; double y21 = y2 - y1; double x23 = x2 - x3; double y23 = y2 - y3; double l21 = Math.sqrt(x21 * x21 + y21 * y21); double l23 = Math.sqrt(x23 * x23 + y23 * y23); if (l21 == 0.0 && l23 == 0.0) { addLine(x1, y1, x3, y3, false); return; } if (l21 == 0.0) { addLine(x2, y2, x3, y3, false); return; } if (l23 == 0.0) { addLine(x1, y1, x2, y2, false); return; } double w; w = w2 / l21; double mx1 = -y21 * w; double my1 = x21 * w; w = w2 / l23; double mx3 = y23 * w; double my3 = -x23 * w; double lx1 = x1 + mx1; double ly1 = y1 + my1; double rx1 = x1 - mx1; double ry1 = y1 - my1; if (checkMove) { if (isMove) { isMove = false; lp.moveTo(lx1, ly1); rp.moveTo(rx1, ry1); } else { addJoin(lp, x1, y1, lx1, ly1, true); addJoin(rp, x1, y1, rx1, ry1, false); } } if (x21 * y23 - y21 * x23 == 0.0) { // On line curve if (x21 * x23 + y21 * y23 > 0.0) { // Twisted curve if (l21 == l23) { double px = x1 + (x21 + x23) / 4.0; double py = y1 + (y21 + y23) / 4.0; lp.lineTo(px + mx1, py + my1); rp.lineTo(px - mx1, py - my1); lp.lineTo(px - mx1, py - my1); rp.lineTo(px + mx1, py + my1); lp.lineTo(x3 - mx1, y3 - my1); rp.lineTo(x3 + mx1, y3 + my1); } else { double px1, py1; double k = l21 / (l21 + l23); double px = x1 + (x21 + x23) * k * k; double py = y1 + (y21 + y23) * k * k; px1 = (x1 + px) / 2.0; py1 = (y1 + py) / 2.0; lp.quadTo(px1 + mx1, py1 + my1, px + mx1, py + my1); rp.quadTo(px1 - mx1, py1 - my1, px - mx1, py - my1); lp.lineTo(px - mx1, py - my1); rp.lineTo(px + mx1, py + my1); px1 = (x3 + px) / 2.0; py1 = (y3 + py) / 2.0; lp.quadTo(px1 - mx1, py1 - my1, x3 - mx1, y3 - my1); rp.quadTo(px1 + mx1, py1 + my1, x3 + mx1, y3 + my1); } } else { // Simple curve lp.quadTo(x2 + mx1, y2 + my1, x3 + mx3, y3 + my3); rp.quadTo(x2 - mx1, y2 - my1, x3 - mx3, y3 - my3); } } else { addSubQuad(x1, y1, x2, y2, x3, y3, 0); } } /** * Subdivides solid quad curve to make outline for source quad segment and * adds it to work path * * @param x1 * - the x coordinate of the first control point * @param y1 * - the y coordinate of the first control point * @param x2 * - the x coordinate of the second control point * @param y2 * - the y coordinate of the second control point * @param x3 * - the x coordinate of the third control point * @param y3 * - the y coordinate of the third control point * @param level * - the maximum level of subdivision deepness */ void addSubQuad(double x1, double y1, double x2, double y2, double x3, double y3, int level) { double x21 = x2 - x1; double y21 = y2 - y1; double x23 = x2 - x3; double y23 = y2 - y3; double cos = x21 * x23 + y21 * y23; double sin = x21 * y23 - y21 * x23; if (level < MAX_LEVEL && (cos >= 0.0 || (Math.abs(sin / cos) > curveDelta))) { double c1x = (x2 + x1) / 2.0; double c1y = (y2 + y1) / 2.0; double c2x = (x2 + x3) / 2.0; double c2y = (y2 + y3) / 2.0; double c3x = (c1x + c2x) / 2.0; double c3y = (c1y + c2y) / 2.0; addSubQuad(x1, y1, c1x, c1y, c3x, c3y, level + 1); addSubQuad(c3x, c3y, c2x, c2y, x3, y3, level + 1); } else { double w; double l21 = Math.sqrt(x21 * x21 + y21 * y21); double l23 = Math.sqrt(x23 * x23 + y23 * y23); w = w2 / sin; double mx2 = (x21 * l23 + x23 * l21) * w; double my2 = (y21 * l23 + y23 * l21) * w; w = w2 / l23; double mx3 = y23 * w; double my3 = -x23 * w; lp.quadTo(x2 + mx2, y2 + my2, x3 + mx3, y3 + my3); rp.quadTo(x2 - mx2, y2 - my2, x3 - mx3, y3 - my3); } } /** * Adds solid cubic segment to the work path * * @param x1 * - the x coordinate of the first control point * @param y1 * - the y coordinate of the first control point * @param x2 * - the x coordinate of the second control point * @param y2 * - the y coordinate of the second control point * @param x3 * - the x coordinate of the third control point * @param y3 * - the y coordinate of the third control point * @param x4 * - the x coordinate of the fours control point * @param y4 * - the y coordinate of the fours control point */ void addCubic(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) { double x12 = x1 - x2; double y12 = y1 - y2; double x23 = x2 - x3; double y23 = y2 - y3; double x34 = x3 - x4; double y34 = y3 - y4; double l12 = Math.sqrt(x12 * x12 + y12 * y12); double l23 = Math.sqrt(x23 * x23 + y23 * y23); double l34 = Math.sqrt(x34 * x34 + y34 * y34); // All edges are zero if (l12 == 0.0 && l23 == 0.0 && l34 == 0.0) { addLine(x1, y1, x4, y4, false); return; } // One zero edge if (l12 == 0.0 && l23 == 0.0) { addLine(x3, y3, x4, y4, false); return; } if (l23 == 0.0 && l34 == 0.0) { addLine(x1, y1, x2, y2, false); return; } if (l12 == 0.0 && l34 == 0.0) { addLine(x2, y2, x3, y3, false); return; } double w, mx1, my1, mx4, my4; boolean onLine; if (l12 == 0.0) { w = w2 / l23; mx1 = y23 * w; my1 = -x23 * w; w = w2 / l34; mx4 = y34 * w; my4 = -x34 * w; onLine = -x23 * y34 + y23 * x34 == 0.0; // sin3 } else if (l34 == 0.0) { w = w2 / l12; mx1 = y12 * w; my1 = -x12 * w; w = w2 / l23; mx4 = y23 * w; my4 = -x23 * w; onLine = -x12 * y23 + y12 * x23 == 0.0; // sin2 } else { w = w2 / l12; mx1 = y12 * w; my1 = -x12 * w; w = w2 / l34; mx4 = y34 * w; my4 = -x34 * w; if (l23 == 0.0) { onLine = -x12 * y34 + y12 * x34 == 0.0; } else { onLine = -x12 * y34 + y12 * x34 == 0.0 && -x12 * y23 + y12 * x23 == 0.0 && // sin2 -x23 * y34 + y23 * x34 == 0.0; // sin3 } } double lx1 = x1 + mx1; double ly1 = y1 + my1; double rx1 = x1 - mx1; double ry1 = y1 - my1; if (checkMove) { if (isMove) { isMove = false; lp.moveTo(lx1, ly1); rp.moveTo(rx1, ry1); } else { addJoin(lp, x1, y1, lx1, ly1, true); addJoin(rp, x1, y1, rx1, ry1, false); } } if (onLine) { if ((x1 == x2 && y1 < y2) || x1 < x2) { l12 = -l12; } if ((x2 == x3 && y2 < y3) || x2 < x3) { l23 = -l23; } if ((x3 == x4 && y3 < y4) || x3 < x4) { l34 = -l34; } double d = l23 * l23 - l12 * l34; double roots[] = new double[3]; int rc = 0; if (d == 0.0) { double t = (l12 - l23) / (l12 + l34 - l23 - l23); if (0.0 < t && t < 1.0) { roots[rc++] = t; } } else if (d > 0.0) { d = Math.sqrt(d); double z = l12 + l34 - l23 - l23; double t; t = (l12 - l23 + d) / z; if (0.0 < t && t < 1.0) { roots[rc++] = t; } t = (l12 - l23 - d) / z; if (0.0 < t && t < 1.0) { roots[rc++] = t; } } if (rc > 0) { // Sort roots if (rc == 2 && roots[0] > roots[1]) { double tmp = roots[0]; roots[0] = roots[1]; roots[1] = tmp; } roots[rc++] = 1.0; double ax = -x34 - x12 + x23 + x23; double ay = -y34 - y12 + y23 + y23; double bx = 3.0 * (-x23 + x12); double by = 3.0 * (-y23 + y12); double cx = 3.0 * (-x12); double cy = 3.0 * (-y12); double xPrev = x1; double yPrev = y1; for (int i = 0; i < rc; i++) { double t = roots[i]; double px = t * (t * (t * ax + bx) + cx) + x1; double py = t * (t * (t * ay + by) + cy) + y1; double px1 = (xPrev + px) / 2.0; double py1 = (yPrev + py) / 2.0; lp.cubicTo(px1 + mx1, py1 + my1, px1 + mx1, py1 + my1, px + mx1, py + my1); rp.cubicTo(px1 - mx1, py1 - my1, px1 - mx1, py1 - my1, px - mx1, py - my1); if (i < rc - 1) { lp.lineTo(px - mx1, py - my1); rp.lineTo(px + mx1, py + my1); } xPrev = px; yPrev = py; mx1 = -mx1; my1 = -my1; } } else { lp.cubicTo(x2 + mx1, y2 + my1, x3 + mx4, y3 + my4, x4 + mx4, y4 + my4); rp.cubicTo(x2 - mx1, y2 - my1, x3 - mx4, y3 - my4, x4 - mx4, y4 - my4); } } else { addSubCubic(x1, y1, x2, y2, x3, y3, x4, y4, 0); } } /** * Subdivides solid cubic curve to make outline for source quad segment and * adds it to work path * * @param x1 * - the x coordinate of the first control point * @param y1 * - the y coordinate of the first control point * @param x2 * - the x coordinate of the second control point * @param y2 * - the y coordinate of the second control point * @param x3 * - the x coordinate of the third control point * @param y3 * - the y coordinate of the third control point * @param x4 * - the x coordinate of the fours control point * @param y4 * - the y coordinate of the fours control point * @param level * - the maximum level of subdivision deepness */ void addSubCubic(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4, int level) { double x12 = x1 - x2; double y12 = y1 - y2; double x23 = x2 - x3; double y23 = y2 - y3; double x34 = x3 - x4; double y34 = y3 - y4; double cos2 = -x12 * x23 - y12 * y23; double cos3 = -x23 * x34 - y23 * y34; double sin2 = -x12 * y23 + y12 * x23; double sin3 = -x23 * y34 + y23 * x34; double sin0 = -x12 * y34 + y12 * x34; double cos0 = -x12 * x34 - y12 * y34; if (level < MAX_LEVEL && (sin2 != 0.0 || sin3 != 0.0 || sin0 != 0.0) && (cos2 >= 0.0 || cos3 >= 0.0 || cos0 >= 0.0 || (Math.abs(sin2 / cos2) > curveDelta) || (Math.abs(sin3 / cos3) > curveDelta) || (Math .abs(sin0 / cos0) > curveDelta))) { double cx = (x2 + x3) / 2.0; double cy = (y2 + y3) / 2.0; double lx2 = (x2 + x1) / 2.0; double ly2 = (y2 + y1) / 2.0; double rx3 = (x3 + x4) / 2.0; double ry3 = (y3 + y4) / 2.0; double lx3 = (cx + lx2) / 2.0; double ly3 = (cy + ly2) / 2.0; double rx2 = (cx + rx3) / 2.0; double ry2 = (cy + ry3) / 2.0; cx = (lx3 + rx2) / 2.0; cy = (ly3 + ry2) / 2.0; addSubCubic(x1, y1, lx2, ly2, lx3, ly3, cx, cy, level + 1); addSubCubic(cx, cy, rx2, ry2, rx3, ry3, x4, y4, level + 1); } else { double w, mx1, my1, mx2, my2, mx3, my3, mx4, my4; double l12 = Math.sqrt(x12 * x12 + y12 * y12); double l23 = Math.sqrt(x23 * x23 + y23 * y23); double l34 = Math.sqrt(x34 * x34 + y34 * y34); if (l12 == 0.0) { w = w2 / l23; mx1 = y23 * w; my1 = -x23 * w; w = w2 / l34; mx4 = y34 * w; my4 = -x34 * w; } else if (l34 == 0.0) { w = w2 / l12; mx1 = y12 * w; my1 = -x12 * w; w = w2 / l23; mx4 = y23 * w; my4 = -x23 * w; } else { // Common case w = w2 / l12; mx1 = y12 * w; my1 = -x12 * w; w = w2 / l34; mx4 = y34 * w; my4 = -x34 * w; } if (sin2 == 0.0) { mx2 = mx1; my2 = my1; } else { w = w2 / sin2; mx2 = -(x12 * l23 - x23 * l12) * w; my2 = -(y12 * l23 - y23 * l12) * w; } if (sin3 == 0.0) { mx3 = mx4; my3 = my4; } else { w = w2 / sin3; mx3 = -(x23 * l34 - x34 * l23) * w; my3 = -(y23 * l34 - y34 * l23) * w; } lp.cubicTo(x2 + mx2, y2 + my2, x3 + mx3, y3 + my3, x4 + mx4, y4 + my4); rp.cubicTo(x2 - mx2, y2 - my2, x3 - mx3, y3 - my3, x4 - mx4, y4 - my4); } } /** * Adds dashed line segment to the work path * * @param x1 * - the x coordinate of the start line point * @param y1 * - the y coordinate of the start line point * @param x2 * - the x coordinate of the end line point * @param y2 * - the y coordinate of the end line point */ void addDashLine(double x1, double y1, double x2, double y2) { double x21 = x2 - x1; double y21 = y2 - y1; double l21 = Math.sqrt(x21 * x21 + y21 * y21); if (l21 == 0.0) { return; } double px1, py1; px1 = py1 = 0.0; double w = w2 / l21; double mx = -y21 * w; double my = x21 * w; dasher.init(new DashIterator.Line(l21)); while (!dasher.eof()) { double t = dasher.getValue(); scx = x1 + t * x21; scy = y1 + t * y21; if (dasher.isOpen()) { px1 = scx; py1 = scy; double lx1 = px1 + mx; double ly1 = py1 + my; double rx1 = px1 - mx; double ry1 = py1 - my; if (isMove) { isMove = false; smx = px1; smy = py1; rp.clean(); lp.moveTo(lx1, ly1); rp.moveTo(rx1, ry1); } else { addJoin(lp, x1, y1, lx1, ly1, true); addJoin(rp, x1, y1, rx1, ry1, false); } } else if (dasher.isContinue()) { double px2 = scx; double py2 = scy; lp.lineTo(px2 + mx, py2 + my); rp.lineTo(px2 - mx, py2 - my); if (dasher.close) { addCap(lp, px2, py2, rp.xLast, rp.yLast); lp.combine(rp); if (isFirst) { isFirst = false; fmx = smx; fmy = smy; sp = lp; lp = new BufferedPath(); } else { addCap(lp, smx, smy, lp.xMove, lp.yMove); lp.closePath(); } isMove = true; } } dasher.next(); } } /** * Adds dashed quad segment to the work path * * @param x1 * - the x coordinate of the first control point * @param y1 * - the y coordinate of the first control point * @param x2 * - the x coordinate of the second control point * @param y2 * - the y coordinate of the second control point * @param x3 * - the x coordinate of the third control point * @param y3 * - the y coordinate of the third control point */ void addDashQuad(double x1, double y1, double x2, double y2, double x3, double y3) { double x21 = x2 - x1; double y21 = y2 - y1; double x23 = x2 - x3; double y23 = y2 - y3; double l21 = Math.sqrt(x21 * x21 + y21 * y21); double l23 = Math.sqrt(x23 * x23 + y23 * y23); if (l21 == 0.0 && l23 == 0.0) { return; } if (l21 == 0.0) { addDashLine(x2, y2, x3, y3); return; } if (l23 == 0.0) { addDashLine(x1, y1, x2, y2); return; } double ax = x1 + x3 - x2 - x2; double ay = y1 + y3 - y2 - y2; double bx = x2 - x1; double by = y2 - y1; double cx = x1; double cy = y1; double px1, py1, dx1, dy1; px1 = py1 = dx1 = dy1 = 0.0; double prev = 0.0; dasher.init(new DashIterator.Quad(x1, y1, x2, y2, x3, y3)); while (!dasher.eof()) { double t = dasher.getValue(); double dx = t * ax + bx; double dy = t * ay + by; scx = t * (dx + bx) + cx; // t^2 * ax + 2.0 * t * bx + cx scy = t * (dy + by) + cy; // t^2 * ay + 2.0 * t * by + cy if (dasher.isOpen()) { px1 = scx; py1 = scy; dx1 = dx; dy1 = dy; double w = w2 / Math.sqrt(dx1 * dx1 + dy1 * dy1); double mx1 = -dy1 * w; double my1 = dx1 * w; double lx1 = px1 + mx1; double ly1 = py1 + my1; double rx1 = px1 - mx1; double ry1 = py1 - my1; if (isMove) { isMove = false; smx = px1; smy = py1; rp.clean(); lp.moveTo(lx1, ly1); rp.moveTo(rx1, ry1); } else { addJoin(lp, x1, y1, lx1, ly1, true); addJoin(rp, x1, y1, rx1, ry1, false); } } else if (dasher.isContinue()) { double px3 = scx; double py3 = scy; double sx = x2 - x23 * prev; double sy = y2 - y23 * prev; double t2 = (t - prev) / (1 - prev); double px2 = px1 + (sx - px1) * t2; double py2 = py1 + (sy - py1) * t2; addQuad(px1, py1, px2, py2, px3, py3); if (dasher.isClosed()) { addCap(lp, px3, py3, rp.xLast, rp.yLast); lp.combine(rp); if (isFirst) { isFirst = false; fmx = smx; fmy = smy; sp = lp; lp = new BufferedPath(); } else { addCap(lp, smx, smy, lp.xMove, lp.yMove); lp.closePath(); } isMove = true; } } prev = t; dasher.next(); } } /** * Adds dashed cubic segment to the work path * * @param x1 * - the x coordinate of the first control point * @param y1 * - the y coordinate of the first control point * @param x2 * - the x coordinate of the second control point * @param y2 * - the y coordinate of the second control point * @param x3 * - the x coordinate of the third control point * @param y3 * - the y coordinate of the third control point * @param x4 * - the x coordinate of the fours control point * @param y4 * - the y coordinate of the fours control point */ void addDashCubic(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) { double x12 = x1 - x2; double y12 = y1 - y2; double x23 = x2 - x3; double y23 = y2 - y3; double x34 = x3 - x4; double y34 = y3 - y4; double l12 = Math.sqrt(x12 * x12 + y12 * y12); double l23 = Math.sqrt(x23 * x23 + y23 * y23); double l34 = Math.sqrt(x34 * x34 + y34 * y34); // All edges are zero if (l12 == 0.0 && l23 == 0.0 && l34 == 0.0) { // NOTHING return; } // One zero edge if (l12 == 0.0 && l23 == 0.0) { addDashLine(x3, y3, x4, y4); return; } if (l23 == 0.0 && l34 == 0.0) { addDashLine(x1, y1, x2, y2); return; } if (l12 == 0.0 && l34 == 0.0) { addDashLine(x2, y2, x3, y3); return; } double ax = x4 - x1 + 3.0 * (x2 - x3); double ay = y4 - y1 + 3.0 * (y2 - y3); double bx = 3.0 * (x1 + x3 - x2 - x2); double by = 3.0 * (y1 + y3 - y2 - y2); double cx = 3.0 * (x2 - x1); double cy = 3.0 * (y2 - y1); double dx = x1; double dy = y1; double px1 = 0.0; double py1 = 0.0; double prev = 0.0; dasher.init(new DashIterator.Cubic(x1, y1, x2, y2, x3, y3, x4, y4)); while (!dasher.eof()) { double t = dasher.getValue(); scx = t * (t * (t * ax + bx) + cx) + dx; scy = t * (t * (t * ay + by) + cy) + dy; if (dasher.isOpen()) { px1 = scx; py1 = scy; double dx1 = t * (t * (ax + ax + ax) + bx + bx) + cx; double dy1 = t * (t * (ay + ay + ay) + by + by) + cy; double w = w2 / Math.sqrt(dx1 * dx1 + dy1 * dy1); double mx1 = -dy1 * w; double my1 = dx1 * w; double lx1 = px1 + mx1; double ly1 = py1 + my1; double rx1 = px1 - mx1; double ry1 = py1 - my1; if (isMove) { isMove = false; smx = px1; smy = py1; rp.clean(); lp.moveTo(lx1, ly1); rp.moveTo(rx1, ry1); } else { addJoin(lp, x1, y1, lx1, ly1, true); addJoin(rp, x1, y1, rx1, ry1, false); } } else if (dasher.isContinue()) { double sx1 = x2 - x23 * prev; double sy1 = y2 - y23 * prev; double sx2 = x3 - x34 * prev; double sy2 = y3 - y34 * prev; double sx3 = sx1 + (sx2 - sx1) * prev; double sy3 = sy1 + (sy2 - sy1) * prev; double t2 = (t - prev) / (1 - prev); double sx4 = sx3 + (sx2 - sx3) * t2; double sy4 = sy3 + (sy2 - sy3) * t2; double px4 = scx; double py4 = scy; double px2 = px1 + (sx3 - px1) * t2; double py2 = py1 + (sy3 - py1) * t2; double px3 = px2 + (sx4 - px2) * t2; double py3 = py2 + (sy4 - py2) * t2; addCubic(px1, py1, px2, py2, px3, py3, px4, py4); if (dasher.isClosed()) { addCap(lp, px4, py4, rp.xLast, rp.yLast); lp.combine(rp); if (isFirst) { isFirst = false; fmx = smx; fmy = smy; sp = lp; lp = new BufferedPath(); } else { addCap(lp, smx, smy, lp.xMove, lp.yMove); lp.closePath(); } isMove = true; } } prev = t; dasher.next(); } } /** * Dasher class provides dashing for particular dash style */ static class Dasher { double pos; boolean close, visible, first; double dash1[]; int index; DashIterator iter; Dasher(double dash[], double phase) { this.dash1 = dash; index = 0; pos = phase; visible = true; while (pos >= dash[index]) { visible = !visible; pos -= dash[index]; index = (index + 1) % dash.length; } pos = -pos; first = visible; } void init(DashIterator iter) { this.iter = iter; close = true; } boolean isOpen() { return visible && pos < iter.length; } boolean isContinue() { return !visible && pos > 0; } boolean isClosed() { return close; } boolean isConnected() { return first && !close; } boolean eof() { if (!close) { pos -= iter.length; return true; } if (pos >= iter.length) { if (visible) { pos -= iter.length; return true; } close = pos == iter.length; } return false; } void next() { if (close) { pos += dash1[index]; index = (index + 1) % dash1.length; } else { // Go back index = (index + dash1.length - 1) % dash1.length; pos -= dash1[index]; } visible = !visible; } double getValue() { double t = iter.getNext(pos); return t < 0 ? 0 : (t > 1 ? 1 : t); } } /** * DashIterator class provides dashing for particular segment type */ static abstract class DashIterator { static final double FLATNESS = 1.0; static class Line extends DashIterator { Line(double len) { length = len; } @Override double getNext(double dashPos) { return dashPos / length; } } static class Quad extends DashIterator { int valSize; int valPos; double curLen; double prevLen; double lastLen; double[] values; double step; Quad(double x1, double y1, double x2, double y2, double x3, double y3) { double nx = x1 + x3 - x2 - x2; double ny = y1 + y3 - y2 - y2; int n = (int) (1 + Math.sqrt(0.75 * (Math.abs(nx) + Math.abs(ny)) * FLATNESS)); step = 1.0 / n; double ax = x1 + x3 - x2 - x2; double ay = y1 + y3 - y2 - y2; double bx = 2.0 * (x2 - x1); double by = 2.0 * (y2 - y1); double dx1 = step * (step * ax + bx); double dy1 = step * (step * ay + by); double dx2 = step * (step * ax * 2.0); double dy2 = step * (step * ay * 2.0); double vx = x1; double vy = y1; valSize = n; values = new double[valSize]; double pvx = vx; double pvy = vy; length = 0.0; for (int i = 0; i < n; i++) { vx += dx1; vy += dy1; dx1 += dx2; dy1 += dy2; double lx = vx - pvx; double ly = vy - pvy; values[i] = Math.sqrt(lx * lx + ly * ly); length += values[i]; pvx = vx; pvy = vy; } valPos = 0; curLen = 0.0; prevLen = 0.0; } @Override double getNext(double dashPos) { double t = 2.0; while (curLen <= dashPos && valPos < valSize) { prevLen = curLen; curLen += lastLen = values[valPos++]; } if (curLen > dashPos) { t = (valPos - 1 + (dashPos - prevLen) / lastLen) * step; } return t; } } static class Cubic extends DashIterator { int valSize; int valPos; double curLen; double prevLen; double lastLen; double[] values; double step; Cubic(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) { double nx1 = x1 + x3 - x2 - x2; double ny1 = y1 + y3 - y2 - y2; double nx2 = x2 + x4 - x3 - x3; double ny2 = y2 + y4 - y3 - y3; double max = Math.max(Math.abs(nx1) + Math.abs(ny1), Math.abs(nx2) + Math.abs(ny2)); int n = (int) (1 + Math.sqrt(0.75 * max) * FLATNESS); step = 1.0 / n; double ax = x4 - x1 + 3.0 * (x2 - x3); double ay = y4 - y1 + 3.0 * (y2 - y3); double bx = 3.0 * (x1 + x3 - x2 - x2); double by = 3.0 * (y1 + y3 - y2 - y2); double cx = 3.0 * (x2 - x1); double cy = 3.0 * (y2 - y1); double dx1 = step * (step * (step * ax + bx) + cx); double dy1 = step * (step * (step * ay + by) + cy); double dx2 = step * (step * (step * ax * 6.0 + bx * 2.0)); double dy2 = step * (step * (step * ay * 6.0 + by * 2.0)); double dx3 = step * (step * (step * ax * 6.0)); double dy3 = step * (step * (step * ay * 6.0)); double vx = x1; double vy = y1; valSize = n; values = new double[valSize]; double pvx = vx; double pvy = vy; length = 0.0; for (int i = 0; i < n; i++) { vx += dx1; vy += dy1; dx1 += dx2; dy1 += dy2; dx2 += dx3; dy2 += dy3; double lx = vx - pvx; double ly = vy - pvy; values[i] = Math.sqrt(lx * lx + ly * ly); length += values[i]; pvx = vx; pvy = vy; } valPos = 0; curLen = 0.0; prevLen = 0.0; } @Override double getNext(double dashPos) { double t = 2.0; while (curLen <= dashPos && valPos < valSize) { prevLen = curLen; curLen += lastLen = values[valPos++]; } if (curLen > dashPos) { t = (valPos - 1 + (dashPos - prevLen) / lastLen) * step; } return t; } } double length; abstract double getNext(double dashPos); } /** * BufferedPath class provides work path storing and processing */ static class BufferedPath { private static final int bufCapacity = 10; static int pointShift[] = { 2, // MOVETO 2, // LINETO 4, // QUADTO 6, // CUBICTO 0 }; // CLOSE byte[] types; double[] points; int typeSize; int pointSize; double xLast; double yLast; double xMove; double yMove; public BufferedPath() { types = new byte[bufCapacity]; points = new double[bufCapacity * 2]; } void checkBuf(int typeCount, int pointCount) { if (typeSize + typeCount > types.length) { byte tmp[] = new byte[typeSize + Math.max(bufCapacity, typeCount)]; System.arraycopy(types, 0, tmp, 0, typeSize); types = tmp; } if (pointSize + pointCount > points.length) { double tmp[] = new double[pointSize + Math.max(bufCapacity * 2, pointCount)]; System.arraycopy(points, 0, tmp, 0, pointSize); points = tmp; } } boolean isEmpty() { return typeSize == 0; } void clean() { typeSize = 0; pointSize = 0; } void moveTo(double x, double y) { checkBuf(1, 2); types[typeSize++] = GPathIterator.SEG_MOVETO; points[pointSize++] = xMove = x; points[pointSize++] = yMove = y; } void lineTo(double x, double y) { checkBuf(1, 2); types[typeSize++] = GPathIterator.SEG_LINETO; points[pointSize++] = xLast = x; points[pointSize++] = yLast = y; } void quadTo(double x1, double y1, double x2, double y2) { checkBuf(1, 4); types[typeSize++] = GPathIterator.SEG_QUADTO; points[pointSize++] = x1; points[pointSize++] = y1; points[pointSize++] = xLast = x2; points[pointSize++] = yLast = y2; } void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3) { checkBuf(1, 6); types[typeSize++] = GPathIterator.SEG_CUBICTO; points[pointSize++] = x1; points[pointSize++] = y1; points[pointSize++] = x2; points[pointSize++] = y2; points[pointSize++] = xLast = x3; points[pointSize++] = yLast = y3; } void closePath() { checkBuf(1, 0); types[typeSize++] = GPathIterator.SEG_CLOSE; } void setLast(double x, double y) { points[pointSize - 2] = xLast = x; points[pointSize - 1] = yLast = y; } void append(BufferedPath p) { checkBuf(p.typeSize, p.pointSize); System.arraycopy(p.points, 0, points, pointSize, p.pointSize); System.arraycopy(p.types, 0, types, typeSize, p.typeSize); pointSize += p.pointSize; typeSize += p.typeSize; if (pointSize >= 2) { xLast = points[pointSize - 2]; yLast = points[pointSize - 1]; } } void appendReverse(BufferedPath p) { checkBuf(p.typeSize, p.pointSize); // Skip last point, because it's the first point of the second path for (int i = p.pointSize - 2; i >= 0; i -= 2) { points[pointSize++] = p.points[i + 0]; points[pointSize++] = p.points[i + 1]; } // Skip first type, because it's always MOVETO int closeIndex = 0; for (int i = p.typeSize - 1; i >= 0; i--) { byte type = p.types[i]; if (type == GPathIterator.SEG_MOVETO) { types[closeIndex] = GPathIterator.SEG_MOVETO; types[typeSize++] = GPathIterator.SEG_CLOSE; } else { if (type == GPathIterator.SEG_CLOSE) { closeIndex = typeSize; } types[typeSize++] = type; } } if (pointSize >= 2) { xLast = points[pointSize - 2]; yLast = points[pointSize - 1]; } } void join(BufferedPath p) { // Skip MOVETO checkBuf(p.typeSize - 1, p.pointSize - 2); System.arraycopy(p.points, 2, points, pointSize, p.pointSize - 2); System.arraycopy(p.types, 1, types, typeSize, p.typeSize - 1); pointSize += p.pointSize - 2; typeSize += p.typeSize - 1; xLast = points[pointSize - 2]; yLast = points[pointSize - 1]; } void combine(BufferedPath p) { checkBuf(p.typeSize - 1, p.pointSize - 2); // Skip last point, beacause it's the first point of the second path for (int i = p.pointSize - 4; i >= 0; i -= 2) { points[pointSize++] = p.points[i + 0]; points[pointSize++] = p.points[i + 1]; } // Skip first type, beacuse it's always MOVETO for (int i = p.typeSize - 1; i >= 1; i--) { types[typeSize++] = p.types[i]; } xLast = points[pointSize - 2]; yLast = points[pointSize - 1]; } GGeneralPath createGeneralPath() { GeneralPath p = new GeneralPath(); int j = 0; for (int i = 0; i < typeSize; i++) { int type = types[i]; switch (type) { case GPathIterator.SEG_MOVETO: p.moveTo(points[j], points[j + 1]); break; case GPathIterator.SEG_LINETO: p.lineTo(points[j], points[j + 1]); break; case GPathIterator.SEG_QUADTO: p.quadTo(points[j], points[j + 1], points[j + 2], points[j + 3]); break; case GPathIterator.SEG_CUBICTO: p.curveTo(points[j], points[j + 1], points[j + 2], points[j + 3], points[j + 4], points[j + 5]); break; case GPathIterator.SEG_CLOSE: p.closePath(); break; } j += pointShift[type]; } return p; } } }