1 /*
   2  * Copyright (c) 2011, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.prism.impl.shape;
  27 
  28 
  29 import com.sun.javafx.geom.PathIterator;
  30 import com.sun.javafx.geom.Path2D;
  31 import com.sun.javafx.geom.Rectangle;
  32 import com.sun.javafx.geom.Shape;
  33 import com.sun.javafx.geom.transform.BaseTransform;
  34 import com.sun.marlin.MarlinConst;
  35 import com.sun.marlin.MarlinProperties;
  36 import com.sun.marlin.DMarlinRenderer;
  37 import com.sun.marlin.DPathConsumer2D;
  38 import com.sun.marlin.DRendererContext;
  39 import com.sun.marlin.DStroker;
  40 import com.sun.marlin.DTransformingPathConsumer2D;
  41 import com.sun.prism.BasicStroke;
  42 
  43 public final class DMarlinPrismUtils {
  44 
  45     private static final boolean FORCE_NO_AA = false;
  46 
  47     static final float UPPER_BND = Float.MAX_VALUE / 2.0f;
  48     static final float LOWER_BND = -UPPER_BND;
  49 
  50     static final boolean DO_CLIP = MarlinProperties.isDoClip();
  51     static final boolean DO_CLIP_FILL = true;
  52 
  53     static final boolean DO_TRACE_PATH = false;
  54 
  55     static final boolean DO_CLIP_RUNTIME_ENABLE = MarlinProperties.isDoClipRuntimeFlag();
  56 
  57     /**
  58      * Private constructor to prevent instantiation.
  59      */
  60     private DMarlinPrismUtils() {
  61     }
  62 
  63     private static boolean nearZero(final double num) {
  64         return Math.abs(num) < 2.0d * Math.ulp(num);
  65     }
  66 
  67     private static DPathConsumer2D initStroker(
  68             final DRendererContext rdrCtx,
  69             final BasicStroke stroke,
  70             final float lineWidth,
  71             final BaseTransform tx,
  72             final DPathConsumer2D out)
  73     {
  74         // We use strokerat so that in Stroker and Dasher we can work only
  75         // with the pre-transformation coordinates. This will repeat a lot of
  76         // computations done in the path iterator, but the alternative is to
  77         // work with transformed paths and compute untransformed coordinates
  78         // as needed. This would be faster but I do not think the complexity
  79         // of working with both untransformed and transformed coordinates in
  80         // the same code is worth it.
  81         // However, if a path's width is constant after a transformation,
  82         // we can skip all this untransforming.
  83 
  84         // As pathTo() will check transformed coordinates for invalid values
  85         // (NaN / Infinity) to ignore such points, it is necessary to apply the
  86         // transformation before the path processing.
  87         BaseTransform strokerTx = null;
  88 
  89         int dashLen = -1;
  90         boolean recycleDashes = false;
  91         double scale = 1.0d;
  92         double width = lineWidth;
  93         float[] dashes = stroke.getDashArray();
  94         double[] dashesD = null;
  95         double dashphase = stroke.getDashPhase();
  96 
  97         // Ensure converting dashes to double precision:
  98         if (dashes != null) {
  99             recycleDashes = true;
 100             dashLen = dashes.length;
 101             dashesD = rdrCtx.dasher.copyDashArray(dashes);
 102         }
 103 
 104         if ((tx != null) && !tx.isIdentity()) {
 105             final double a = tx.getMxx();
 106             final double b = tx.getMxy();
 107             final double c = tx.getMyx();
 108             final double d = tx.getMyy();
 109 
 110             // If the transform is a constant multiple of an orthogonal transformation
 111             // then every length is just multiplied by a constant, so we just
 112             // need to transform input paths to stroker and tell stroker
 113             // the scaled width. This condition is satisfied if
 114             // a*b == -c*d && a*a+c*c == b*b+d*d. In the actual check below, we
 115             // leave a bit of room for error.
 116             if (nearZero(a*b + c*d) && nearZero(a*a + c*c - (b*b + d*d))) {
 117                 scale = Math.sqrt(a*a + c*c);
 118 
 119                 if (dashesD != null) {
 120                     for (int i = 0; i < dashLen; i++) {
 121                         dashesD[i] *= scale;
 122                     }
 123                     dashphase *= scale;
 124                 }
 125                 width *= scale;
 126 
 127                 // by now strokerat == null. Input paths to
 128                 // stroker (and maybe dasher) will have the full transform tx
 129                 // applied to them and nothing will happen to the output paths.
 130             } else {
 131                 strokerTx = tx;
 132 
 133                 // by now strokerat == tx. Input paths to
 134                 // stroker (and maybe dasher) will have the full transform tx
 135                 // applied to them, then they will be normalized, and then
 136                 // the inverse of *only the non translation part of tx* will
 137                 // be applied to the normalized paths. This won't cause problems
 138                 // in stroker, because, suppose tx = T*A, where T is just the
 139                 // translation part of tx, and A is the rest. T*A has already
 140                 // been applied to Stroker/Dasher's input. Then Ainv will be
 141                 // applied. Ainv*T*A is not equal to T, but it is a translation,
 142                 // which means that none of stroker's assumptions about its
 143                 // input will be violated. After all this, A will be applied
 144                 // to stroker's output.
 145             }
 146         }
 147 
 148         // Get renderer offsets:
 149         double rdrOffX = 0.0d, rdrOffY = 0.0d;
 150 
 151         if (rdrCtx.doClip && (tx != null)) {
 152             final DMarlinRenderer renderer = (DMarlinRenderer)out;
 153             rdrOffX = renderer.getOffsetX();
 154             rdrOffY = renderer.getOffsetY();
 155         }
 156 
 157         // Prepare the pipeline:
 158         DPathConsumer2D pc = out;
 159 
 160         final DTransformingPathConsumer2D transformerPC2D = rdrCtx.transformerPC2D;
 161 
 162         if (DO_TRACE_PATH) {
 163             // trace Stroker:
 164             pc = transformerPC2D.traceStroker(pc);
 165         }
 166 
 167         if (MarlinConst.USE_SIMPLIFIER) {
 168             // Use simplifier after stroker before Renderer
 169             // to remove collinear segments (notably due to cap square)
 170             pc = rdrCtx.simplifier.init(pc);
 171         }
 172 
 173         // deltaTransformConsumer may adjust the clip rectangle:
 174         pc = transformerPC2D.deltaTransformConsumer(pc, strokerTx, rdrOffX, rdrOffY);
 175 
 176         // stroker will adjust the clip rectangle (width / miter limit):
 177         pc = rdrCtx.stroker.init(pc, width, stroke.getEndCap(),
 178                 stroke.getLineJoin(), stroke.getMiterLimit(),
 179                 scale, rdrOffX, rdrOffY);
 180 
 181         if (dashesD != null) {
 182             pc = rdrCtx.dasher.init(pc, dashesD, dashLen, dashphase, recycleDashes);
 183         } else if (rdrCtx.doClip && (stroke.getEndCap() != DStroker.CAP_BUTT)) {
 184             if (DO_TRACE_PATH) {
 185                 pc = transformerPC2D.traceClosedPathDetector(pc);
 186             }
 187 
 188             // If no dash and clip is enabled:
 189             // detect closedPaths (polygons) for caps
 190             pc = transformerPC2D.detectClosedPath(pc);
 191         }
 192         pc = transformerPC2D.inverseDeltaTransformConsumer(pc, strokerTx);
 193 
 194         if (DO_TRACE_PATH) {
 195             // trace Input:
 196             pc = transformerPC2D.traceInput(pc);
 197         }
 198         /*
 199          * Pipeline seems to be:
 200          * shape.getPathIterator(tx)
 201          * -> (inverseDeltaTransformConsumer)
 202          * -> (Dasher)
 203          * -> Stroker
 204          * -> (deltaTransformConsumer)
 205          *
 206          * -> (CollinearSimplifier) to remove redundant segments
 207          *
 208          * -> pc2d = Renderer (bounding box)
 209          */
 210         return pc;
 211     }
 212 
 213     private static DPathConsumer2D initRenderer(
 214             final DRendererContext rdrCtx,
 215             final BasicStroke stroke,
 216             final BaseTransform tx,
 217             final Rectangle clip,
 218             final int piRule,
 219             final DMarlinRenderer renderer)
 220     {
 221         if (DO_CLIP || (DO_CLIP_RUNTIME_ENABLE && MarlinProperties.isDoClipAtRuntime())) {
 222             // Define the initial clip bounds:
 223             final double[] clipRect = rdrCtx.clipRect;
 224 
 225             clipRect[0] = clip.y;
 226             clipRect[1] = clip.y + clip.height;
 227             clipRect[2] = clip.x;
 228             clipRect[3] = clip.x + clip.width;
 229 
 230             // Enable clipping:
 231             rdrCtx.doClip = true;
 232         }
 233 
 234         if (stroke != null) {
 235             renderer.init(clip.x, clip.y, clip.width, clip.height,
 236                           MarlinConst.WIND_NON_ZERO);
 237 
 238             return initStroker(rdrCtx, stroke, stroke.getLineWidth(), tx, renderer);
 239         } else {
 240             // Filler:
 241             final int oprule = (piRule == PathIterator.WIND_EVEN_ODD) ?
 242                 MarlinConst.WIND_EVEN_ODD : MarlinConst.WIND_NON_ZERO;
 243 
 244             renderer.init(clip.x, clip.y, clip.width, clip.height, oprule);
 245 
 246             DPathConsumer2D pc = renderer;
 247 
 248             final DTransformingPathConsumer2D transformerPC2D = rdrCtx.transformerPC2D;
 249 
 250             if (DO_CLIP_FILL && rdrCtx.doClip) {
 251                 double rdrOffX = renderer.getOffsetX();
 252                 double rdrOffY = renderer.getOffsetY();
 253 
 254                 if (DO_TRACE_PATH) {
 255                     // trace Filler:
 256                     pc = rdrCtx.transformerPC2D.traceFiller(pc);
 257                 }
 258                 pc = rdrCtx.transformerPC2D.pathClipper(pc, rdrOffX, rdrOffY);
 259             }
 260 
 261             if (DO_TRACE_PATH) {
 262                 // trace Input:
 263                 pc = transformerPC2D.traceInput(pc);
 264             }
 265             return pc;
 266         }
 267     }
 268 
 269     public static DMarlinRenderer setupRenderer(
 270             final DRendererContext rdrCtx,
 271             final Shape shape,
 272             final BasicStroke stroke,
 273             final BaseTransform xform,
 274             final Rectangle rclip,
 275             final boolean antialiasedShape)
 276     {
 277         // Test if transform is identity:
 278         final BaseTransform tf = ((xform != null) && !xform.isIdentity()) ? xform : null;
 279 
 280         final DMarlinRenderer r =  (!FORCE_NO_AA && antialiasedShape) ?
 281                 rdrCtx.renderer : rdrCtx.getRendererNoAA();
 282 
 283         if (shape instanceof Path2D) {
 284             final Path2D p2d = (Path2D)shape;
 285             final DPathConsumer2D pc2d = initRenderer(rdrCtx, stroke, tf, rclip, p2d.getWindingRule(), r);
 286             feedConsumer(rdrCtx, p2d, tf, pc2d);
 287         } else {
 288             final PathIterator pi = shape.getPathIterator(tf);
 289             final DPathConsumer2D pc2d = initRenderer(rdrCtx, stroke, tf, rclip, pi.getWindingRule(), r);
 290             feedConsumer(rdrCtx, pi, pc2d);
 291         }
 292         return r;
 293     }
 294 
 295     public static void strokeTo(
 296             final DRendererContext rdrCtx,
 297             final Shape shape,
 298             final BasicStroke stroke,
 299             final float lineWidth,
 300             final DPathConsumer2D out)
 301     {
 302         final DPathConsumer2D pc2d = initStroker(rdrCtx, stroke, lineWidth, null, out);
 303 
 304         if (shape instanceof Path2D) {
 305             feedConsumer(rdrCtx, (Path2D)shape, null, pc2d);
 306         } else {
 307             feedConsumer(rdrCtx, shape.getPathIterator(null), pc2d);
 308         }
 309     }
 310 
 311     private static void feedConsumer(final DRendererContext rdrCtx, final PathIterator pi,
 312                                      final DPathConsumer2D pc2d)
 313     {
 314         // mark context as DIRTY:
 315         rdrCtx.dirty = true;
 316 
 317         final float[] coords = rdrCtx.float6;
 318 
 319         // ported from DuctusRenderingEngine.feedConsumer() but simplified:
 320         // - removed skip flag = !subpathStarted
 321         // - removed pathClosed (ie subpathStarted not set to false)
 322         boolean subpathStarted = false;
 323 
 324         for (; !pi.isDone(); pi.next()) {
 325             switch (pi.currentSegment(coords)) {
 326             case PathIterator.SEG_MOVETO:
 327                 /* Checking SEG_MOVETO coordinates if they are out of the
 328                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 329                  * and Infinity values. Skipping next path segment in case of
 330                  * invalid data.
 331                  */
 332                 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 333                     coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 334                 {
 335                     pc2d.moveTo(coords[0], coords[1]);
 336                     subpathStarted = true;
 337                 }
 338                 break;
 339             case PathIterator.SEG_LINETO:
 340                 /* Checking SEG_LINETO coordinates if they are out of the
 341                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 342                  * and Infinity values. Ignoring current path segment in case
 343                  * of invalid data. If segment is skipped its endpoint
 344                  * (if valid) is used to begin new subpath.
 345                  */
 346                 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 347                     coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 348                 {
 349                     if (subpathStarted) {
 350                         pc2d.lineTo(coords[0], coords[1]);
 351                     } else {
 352                         pc2d.moveTo(coords[0], coords[1]);
 353                         subpathStarted = true;
 354                     }
 355                 }
 356                 break;
 357             case PathIterator.SEG_QUADTO:
 358                 // Quadratic curves take two points
 359                 /* Checking SEG_QUADTO coordinates if they are out of the
 360                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 361                  * and Infinity values. Ignoring current path segment in case
 362                  * of invalid endpoints's data. Equivalent to the SEG_LINETO
 363                  * if endpoint coordinates are valid but there are invalid data
 364                  * among other coordinates
 365                  */
 366                 if (coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
 367                     coords[3] < UPPER_BND && coords[3] > LOWER_BND)
 368                 {
 369                     if (subpathStarted) {
 370                         if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 371                             coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 372                         {
 373                             pc2d.quadTo(coords[0], coords[1],
 374                                         coords[2], coords[3]);
 375                         } else {
 376                             pc2d.lineTo(coords[2], coords[3]);
 377                         }
 378                     } else {
 379                         pc2d.moveTo(coords[2], coords[3]);
 380                         subpathStarted = true;
 381                     }
 382                 }
 383                 break;
 384             case PathIterator.SEG_CUBICTO:
 385                 // Cubic curves take three points
 386                 /* Checking SEG_CUBICTO coordinates if they are out of the
 387                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 388                  * and Infinity values. Ignoring current path segment in case
 389                  * of invalid endpoints's data. Equivalent to the SEG_LINETO
 390                  * if endpoint coordinates are valid but there are invalid data
 391                  * among other coordinates
 392                  */
 393                 if (coords[4] < UPPER_BND && coords[4] > LOWER_BND &&
 394                     coords[5] < UPPER_BND && coords[5] > LOWER_BND)
 395                 {
 396                     if (subpathStarted) {
 397                         if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 398                             coords[1] < UPPER_BND && coords[1] > LOWER_BND &&
 399                             coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
 400                             coords[3] < UPPER_BND && coords[3] > LOWER_BND)
 401                         {
 402                             pc2d.curveTo(coords[0], coords[1],
 403                                          coords[2], coords[3],
 404                                          coords[4], coords[5]);
 405                         } else {
 406                             pc2d.lineTo(coords[4], coords[5]);
 407                         }
 408                     } else {
 409                         pc2d.moveTo(coords[4], coords[5]);
 410                         subpathStarted = true;
 411                     }
 412                 }
 413                 break;
 414             case PathIterator.SEG_CLOSE:
 415                 if (subpathStarted) {
 416                     pc2d.closePath();
 417                     // do not set subpathStarted to false
 418                     // in case of missing moveTo() after close()
 419                 }
 420                 break;
 421             default:
 422             }
 423         }
 424         pc2d.pathDone();
 425 
 426         // mark context as CLEAN:
 427         rdrCtx.dirty = false;
 428     }
 429 
 430     private static void feedConsumer(final DRendererContext rdrCtx,
 431                                      final Path2D p2d,
 432                                      final BaseTransform xform,
 433                                      final DPathConsumer2D pc2d)
 434     {
 435         // mark context as DIRTY:
 436         rdrCtx.dirty = true;
 437 
 438         final float[] coords = rdrCtx.float6;
 439 
 440         // ported from DuctusRenderingEngine.feedConsumer() but simplified:
 441         // - removed skip flag = !subpathStarted
 442         // - removed pathClosed (ie subpathStarted not set to false)
 443         boolean subpathStarted = false;
 444 
 445         final float[] pCoords = p2d.getFloatCoordsNoClone();
 446         final byte[] pTypes = p2d.getCommandsNoClone();
 447         final int nsegs = p2d.getNumCommands();
 448 
 449         for (int i = 0, coff = 0; i < nsegs; i++) {
 450             switch (pTypes[i]) {
 451             case PathIterator.SEG_MOVETO:
 452                 if (xform == null) {
 453                     coords[0] = pCoords[coff];
 454                     coords[1] = pCoords[coff+1];
 455                 } else {
 456                     xform.transform(pCoords, coff, coords, 0, 1);
 457                 }
 458                 coff += 2;
 459                 /* Checking SEG_MOVETO coordinates if they are out of the
 460                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 461                  * and Infinity values. Skipping next path segment in case of
 462                  * invalid data.
 463                  */
 464                 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 465                     coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 466                 {
 467                     pc2d.moveTo(coords[0], coords[1]);
 468                     subpathStarted = true;
 469                 }
 470                 break;
 471             case PathIterator.SEG_LINETO:
 472                 if (xform == null) {
 473                     coords[0] = pCoords[coff];
 474                     coords[1] = pCoords[coff+1];
 475                 } else {
 476                     xform.transform(pCoords, coff, coords, 0, 1);
 477                 }
 478                 coff += 2;
 479                 /* Checking SEG_LINETO coordinates if they are out of the
 480                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 481                  * and Infinity values. Ignoring current path segment in case
 482                  * of invalid data. If segment is skipped its endpoint
 483                  * (if valid) is used to begin new subpath.
 484                  */
 485                 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 486                     coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 487                 {
 488                     if (subpathStarted) {
 489                         pc2d.lineTo(coords[0], coords[1]);
 490                     } else {
 491                         pc2d.moveTo(coords[0], coords[1]);
 492                         subpathStarted = true;
 493                     }
 494                 }
 495                 break;
 496             case PathIterator.SEG_QUADTO:
 497                 if (xform == null) {
 498                     coords[0] = pCoords[coff];
 499                     coords[1] = pCoords[coff+1];
 500                     coords[2] = pCoords[coff+2];
 501                     coords[3] = pCoords[coff+3];
 502                 } else {
 503                     xform.transform(pCoords, coff, coords, 0, 2);
 504                 }
 505                 coff += 4;
 506                 // Quadratic curves take two points
 507                 /* Checking SEG_QUADTO coordinates if they are out of the
 508                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 509                  * and Infinity values. Ignoring current path segment in case
 510                  * of invalid endpoints's data. Equivalent to the SEG_LINETO
 511                  * if endpoint coordinates are valid but there are invalid data
 512                  * among other coordinates
 513                  */
 514                 if (coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
 515                     coords[3] < UPPER_BND && coords[3] > LOWER_BND)
 516                 {
 517                     if (subpathStarted) {
 518                         if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 519                             coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 520                         {
 521                             pc2d.quadTo(coords[0], coords[1],
 522                                         coords[2], coords[3]);
 523                         } else {
 524                             pc2d.lineTo(coords[2], coords[3]);
 525                         }
 526                     } else {
 527                         pc2d.moveTo(coords[2], coords[3]);
 528                         subpathStarted = true;
 529                     }
 530                 }
 531                 break;
 532             case PathIterator.SEG_CUBICTO:
 533                 if (xform == null) {
 534                     coords[0] = pCoords[coff];
 535                     coords[1] = pCoords[coff+1];
 536                     coords[2] = pCoords[coff+2];
 537                     coords[3] = pCoords[coff+3];
 538                     coords[4] = pCoords[coff+4];
 539                     coords[5] = pCoords[coff+5];
 540                 } else {
 541                     xform.transform(pCoords, coff, coords, 0, 3);
 542                 }
 543                 coff += 6;
 544                 // Cubic curves take three points
 545                 /* Checking SEG_CUBICTO coordinates if they are out of the
 546                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 547                  * and Infinity values. Ignoring current path segment in case
 548                  * of invalid endpoints's data. Equivalent to the SEG_LINETO
 549                  * if endpoint coordinates are valid but there are invalid data
 550                  * among other coordinates
 551                  */
 552                 if (coords[4] < UPPER_BND && coords[4] > LOWER_BND &&
 553                     coords[5] < UPPER_BND && coords[5] > LOWER_BND)
 554                 {
 555                     if (subpathStarted) {
 556                         if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 557                             coords[1] < UPPER_BND && coords[1] > LOWER_BND &&
 558                             coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
 559                             coords[3] < UPPER_BND && coords[3] > LOWER_BND)
 560                         {
 561                             pc2d.curveTo(coords[0], coords[1],
 562                                          coords[2], coords[3],
 563                                          coords[4], coords[5]);
 564                         } else {
 565                             pc2d.lineTo(coords[4], coords[5]);
 566                         }
 567                     } else {
 568                         pc2d.moveTo(coords[4], coords[5]);
 569                         subpathStarted = true;
 570                     }
 571                 }
 572                 break;
 573             case PathIterator.SEG_CLOSE:
 574                 if (subpathStarted) {
 575                     pc2d.closePath();
 576                     // do not set subpathStarted to false
 577                     // in case of missing moveTo() after close()
 578                 }
 579                 break;
 580             default:
 581             }
 582         }
 583         pc2d.pathDone();
 584 
 585         // mark context as CLEAN:
 586         rdrCtx.dirty = false;
 587     }
 588 }