Path: blob/master/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java
41159 views
/*1* Copyright (c) 2007, 2021, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package sun.java2d.marlin;2627import java.util.Arrays;28import sun.java2d.marlin.Helpers.PolyStack;29import sun.java2d.marlin.TransformingPathConsumer2D.CurveBasicMonotonizer;30import sun.java2d.marlin.TransformingPathConsumer2D.CurveClipSplitter;3132// TODO: some of the arithmetic here is too verbose and prone to hard to33// debug typos. We should consider making a small Point/Vector class that34// has methods like plus(Point), minus(Point), dot(Point), cross(Point)and such35final class Stroker implements DPathConsumer2D, MarlinConst {3637private static final int MOVE_TO = 0;38private static final int DRAWING_OP_TO = 1; // ie. curve, line, or quad39private static final int CLOSE = 2;4041// round join threshold = 1 subpixel42private static final double ERR_JOIN = (1.0f / MIN_SUBPIXELS);43private static final double ROUND_JOIN_THRESHOLD = ERR_JOIN * ERR_JOIN;4445// kappa = (4/3) * (SQRT(2) - 1)46private static final double C = (4.0d * (Math.sqrt(2.0d) - 1.0d) / 3.0d);4748// SQRT(2)49private static final double SQRT_2 = Math.sqrt(2.0d);5051private DPathConsumer2D out;5253private int capStyle;54private int joinStyle;5556private double lineWidth2;57private double invHalfLineWidth2Sq;5859private final double[] offset0 = new double[2];60private final double[] offset1 = new double[2];61private final double[] offset2 = new double[2];62private final double[] miter = new double[2];63private double miterLimitSq;6465private int prev;6667// The starting point of the path, and the slope there.68private double sx0, sy0, sdx, sdy;69// the current point and the slope there.70private double cx0, cy0, cdx, cdy; // c stands for current71// vectors that when added to (sx0,sy0) and (cx0,cy0) respectively yield the72// first and last points on the left parallel path. Since this path is73// parallel, it's slope at any point is parallel to the slope of the74// original path (thought they may have different directions), so these75// could be computed from sdx,sdy and cdx,cdy (and vice versa), but that76// would be error prone and hard to read, so we keep these anyway.77private double smx, smy, cmx, cmy;7879private final PolyStack reverse;8081private final double[] lp = new double[8];82private final double[] rp = new double[8];8384// per-thread renderer context85final RendererContext rdrCtx;8687// dirty curve88final Curve curve;8990// Bounds of the drawing region, at pixel precision.91private double[] clipRect;9293// the outcode of the current point94private int cOutCode = 0;9596// the outcode of the starting point97private int sOutCode = 0;9899// flag indicating if the path is opened (clipped)100private boolean opened = false;101// flag indicating if the starting point's cap is done102private boolean capStart = false;103// flag indicating to monotonize curves104private boolean monotonize;105106private boolean subdivide = false;107private final CurveClipSplitter curveSplitter;108109/**110* Constructs a <code>Stroker</code>.111* @param rdrCtx per-thread renderer context112*/113Stroker(final RendererContext rdrCtx) {114this.rdrCtx = rdrCtx;115116this.reverse = (rdrCtx.stats != null) ?117new PolyStack(rdrCtx,118rdrCtx.stats.stat_str_polystack_types,119rdrCtx.stats.stat_str_polystack_curves,120rdrCtx.stats.hist_str_polystack_curves,121rdrCtx.stats.stat_array_str_polystack_curves,122rdrCtx.stats.stat_array_str_polystack_types)123: new PolyStack(rdrCtx);124125this.curve = rdrCtx.curve;126this.curveSplitter = rdrCtx.curveClipSplitter;127}128129/**130* Inits the <code>Stroker</code>.131*132* @param pc2d an output <code>DPathConsumer2D</code>.133* @param lineWidth the desired line width in pixels134* @param capStyle the desired end cap style, one of135* <code>CAP_BUTT</code>, <code>CAP_ROUND</code> or136* <code>CAP_SQUARE</code>.137* @param joinStyle the desired line join style, one of138* <code>JOIN_MITER</code>, <code>JOIN_ROUND</code> or139* <code>JOIN_BEVEL</code>.140* @param miterLimit the desired miter limit141* @param subdivideCurves true to indicate to subdivide curves, false if dasher does142* @return this instance143*/144Stroker init(final DPathConsumer2D pc2d,145final double lineWidth,146final int capStyle,147final int joinStyle,148final double miterLimit,149final boolean subdivideCurves)150{151this.out = pc2d;152153this.lineWidth2 = lineWidth / 2.0d;154this.invHalfLineWidth2Sq = 1.0d / (2.0d * lineWidth2 * lineWidth2);155this.monotonize = subdivideCurves;156157this.capStyle = capStyle;158this.joinStyle = joinStyle;159160final double limit = miterLimit * lineWidth2;161this.miterLimitSq = limit * limit;162163this.prev = CLOSE;164165rdrCtx.stroking = 1;166167if (rdrCtx.doClip) {168// Adjust the clipping rectangle with the stroker margin (miter limit, width)169double margin = lineWidth2;170171if (capStyle == CAP_SQUARE) {172margin *= SQRT_2;173}174if ((joinStyle == JOIN_MITER) && (margin < limit)) {175margin = limit;176}177178// bounds as half-open intervals: minX <= x < maxX and minY <= y < maxY179// adjust clip rectangle (ymin, ymax, xmin, xmax):180final double[] _clipRect = rdrCtx.clipRect;181_clipRect[0] -= margin;182_clipRect[1] += margin;183_clipRect[2] -= margin;184_clipRect[3] += margin;185this.clipRect = _clipRect;186187if (MarlinConst.DO_LOG_CLIP) {188MarlinUtils.logInfo("clipRect (stroker): "189+ Arrays.toString(rdrCtx.clipRect));190}191192// initialize curve splitter here for stroker & dasher:193if (DO_CLIP_SUBDIVIDER) {194subdivide = subdivideCurves;195// adjust padded clip rectangle:196curveSplitter.init();197} else {198subdivide = false;199}200} else {201this.clipRect = null;202this.cOutCode = 0;203this.sOutCode = 0;204}205return this; // fluent API206}207208void disableClipping() {209this.clipRect = null;210this.cOutCode = 0;211this.sOutCode = 0;212}213214/**215* Disposes this stroker:216* clean up before reusing this instance217*/218void dispose() {219reverse.dispose();220221opened = false;222capStart = false;223224if (DO_CLEAN_DIRTY) {225// Force zero-fill dirty arrays:226Arrays.fill(offset0, 0.0d);227Arrays.fill(offset1, 0.0d);228Arrays.fill(offset2, 0.0d);229Arrays.fill(miter, 0.0d);230Arrays.fill(lp, 0.0d);231Arrays.fill(rp, 0.0d);232}233}234235private static void computeOffset(final double lx, final double ly,236final double w, final double[] m)237{238double len = lx*lx + ly*ly;239if (len == 0.0d) {240m[0] = 0.0d;241m[1] = 0.0d;242} else {243len = Math.sqrt(len);244m[0] = (ly * w) / len;245m[1] = -(lx * w) / len;246}247}248249// Returns true if the vectors (dx1, dy1) and (dx2, dy2) are250// clockwise (if dx1,dy1 needs to be rotated clockwise to close251// the smallest angle between it and dx2,dy2).252// This is equivalent to detecting whether a point q is on the right side253// of a line passing through points p1, p2 where p2 = p1+(dx1,dy1) and254// q = p2+(dx2,dy2), which is the same as saying p1, p2, q are in a255// clockwise order.256// NOTE: "clockwise" here assumes coordinates with 0,0 at the bottom left.257private static boolean isCW(final double dx1, final double dy1,258final double dx2, final double dy2)259{260return dx1 * dy2 <= dy1 * dx2;261}262263private void mayDrawRoundJoin(double cx, double cy,264double omx, double omy,265double mx, double my,266boolean rev)267{268if ((omx == 0.0d && omy == 0.0d) || (mx == 0.0d && my == 0.0d)) {269return;270}271272final double domx = omx - mx;273final double domy = omy - my;274final double lenSq = domx*domx + domy*domy;275276if (lenSq < ROUND_JOIN_THRESHOLD) {277return;278}279280if (rev) {281omx = -omx;282omy = -omy;283mx = -mx;284my = -my;285}286drawRoundJoin(cx, cy, omx, omy, mx, my, rev);287}288289private void drawRoundJoin(double cx, double cy,290double omx, double omy,291double mx, double my,292boolean rev)293{294// The sign of the dot product of mx,my and omx,omy is equal to the295// the sign of the cosine of ext296// (ext is the angle between omx,omy and mx,my).297final double cosext = omx * mx + omy * my;298// If it is >=0, we know that abs(ext) is <= 90 degrees, so we only299// need 1 curve to approximate the circle section that joins omx,omy300// and mx,my.301if (cosext >= 0.0d) {302drawBezApproxForArc(cx, cy, omx, omy, mx, my, rev);303} else {304// we need to split the arc into 2 arcs spanning the same angle.305// The point we want will be one of the 2 intersections of the306// perpendicular bisector of the chord (omx,omy)->(mx,my) and the307// circle. We could find this by scaling the vector308// (omx+mx, omy+my)/2 so that it has length=lineWidth2 (and thus lies309// on the circle), but that can have numerical problems when the angle310// between omx,omy and mx,my is close to 180 degrees. So we compute a311// normal of (omx,omy)-(mx,my). This will be the direction of the312// perpendicular bisector. To get one of the intersections, we just scale313// this vector that its length is lineWidth2 (this works because the314// perpendicular bisector goes through the origin). This scaling doesn't315// have numerical problems because we know that lineWidth2 divided by316// this normal's length is at least 0.5 and at most sqrt(2)/2 (because317// we know the angle of the arc is > 90 degrees).318double nx = my - omy, ny = omx - mx;319double nlen = Math.sqrt(nx*nx + ny*ny);320double scale = lineWidth2/nlen;321double mmx = nx * scale, mmy = ny * scale;322323// if (isCW(omx, omy, mx, my) != isCW(mmx, mmy, mx, my)) then we've324// computed the wrong intersection so we get the other one.325// The test above is equivalent to if (rev).326if (rev) {327mmx = -mmx;328mmy = -mmy;329}330drawBezApproxForArc(cx, cy, omx, omy, mmx, mmy, rev);331drawBezApproxForArc(cx, cy, mmx, mmy, mx, my, rev);332}333}334335// the input arc defined by omx,omy and mx,my must span <= 90 degrees.336private void drawBezApproxForArc(final double cx, final double cy,337final double omx, final double omy,338final double mx, final double my,339boolean rev)340{341final double cosext2 = (omx * mx + omy * my) * invHalfLineWidth2Sq;342343// check round off errors producing cos(ext) > 1 and a NaN below344// cos(ext) == 1 implies colinear segments and an empty join anyway345if (cosext2 >= 0.5d) {346// just return to avoid generating a flat curve:347return;348}349350// cv is the length of P1-P0 and P2-P3 divided by the radius of the arc351// (so, cv assumes the arc has radius 1). P0, P1, P2, P3 are the points that352// define the bezier curve we're computing.353// It is computed using the constraints that P1-P0 and P3-P2 are parallel354// to the arc tangents at the endpoints, and that |P1-P0|=|P3-P2|.355double cv = ((4.0d / 3.0d) * Math.sqrt(0.5d - cosext2) /356(1.0d + Math.sqrt(cosext2 + 0.5d)));357// if clockwise, we need to negate cv.358if (rev) { // rev is equivalent to isCW(omx, omy, mx, my)359cv = -cv;360}361final double x1 = cx + omx;362final double y1 = cy + omy;363final double x2 = x1 - cv * omy;364final double y2 = y1 + cv * omx;365366final double x4 = cx + mx;367final double y4 = cy + my;368final double x3 = x4 + cv * my;369final double y3 = y4 - cv * mx;370371emitCurveTo(x1, y1, x2, y2, x3, y3, x4, y4, rev);372}373374private void drawRoundCap(double cx, double cy, double mx, double my) {375final double Cmx = C * mx;376final double Cmy = C * my;377emitCurveTo(cx + mx - Cmy, cy + my + Cmx,378cx - my + Cmx, cy + mx + Cmy,379cx - my, cy + mx);380emitCurveTo(cx - my - Cmx, cy + mx - Cmy,381cx - mx - Cmy, cy - my + Cmx,382cx - mx, cy - my);383}384385// Return the intersection point of the lines (x0, y0) -> (x1, y1)386// and (x0p, y0p) -> (x1p, y1p) in m[off] and m[off+1]387private static void computeMiter(final double x0, final double y0,388final double x1, final double y1,389final double x0p, final double y0p,390final double x1p, final double y1p,391final double[] m)392{393double x10 = x1 - x0;394double y10 = y1 - y0;395double x10p = x1p - x0p;396double y10p = y1p - y0p;397398// if this is 0, the lines are parallel. If they go in the399// same direction, there is no intersection so m[off] and400// m[off+1] will contain infinity, so no miter will be drawn.401// If they go in the same direction that means that the start of the402// current segment and the end of the previous segment have the same403// tangent, in which case this method won't even be involved in404// miter drawing because it won't be called by drawMiter (because405// (mx == omx && my == omy) will be true, and drawMiter will return406// immediately).407double den = x10*y10p - x10p*y10;408double t = x10p*(y0-y0p) - y10p*(x0-x0p);409t /= den;410m[0] = x0 + t*x10;411m[1] = y0 + t*y10;412}413414// Return the intersection point of the lines (x0, y0) -> (x1, y1)415// and (x0p, y0p) -> (x1p, y1p) in m[off] and m[off+1]416private static void safeComputeMiter(final double x0, final double y0,417final double x1, final double y1,418final double x0p, final double y0p,419final double x1p, final double y1p,420final double[] m)421{422double x10 = x1 - x0;423double y10 = y1 - y0;424double x10p = x1p - x0p;425double y10p = y1p - y0p;426427// if this is 0, the lines are parallel. If they go in the428// same direction, there is no intersection so m[off] and429// m[off+1] will contain infinity, so no miter will be drawn.430// If they go in the same direction that means that the start of the431// current segment and the end of the previous segment have the same432// tangent, in which case this method won't even be involved in433// miter drawing because it won't be called by drawMiter (because434// (mx == omx && my == omy) will be true, and drawMiter will return435// immediately).436double den = x10*y10p - x10p*y10;437if (den == 0.0d) {438m[2] = (x0 + x0p) / 2.0d;439m[3] = (y0 + y0p) / 2.0d;440} else {441double t = x10p*(y0-y0p) - y10p*(x0-x0p);442t /= den;443m[2] = x0 + t*x10;444m[3] = y0 + t*y10;445}446}447448private void drawMiter(final double pdx, final double pdy,449final double x0, final double y0,450final double dx, final double dy,451double omx, double omy,452double mx, double my,453boolean rev)454{455if ((mx == omx && my == omy) ||456(pdx == 0.0d && pdy == 0.0d) ||457(dx == 0.0d && dy == 0.0d))458{459return;460}461462if (rev) {463omx = -omx;464omy = -omy;465mx = -mx;466my = -my;467}468469computeMiter((x0 - pdx) + omx, (y0 - pdy) + omy, x0 + omx, y0 + omy,470(dx + x0) + mx, (dy + y0) + my, x0 + mx, y0 + my, miter);471472final double miterX = miter[0];473final double miterY = miter[1];474double lenSq = (miterX-x0)*(miterX-x0) + (miterY-y0)*(miterY-y0);475476// If the lines are parallel, lenSq will be either NaN or +inf477// (actually, I'm not sure if the latter is possible. The important478// thing is that -inf is not possible, because lenSq is a square).479// For both of those values, the comparison below will fail and480// no miter will be drawn, which is correct.481if (lenSq < miterLimitSq) {482emitLineTo(miterX, miterY, rev);483}484}485486@Override487public void moveTo(final double x0, final double y0) {488_moveTo(x0, y0, cOutCode);489// update starting point:490this.sx0 = x0;491this.sy0 = y0;492this.sdx = 1.0d;493this.sdy = 0.0d;494this.opened = false;495this.capStart = false;496497if (clipRect != null) {498final int outcode = Helpers.outcode(x0, y0, clipRect);499this.cOutCode = outcode;500this.sOutCode = outcode;501}502}503504private void _moveTo(final double x0, final double y0,505final int outcode)506{507if (prev == MOVE_TO) {508this.cx0 = x0;509this.cy0 = y0;510} else {511if (prev == DRAWING_OP_TO) {512finish(outcode);513}514this.prev = MOVE_TO;515this.cx0 = x0;516this.cy0 = y0;517this.cdx = 1.0d;518this.cdy = 0.0d;519}520}521522@Override523public void lineTo(final double x1, final double y1) {524lineTo(x1, y1, false);525}526527private void lineTo(final double x1, final double y1,528final boolean force)529{530final int outcode0 = this.cOutCode;531532if (!force && clipRect != null) {533final int outcode1 = Helpers.outcode(x1, y1, clipRect);534535// Should clip536final int orCode = (outcode0 | outcode1);537if (orCode != 0) {538final int sideCode = outcode0 & outcode1;539540// basic rejection criteria:541if (sideCode == 0) {542// overlap clip:543if (subdivide) {544// avoid reentrance545subdivide = false;546// subdivide curve => callback with subdivided parts:547boolean ret = curveSplitter.splitLine(cx0, cy0, x1, y1,548orCode, this);549// reentrance is done:550subdivide = true;551if (ret) {552return;553}554}555// already subdivided so render it556} else {557this.cOutCode = outcode1;558_moveTo(x1, y1, outcode0);559opened = true;560return;561}562}563564this.cOutCode = outcode1;565}566567double dx = x1 - cx0;568double dy = y1 - cy0;569if (dx == 0.0d && dy == 0.0d) {570dx = 1.0d;571}572computeOffset(dx, dy, lineWidth2, offset0);573final double mx = offset0[0];574final double my = offset0[1];575576drawJoin(cdx, cdy, cx0, cy0, dx, dy, cmx, cmy, mx, my, outcode0);577578emitLineTo(cx0 + mx, cy0 + my);579emitLineTo( x1 + mx, y1 + my);580581emitLineToRev(cx0 - mx, cy0 - my);582emitLineToRev( x1 - mx, y1 - my);583584this.prev = DRAWING_OP_TO;585this.cx0 = x1;586this.cy0 = y1;587this.cdx = dx;588this.cdy = dy;589this.cmx = mx;590this.cmy = my;591}592593@Override594public void closePath() {595// distinguish empty path at all vs opened path ?596if (prev != DRAWING_OP_TO && !opened) {597if (prev == CLOSE) {598return;599}600emitMoveTo(cx0, cy0 - lineWidth2);601602this.sdx = 1.0d;603this.sdy = 0.0d;604this.cdx = 1.0d;605this.cdy = 0.0d;606607this.smx = 0.0d;608this.smy = -lineWidth2;609this.cmx = 0.0d;610this.cmy = -lineWidth2;611612finish(cOutCode);613return;614}615616// basic acceptance criteria617if ((sOutCode & cOutCode) == 0) {618if (cx0 != sx0 || cy0 != sy0) {619lineTo(sx0, sy0, true);620}621622drawJoin(cdx, cdy, cx0, cy0, sdx, sdy, cmx, cmy, smx, smy, sOutCode);623624emitLineTo(sx0 + smx, sy0 + smy);625626if (opened) {627emitLineTo(sx0 - smx, sy0 - smy);628} else {629emitMoveTo(sx0 - smx, sy0 - smy);630}631}632// Ignore caps like finish(false)633emitReverse();634635this.prev = CLOSE;636this.cx0 = sx0;637this.cy0 = sy0;638this.cOutCode = sOutCode;639640if (opened) {641// do not emit close642opened = false;643} else {644emitClose();645}646}647648private void emitReverse() {649reverse.popAll(out);650}651652@Override653public void pathDone() {654if (prev == DRAWING_OP_TO) {655finish(cOutCode);656}657658out.pathDone();659660// this shouldn't matter since this object won't be used661// after the call to this method.662this.prev = CLOSE;663664// Dispose this instance:665dispose();666}667668private void finish(final int outcode) {669// Problem: impossible to guess if the path will be closed in advance670// i.e. if caps must be drawn or not ?671// Solution: use the ClosedPathDetector before Stroker to determine672// if the path is a closed path or not673if (rdrCtx.closedPath) {674emitReverse();675} else {676if (outcode == 0) {677// current point = end's cap:678if (capStyle == CAP_ROUND) {679drawRoundCap(cx0, cy0, cmx, cmy);680} else if (capStyle == CAP_SQUARE) {681emitLineTo(cx0 - cmy + cmx, cy0 + cmx + cmy);682emitLineTo(cx0 - cmy - cmx, cy0 + cmx - cmy);683}684}685emitReverse();686687if (!capStart) {688capStart = true;689690if (sOutCode == 0) {691// starting point = initial cap:692if (capStyle == CAP_ROUND) {693drawRoundCap(sx0, sy0, -smx, -smy);694} else if (capStyle == CAP_SQUARE) {695emitLineTo(sx0 + smy - smx, sy0 - smx - smy);696emitLineTo(sx0 + smy + smx, sy0 - smx + smy);697}698}699}700}701emitClose();702}703704private void emitMoveTo(final double x0, final double y0) {705out.moveTo(x0, y0);706}707708private void emitLineTo(final double x1, final double y1) {709out.lineTo(x1, y1);710}711712private void emitLineToRev(final double x1, final double y1) {713reverse.pushLine(x1, y1);714}715716private void emitLineTo(final double x1, final double y1,717final boolean rev)718{719if (rev) {720emitLineToRev(x1, y1);721} else {722emitLineTo(x1, y1);723}724}725726private void emitQuadTo(final double x1, final double y1,727final double x2, final double y2)728{729out.quadTo(x1, y1, x2, y2);730}731732private void emitQuadToRev(final double x0, final double y0,733final double x1, final double y1)734{735reverse.pushQuad(x0, y0, x1, y1);736}737738private void emitCurveTo(final double x1, final double y1,739final double x2, final double y2,740final double x3, final double y3)741{742out.curveTo(x1, y1, x2, y2, x3, y3);743}744745private void emitCurveToRev(final double x0, final double y0,746final double x1, final double y1,747final double x2, final double y2)748{749reverse.pushCubic(x0, y0, x1, y1, x2, y2);750}751752private void emitCurveTo(final double x0, final double y0,753final double x1, final double y1,754final double x2, final double y2,755final double x3, final double y3, final boolean rev)756{757if (rev) {758reverse.pushCubic(x0, y0, x1, y1, x2, y2);759} else {760out.curveTo(x1, y1, x2, y2, x3, y3);761}762}763764private void emitClose() {765out.closePath();766}767768private void drawJoin(double pdx, double pdy,769double x0, double y0,770double dx, double dy,771double omx, double omy,772double mx, double my,773final int outcode)774{775if (prev != DRAWING_OP_TO) {776emitMoveTo(x0 + mx, y0 + my);777if (!opened) {778this.sdx = dx;779this.sdy = dy;780this.smx = mx;781this.smy = my;782}783} else {784final boolean cw = isCW(pdx, pdy, dx, dy);785if (outcode == 0) {786if (joinStyle == JOIN_MITER) {787drawMiter(pdx, pdy, x0, y0, dx, dy, omx, omy, mx, my, cw);788} else if (joinStyle == JOIN_ROUND) {789mayDrawRoundJoin(x0, y0, omx, omy, mx, my, cw);790}791}792emitLineTo(x0, y0, !cw);793}794prev = DRAWING_OP_TO;795}796797private static boolean within(final double x1, final double y1,798final double x2, final double y2,799final double err)800{801assert err > 0 : "";802// compare taxicab distance. ERR will always be small, so using803// true distance won't give much benefit804return (Helpers.within(x1, x2, err) && // we want to avoid calling Math.abs805Helpers.within(y1, y2, err)); // this is just as good.806}807808private void getLineOffsets(final double x1, final double y1,809final double x2, final double y2,810final double[] left, final double[] right)811{812computeOffset(x2 - x1, y2 - y1, lineWidth2, offset0);813final double mx = offset0[0];814final double my = offset0[1];815left[0] = x1 + mx;816left[1] = y1 + my;817left[2] = x2 + mx;818left[3] = y2 + my;819820right[0] = x1 - mx;821right[1] = y1 - my;822right[2] = x2 - mx;823right[3] = y2 - my;824}825826private int computeOffsetCubic(final double[] pts, final int off,827final double[] leftOff,828final double[] rightOff)829{830// if p1=p2 or p3=p4 it means that the derivative at the endpoint831// vanishes, which creates problems with computeOffset. Usually832// this happens when this stroker object is trying to widen833// a curve with a cusp. What happens is that curveTo splits834// the input curve at the cusp, and passes it to this function.835// because of inaccuracies in the splitting, we consider points836// equal if they're very close to each other.837final double x1 = pts[off ], y1 = pts[off + 1];838final double x2 = pts[off + 2], y2 = pts[off + 3];839final double x3 = pts[off + 4], y3 = pts[off + 5];840final double x4 = pts[off + 6], y4 = pts[off + 7];841842double dx4 = x4 - x3;843double dy4 = y4 - y3;844double dx1 = x2 - x1;845double dy1 = y2 - y1;846847// if p1 == p2 && p3 == p4: draw line from p1->p4, unless p1 == p4,848// in which case ignore if p1 == p2849final boolean p1eqp2 = within(x1, y1, x2, y2, 6.0d * Math.ulp(y2));850final boolean p3eqp4 = within(x3, y3, x4, y4, 6.0d * Math.ulp(y4));851852if (p1eqp2 && p3eqp4) {853getLineOffsets(x1, y1, x4, y4, leftOff, rightOff);854return 4;855} else if (p1eqp2) {856dx1 = x3 - x1;857dy1 = y3 - y1;858} else if (p3eqp4) {859dx4 = x4 - x2;860dy4 = y4 - y2;861}862863// if p2-p1 and p4-p3 are parallel, that must mean this curve is a line864double dotsq = (dx1 * dx4 + dy1 * dy4);865dotsq *= dotsq;866double l1sq = dx1 * dx1 + dy1 * dy1, l4sq = dx4 * dx4 + dy4 * dy4;867868if (Helpers.within(dotsq, l1sq * l4sq, 4.0d * Math.ulp(dotsq))) {869getLineOffsets(x1, y1, x4, y4, leftOff, rightOff);870return 4;871}872873// What we're trying to do in this function is to approximate an ideal874// offset curve (call it I) of the input curve B using a bezier curve Bp.875// The constraints I use to get the equations are:876//877// 1. The computed curve Bp should go through I(0) and I(1). These are878// x1p, y1p, x4p, y4p, which are p1p and p4p. We still need to find879// 4 variables: the x and y components of p2p and p3p (i.e. x2p, y2p, x3p, y3p).880//881// 2. Bp should have slope equal in absolute value to I at the endpoints. So,882// (by the way, the operator || in the comments below means "aligned with".883// It is defined on vectors, so when we say I'(0) || Bp'(0) we mean that884// vectors I'(0) and Bp'(0) are aligned, which is the same as saying885// that the tangent lines of I and Bp at 0 are parallel. Mathematically886// this means (I'(t) || Bp'(t)) <==> (I'(t) = c * Bp'(t)) where c is some887// nonzero constant.)888// I'(0) || Bp'(0) and I'(1) || Bp'(1). Obviously, I'(0) || B'(0) and889// I'(1) || B'(1); therefore, Bp'(0) || B'(0) and Bp'(1) || B'(1).890// We know that Bp'(0) || (p2p-p1p) and Bp'(1) || (p4p-p3p) and the same891// is true for any bezier curve; therefore, we get the equations892// (1) p2p = c1 * (p2-p1) + p1p893// (2) p3p = c2 * (p4-p3) + p4p894// We know p1p, p4p, p2, p1, p3, and p4; therefore, this reduces the number895// of unknowns from 4 to 2 (i.e. just c1 and c2).896// To eliminate these 2 unknowns we use the following constraint:897//898// 3. Bp(0.5) == I(0.5). Bp(0.5)=(x,y) and I(0.5)=(xi,yi), and I should note899// that I(0.5) is *the only* reason for computing dxm,dym. This gives us900// (3) Bp(0.5) = (p1p + 3 * (p2p + p3p) + p4p)/8, which is equivalent to901// (4) p2p + p3p = (Bp(0.5)*8 - p1p - p4p) / 3902// We can substitute (1) and (2) from above into (4) and we get:903// (5) c1*(p2-p1) + c2*(p4-p3) = (Bp(0.5)*8 - p1p - p4p)/3 - p1p - p4p904// which is equivalent to905// (6) c1*(p2-p1) + c2*(p4-p3) = (4/3) * (Bp(0.5) * 2 - p1p - p4p)906//907// The right side of this is a 2D vector, and we know I(0.5), which gives us908// Bp(0.5), which gives us the value of the right side.909// The left side is just a matrix vector multiplication in disguise. It is910//911// [x2-x1, x4-x3][c1]912// [y2-y1, y4-y3][c2]913// which, is equal to914// [dx1, dx4][c1]915// [dy1, dy4][c2]916// At this point we are left with a simple linear system and we solve it by917// getting the inverse of the matrix above. Then we use [c1,c2] to compute918// p2p and p3p.919920double x = (x1 + 3.0d * (x2 + x3) + x4) / 8.0d;921double y = (y1 + 3.0d * (y2 + y3) + y4) / 8.0d;922// (dxm,dym) is some tangent of B at t=0.5. This means it's equal to923// c*B'(0.5) for some constant c.924double dxm = x3 + x4 - x1 - x2, dym = y3 + y4 - y1 - y2;925926// this computes the offsets at t=0, 0.5, 1, using the property that927// for any bezier curve the vectors p2-p1 and p4-p3 are parallel to928// the (dx/dt, dy/dt) vectors at the endpoints.929computeOffset(dx1, dy1, lineWidth2, offset0);930computeOffset(dxm, dym, lineWidth2, offset1);931computeOffset(dx4, dy4, lineWidth2, offset2);932double x1p = x1 + offset0[0]; // start933double y1p = y1 + offset0[1]; // point934double xi = x + offset1[0]; // interpolation935double yi = y + offset1[1]; // point936double x4p = x4 + offset2[0]; // end937double y4p = y4 + offset2[1]; // point938939double invdet43 = 4.0d / (3.0d * (dx1 * dy4 - dy1 * dx4));940941double two_pi_m_p1_m_p4x = 2.0d * xi - x1p - x4p;942double two_pi_m_p1_m_p4y = 2.0d * yi - y1p - y4p;943double c1 = invdet43 * (dy4 * two_pi_m_p1_m_p4x - dx4 * two_pi_m_p1_m_p4y);944double c2 = invdet43 * (dx1 * two_pi_m_p1_m_p4y - dy1 * two_pi_m_p1_m_p4x);945946double x2p, y2p, x3p, y3p;947x2p = x1p + c1*dx1;948y2p = y1p + c1*dy1;949x3p = x4p + c2*dx4;950y3p = y4p + c2*dy4;951952leftOff[0] = x1p; leftOff[1] = y1p;953leftOff[2] = x2p; leftOff[3] = y2p;954leftOff[4] = x3p; leftOff[5] = y3p;955leftOff[6] = x4p; leftOff[7] = y4p;956957x1p = x1 - offset0[0]; y1p = y1 - offset0[1];958xi = xi - 2.0d * offset1[0]; yi = yi - 2.0d * offset1[1];959x4p = x4 - offset2[0]; y4p = y4 - offset2[1];960961two_pi_m_p1_m_p4x = 2.0d * xi - x1p - x4p;962two_pi_m_p1_m_p4y = 2.0d * yi - y1p - y4p;963c1 = invdet43 * (dy4 * two_pi_m_p1_m_p4x - dx4 * two_pi_m_p1_m_p4y);964c2 = invdet43 * (dx1 * two_pi_m_p1_m_p4y - dy1 * two_pi_m_p1_m_p4x);965966x2p = x1p + c1*dx1;967y2p = y1p + c1*dy1;968x3p = x4p + c2*dx4;969y3p = y4p + c2*dy4;970971rightOff[0] = x1p; rightOff[1] = y1p;972rightOff[2] = x2p; rightOff[3] = y2p;973rightOff[4] = x3p; rightOff[5] = y3p;974rightOff[6] = x4p; rightOff[7] = y4p;975return 8;976}977978// compute offset curves using bezier spline through t=0.5 (i.e.979// ComputedCurve(0.5) == IdealParallelCurve(0.5))980// return the kind of curve in the right and left arrays.981private int computeOffsetQuad(final double[] pts, final int off,982final double[] leftOff,983final double[] rightOff)984{985final double x1 = pts[off ], y1 = pts[off + 1];986final double x2 = pts[off + 2], y2 = pts[off + 3];987final double x3 = pts[off + 4], y3 = pts[off + 5];988989final double dx3 = x3 - x2;990final double dy3 = y3 - y2;991final double dx1 = x2 - x1;992final double dy1 = y2 - y1;993994// if p1=p2 or p3=p4 it means that the derivative at the endpoint995// vanishes, which creates problems with computeOffset. Usually996// this happens when this stroker object is trying to widen997// a curve with a cusp. What happens is that curveTo splits998// the input curve at the cusp, and passes it to this function.999// because of inaccuracies in the splitting, we consider points1000// equal if they're very close to each other.10011002// if p1 == p2 && p3 == p4: draw line from p1->p4, unless p1 == p4,1003// in which case ignore.1004final boolean p1eqp2 = within(x1, y1, x2, y2, 6.0d * Math.ulp(y2));1005final boolean p2eqp3 = within(x2, y2, x3, y3, 6.0d * Math.ulp(y3));10061007if (p1eqp2 || p2eqp3) {1008getLineOffsets(x1, y1, x3, y3, leftOff, rightOff);1009return 4;1010}10111012// if p2-p1 and p4-p3 are parallel, that must mean this curve is a line1013double dotsq = (dx1 * dx3 + dy1 * dy3);1014dotsq *= dotsq;1015double l1sq = dx1 * dx1 + dy1 * dy1, l3sq = dx3 * dx3 + dy3 * dy3;10161017if (Helpers.within(dotsq, l1sq * l3sq, 4.0d * Math.ulp(dotsq))) {1018getLineOffsets(x1, y1, x3, y3, leftOff, rightOff);1019return 4;1020}10211022// this computes the offsets at t=0, 0.5, 1, using the property that1023// for any bezier curve the vectors p2-p1 and p4-p3 are parallel to1024// the (dx/dt, dy/dt) vectors at the endpoints.1025computeOffset(dx1, dy1, lineWidth2, offset0);1026computeOffset(dx3, dy3, lineWidth2, offset1);10271028double x1p = x1 + offset0[0]; // start1029double y1p = y1 + offset0[1]; // point1030double x3p = x3 + offset1[0]; // end1031double y3p = y3 + offset1[1]; // point1032safeComputeMiter(x1p, y1p, x1p+dx1, y1p+dy1, x3p, y3p, x3p-dx3, y3p-dy3, leftOff);1033leftOff[0] = x1p; leftOff[1] = y1p;1034leftOff[4] = x3p; leftOff[5] = y3p;10351036x1p = x1 - offset0[0]; y1p = y1 - offset0[1];1037x3p = x3 - offset1[0]; y3p = y3 - offset1[1];1038safeComputeMiter(x1p, y1p, x1p+dx1, y1p+dy1, x3p, y3p, x3p-dx3, y3p-dy3, rightOff);1039rightOff[0] = x1p; rightOff[1] = y1p;1040rightOff[4] = x3p; rightOff[5] = y3p;1041return 6;1042}10431044@Override1045public void curveTo(final double x1, final double y1,1046final double x2, final double y2,1047final double x3, final double y3)1048{1049final int outcode0 = this.cOutCode;10501051if (clipRect != null) {1052final int outcode1 = Helpers.outcode(x1, y1, clipRect);1053final int outcode2 = Helpers.outcode(x2, y2, clipRect);1054final int outcode3 = Helpers.outcode(x3, y3, clipRect);10551056// Should clip1057final int orCode = (outcode0 | outcode1 | outcode2 | outcode3);1058if (orCode != 0) {1059final int sideCode = outcode0 & outcode1 & outcode2 & outcode3;10601061// basic rejection criteria:1062if (sideCode == 0) {1063// overlap clip:1064if (subdivide) {1065// avoid reentrance1066subdivide = false;1067// subdivide curve => callback with subdivided parts:1068boolean ret = curveSplitter.splitCurve(cx0, cy0, x1, y1,1069x2, y2, x3, y3,1070orCode, this);1071// reentrance is done:1072subdivide = true;1073if (ret) {1074return;1075}1076}1077// already subdivided so render it1078} else {1079this.cOutCode = outcode3;1080_moveTo(x3, y3, outcode0);1081opened = true;1082return;1083}1084}10851086this.cOutCode = outcode3;1087}1088_curveTo(x1, y1, x2, y2, x3, y3, outcode0);1089}10901091private void _curveTo(final double x1, final double y1,1092final double x2, final double y2,1093final double x3, final double y3,1094final int outcode0)1095{1096// need these so we can update the state at the end of this method1097double dxs = x1 - cx0;1098double dys = y1 - cy0;1099double dxf = x3 - x2;1100double dyf = y3 - y2;11011102if ((dxs == 0.0d) && (dys == 0.0d)) {1103dxs = x2 - cx0;1104dys = y2 - cy0;1105if ((dxs == 0.0d) && (dys == 0.0d)) {1106dxs = x3 - cx0;1107dys = y3 - cy0;1108}1109}1110if ((dxf == 0.0d) && (dyf == 0.0d)) {1111dxf = x3 - x1;1112dyf = y3 - y1;1113if ((dxf == 0.0d) && (dyf == 0.0d)) {1114dxf = x3 - cx0;1115dyf = y3 - cy0;1116}1117}1118if ((dxs == 0.0d) && (dys == 0.0d)) {1119// this happens if the "curve" is just a point1120// fix outcode0 for lineTo() call:1121if (clipRect != null) {1122this.cOutCode = outcode0;1123}1124lineTo(cx0, cy0);1125return;1126}11271128// if these vectors are too small, normalize them, to avoid future1129// precision problems.1130if (Math.abs(dxs) < 0.1d && Math.abs(dys) < 0.1d) {1131final double len = Math.sqrt(dxs * dxs + dys * dys);1132dxs /= len;1133dys /= len;1134}1135if (Math.abs(dxf) < 0.1d && Math.abs(dyf) < 0.1d) {1136final double len = Math.sqrt(dxf * dxf + dyf * dyf);1137dxf /= len;1138dyf /= len;1139}11401141computeOffset(dxs, dys, lineWidth2, offset0);1142drawJoin(cdx, cdy, cx0, cy0, dxs, dys, cmx, cmy, offset0[0], offset0[1], outcode0);11431144int nSplits = 0;1145final double[] mid;1146final double[] l = lp;11471148if (monotonize) {1149// monotonize curve:1150final CurveBasicMonotonizer monotonizer1151= rdrCtx.monotonizer.curve(cx0, cy0, x1, y1, x2, y2, x3, y3);11521153nSplits = monotonizer.nbSplits;1154mid = monotonizer.middle;1155} else {1156// use left instead:1157mid = l;1158mid[0] = cx0; mid[1] = cy0;1159mid[2] = x1; mid[3] = y1;1160mid[4] = x2; mid[5] = y2;1161mid[6] = x3; mid[7] = y3;1162}1163final double[] r = rp;11641165int kind = 0;1166for (int i = 0, off = 0; i <= nSplits; i++, off += 6) {1167kind = computeOffsetCubic(mid, off, l, r);11681169emitLineTo(l[0], l[1]);11701171switch(kind) {1172case 8:1173emitCurveTo(l[2], l[3], l[4], l[5], l[6], l[7]);1174emitCurveToRev(r[0], r[1], r[2], r[3], r[4], r[5]);1175break;1176case 4:1177emitLineTo(l[2], l[3]);1178emitLineToRev(r[0], r[1]);1179break;1180default:1181}1182emitLineToRev(r[kind - 2], r[kind - 1]);1183}11841185this.prev = DRAWING_OP_TO;1186this.cx0 = x3;1187this.cy0 = y3;1188this.cdx = dxf;1189this.cdy = dyf;1190this.cmx = (l[kind - 2] - r[kind - 2]) / 2.0d;1191this.cmy = (l[kind - 1] - r[kind - 1]) / 2.0d;1192}11931194@Override1195public void quadTo(final double x1, final double y1,1196final double x2, final double y2)1197{1198final int outcode0 = this.cOutCode;11991200if (clipRect != null) {1201final int outcode1 = Helpers.outcode(x1, y1, clipRect);1202final int outcode2 = Helpers.outcode(x2, y2, clipRect);12031204// Should clip1205final int orCode = (outcode0 | outcode1 | outcode2);1206if (orCode != 0) {1207final int sideCode = outcode0 & outcode1 & outcode2;12081209// basic rejection criteria:1210if (sideCode == 0) {1211// overlap clip:1212if (subdivide) {1213// avoid reentrance1214subdivide = false;1215// subdivide curve => call lineTo() with subdivided curves:1216boolean ret = curveSplitter.splitQuad(cx0, cy0, x1, y1,1217x2, y2, orCode, this);1218// reentrance is done:1219subdivide = true;1220if (ret) {1221return;1222}1223}1224// already subdivided so render it1225} else {1226this.cOutCode = outcode2;1227_moveTo(x2, y2, outcode0);1228opened = true;1229return;1230}1231}12321233this.cOutCode = outcode2;1234}1235_quadTo(x1, y1, x2, y2, outcode0);1236}12371238private void _quadTo(final double x1, final double y1,1239final double x2, final double y2,1240final int outcode0)1241{1242// need these so we can update the state at the end of this method1243double dxs = x1 - cx0;1244double dys = y1 - cy0;1245double dxf = x2 - x1;1246double dyf = y2 - y1;12471248if (((dxs == 0.0d) && (dys == 0.0d)) || ((dxf == 0.0d) && (dyf == 0.0d))) {1249dxs = dxf = x2 - cx0;1250dys = dyf = y2 - cy0;1251}1252if ((dxs == 0.0d) && (dys == 0.0d)) {1253// this happens if the "curve" is just a point1254// fix outcode0 for lineTo() call:1255if (clipRect != null) {1256this.cOutCode = outcode0;1257}1258lineTo(cx0, cy0);1259return;1260}1261// if these vectors are too small, normalize them, to avoid future1262// precision problems.1263if (Math.abs(dxs) < 0.1d && Math.abs(dys) < 0.1d) {1264final double len = Math.sqrt(dxs * dxs + dys * dys);1265dxs /= len;1266dys /= len;1267}1268if (Math.abs(dxf) < 0.1d && Math.abs(dyf) < 0.1d) {1269final double len = Math.sqrt(dxf * dxf + dyf * dyf);1270dxf /= len;1271dyf /= len;1272}1273computeOffset(dxs, dys, lineWidth2, offset0);1274drawJoin(cdx, cdy, cx0, cy0, dxs, dys, cmx, cmy, offset0[0], offset0[1], outcode0);12751276int nSplits = 0;1277final double[] mid;1278final double[] l = lp;12791280if (monotonize) {1281// monotonize quad:1282final CurveBasicMonotonizer monotonizer1283= rdrCtx.monotonizer.quad(cx0, cy0, x1, y1, x2, y2);12841285nSplits = monotonizer.nbSplits;1286mid = monotonizer.middle;1287} else {1288// use left instead:1289mid = l;1290mid[0] = cx0; mid[1] = cy0;1291mid[2] = x1; mid[3] = y1;1292mid[4] = x2; mid[5] = y2;1293}1294final double[] r = rp;12951296int kind = 0;1297for (int i = 0, off = 0; i <= nSplits; i++, off += 4) {1298kind = computeOffsetQuad(mid, off, l, r);12991300emitLineTo(l[0], l[1]);13011302switch(kind) {1303case 6:1304emitQuadTo(l[2], l[3], l[4], l[5]);1305emitQuadToRev(r[0], r[1], r[2], r[3]);1306break;1307case 4:1308emitLineTo(l[2], l[3]);1309emitLineToRev(r[0], r[1]);1310break;1311default:1312}1313emitLineToRev(r[kind - 2], r[kind - 1]);1314}13151316this.prev = DRAWING_OP_TO;1317this.cx0 = x2;1318this.cy0 = y2;1319this.cdx = dxf;1320this.cdy = dyf;1321this.cmx = (l[kind - 2] - r[kind - 2]) / 2.0d;1322this.cmy = (l[kind - 1] - r[kind - 1]) / 2.0d;1323}13241325@Override public long getNativeConsumer() {1326throw new InternalError("Stroker doesn't use a native consumer");1327}1328}132913301331