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.PathConsumer2D;
  30 import com.sun.javafx.geom.PathIterator;
  31 import com.sun.javafx.geom.Path2D;
  32 import com.sun.javafx.geom.Rectangle;
  33 import com.sun.javafx.geom.Shape;
  34 import com.sun.javafx.geom.transform.BaseTransform;
  35 import com.sun.marlin.MarlinConst;
  36 import com.sun.marlin.MarlinProperties;
  37 import com.sun.marlin.MarlinRenderer;
  38 import com.sun.marlin.RendererContext;
  39 import com.sun.marlin.Stroker;
  40 import com.sun.marlin.TransformingPathConsumer2D;
  41 import com.sun.prism.BasicStroke;
  42 
  43 public final class MarlinPrismUtils {
  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 MarlinPrismUtils() {
  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 PathConsumer2D initStroker(
  68             final RendererContext rdrCtx,
  69             final BasicStroke stroke,
  70             final float lineWidth,
  71             final BaseTransform tx,
  72             final PathConsumer2D 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         float scale = 1.0f;
  92         float width = lineWidth;
  93         float[] dashes = stroke.getDashArray();
  94         float dashphase = stroke.getDashPhase();
  95 
  96         if ((tx != null) && !tx.isIdentity()) {
  97             final double a = tx.getMxx();
  98             final double b = tx.getMxy();
  99             final double c = tx.getMyx();
 100             final double d = tx.getMyy();
 101 
 102             // If the transform is a constant multiple of an orthogonal transformation
 103             // then every length is just multiplied by a constant, so we just
 104             // need to transform input paths to stroker and tell stroker
 105             // the scaled width. This condition is satisfied if
 106             // a*b == -c*d && a*a+c*c == b*b+d*d. In the actual check below, we
 107             // leave a bit of room for error.
 108             if (nearZero(a*b + c*d) && nearZero(a*a + c*c - (b*b + d*d))) {
 109                 scale = (float) Math.sqrt(a*a + c*c);
 110 
 111                 if (dashes != null) {
 112                     recycleDashes = true;
 113                     dashLen = dashes.length;
 114                     dashes = rdrCtx.dasher.copyDashArray(dashes);
 115                     for (int i = 0; i < dashLen; i++) {
 116                         dashes[i] *= scale;
 117                     }
 118                     dashphase *= scale;
 119                 }
 120                 width *= scale;
 121 
 122                 // by now strokerat == null. Input paths to
 123                 // stroker (and maybe dasher) will have the full transform tx
 124                 // applied to them and nothing will happen to the output paths.
 125             } else {
 126                 strokerTx = tx;
 127 
 128                 // by now strokerat == tx. Input paths to
 129                 // stroker (and maybe dasher) will have the full transform tx
 130                 // applied to them, then they will be normalized, and then
 131                 // the inverse of *only the non translation part of tx* will
 132                 // be applied to the normalized paths. This won't cause problems
 133                 // in stroker, because, suppose tx = T*A, where T is just the
 134                 // translation part of tx, and A is the rest. T*A has already
 135                 // been applied to Stroker/Dasher's input. Then Ainv will be
 136                 // applied. Ainv*T*A is not equal to T, but it is a translation,
 137                 // which means that none of stroker's assumptions about its
 138                 // input will be violated. After all this, A will be applied
 139                 // to stroker's output.
 140             }
 141         }
 142 
 143         // Get renderer offsets:
 144         float rdrOffX = 0.0f, rdrOffY = 0.0f;
 145 
 146         if (rdrCtx.doClip && (tx != null)) {
 147             final MarlinRenderer renderer = (MarlinRenderer)out;
 148             rdrOffX = renderer.getOffsetX();
 149             rdrOffY = renderer.getOffsetY();
 150         }
 151 
 152         // Prepare the pipeline:
 153         PathConsumer2D pc = out;
 154 
 155         final TransformingPathConsumer2D transformerPC2D = rdrCtx.transformerPC2D;
 156 
 157         if (DO_TRACE_PATH) {
 158             // trace Stroker:
 159             pc = transformerPC2D.traceStroker(pc);
 160         }
 161 
 162         if (MarlinConst.USE_SIMPLIFIER) {
 163             // Use simplifier after stroker before Renderer
 164             // to remove collinear segments (notably due to cap square)
 165             pc = rdrCtx.simplifier.init(pc);
 166         }
 167 
 168         // deltaTransformConsumer may adjust the clip rectangle:
 169         pc = transformerPC2D.deltaTransformConsumer(pc, strokerTx, rdrOffX, rdrOffY);
 170 
 171         // stroker will adjust the clip rectangle (width / miter limit):
 172         pc = rdrCtx.stroker.init(pc, width, stroke.getEndCap(),
 173                 stroke.getLineJoin(), stroke.getMiterLimit(),
 174                 scale, rdrOffX, rdrOffY);
 175 
 176         if (dashes != null) {
 177             if (!recycleDashes) {
 178                 dashLen = dashes.length;
 179             }
 180             pc = rdrCtx.dasher.init(pc, dashes, dashLen, dashphase, recycleDashes);
 181         } else if (rdrCtx.doClip && (stroke.getEndCap() != Stroker.CAP_BUTT)) {
 182             if (DO_TRACE_PATH) {
 183                 pc = transformerPC2D.traceClosedPathDetector(pc);
 184             }
 185 
 186             // If no dash and clip is enabled:
 187             // detect closedPaths (polygons) for caps
 188             pc = transformerPC2D.detectClosedPath(pc);
 189         }
 190         pc = transformerPC2D.inverseDeltaTransformConsumer(pc, strokerTx);
 191 
 192         if (DO_TRACE_PATH) {
 193             // trace Input:
 194             pc = transformerPC2D.traceInput(pc);
 195         }
 196         /*
 197          * Pipeline seems to be:
 198          * shape.getPathIterator(tx)
 199          * -> (inverseDeltaTransformConsumer)
 200          * -> (Dasher)
 201          * -> Stroker
 202          * -> (deltaTransformConsumer)
 203          *
 204          * -> (CollinearSimplifier) to remove redundant segments
 205          *
 206          * -> pc2d = Renderer (bounding box)
 207          */
 208         return pc;
 209     }
 210 
 211     private static PathConsumer2D initRenderer(
 212             final RendererContext rdrCtx,
 213             final BasicStroke stroke,
 214             final BaseTransform tx,
 215             final Rectangle clip,
 216             final int piRule,
 217             final MarlinRenderer renderer)
 218     {
 219         if (DO_CLIP || (DO_CLIP_RUNTIME_ENABLE && MarlinProperties.isDoClipAtRuntime())) {
 220             // Define the initial clip bounds:
 221             final float[] clipRect = rdrCtx.clipRect;
 222 
 223             clipRect[0] = clip.y;
 224             clipRect[1] = clip.y + clip.height;
 225             clipRect[2] = clip.x;
 226             clipRect[3] = clip.x + clip.width;
 227 
 228             // Enable clipping:
 229             rdrCtx.doClip = true;
 230         }
 231 
 232         if (stroke != null) {
 233             renderer.init(clip.x, clip.y, clip.width, clip.height,
 234                           MarlinConst.WIND_NON_ZERO);
 235 
 236             return initStroker(rdrCtx, stroke, stroke.getLineWidth(), tx, renderer);
 237         } else {
 238             // Filler:
 239             final int oprule = (piRule == PathIterator.WIND_EVEN_ODD) ?
 240                 MarlinConst.WIND_EVEN_ODD : MarlinConst.WIND_NON_ZERO;
 241 
 242             renderer.init(clip.x, clip.y, clip.width, clip.height, oprule);
 243 
 244             PathConsumer2D pc = renderer;
 245 
 246             final TransformingPathConsumer2D transformerPC2D = rdrCtx.transformerPC2D;
 247 
 248             if (DO_CLIP_FILL && rdrCtx.doClip) {
 249                 float rdrOffX = renderer.getOffsetX();
 250                 float rdrOffY = renderer.getOffsetY();
 251 
 252                 if (DO_TRACE_PATH) {
 253                     // trace Filler:
 254                     pc = rdrCtx.transformerPC2D.traceFiller(pc);
 255                 }
 256                 pc = rdrCtx.transformerPC2D.pathClipper(pc, rdrOffX, rdrOffY);
 257             }
 258 
 259             if (DO_TRACE_PATH) {
 260                 // trace Input:
 261                 pc = transformerPC2D.traceInput(pc);
 262             }
 263             return pc;
 264         }
 265     }
 266 
 267     public static MarlinRenderer setupRenderer(
 268             final RendererContext rdrCtx,
 269             final Shape shape,
 270             final BasicStroke stroke,
 271             final BaseTransform xform,
 272             final Rectangle rclip,
 273             final boolean antialiasedShape)
 274     {
 275         // Test if transform is identity:
 276         final BaseTransform tf = ((xform != null) && !xform.isIdentity()) ? xform : null;
 277 
 278         final MarlinRenderer r =  (!FORCE_NO_AA && antialiasedShape) ?
 279                 rdrCtx.renderer : rdrCtx.getRendererNoAA();
 280 
 281         if (shape instanceof Path2D) {
 282             final Path2D p2d = (Path2D)shape;
 283             final PathConsumer2D pc2d = initRenderer(rdrCtx, stroke, tf, rclip, p2d.getWindingRule(), r);
 284             feedConsumer(rdrCtx, p2d, tf, pc2d);
 285         } else {
 286             final PathIterator pi = shape.getPathIterator(tf);
 287             final PathConsumer2D pc2d = initRenderer(rdrCtx, stroke, tf, rclip, pi.getWindingRule(), r);
 288             feedConsumer(rdrCtx, pi, pc2d);
 289         }
 290         return r;
 291     }
 292 
 293     public static void strokeTo(
 294             final RendererContext rdrCtx,
 295             final Shape shape,
 296             final BasicStroke stroke,
 297             final float lineWidth,
 298             final PathConsumer2D out)
 299     {
 300         final PathConsumer2D pc2d = initStroker(rdrCtx, stroke, lineWidth, null, out);
 301 
 302         if (shape instanceof Path2D) {
 303             feedConsumer(rdrCtx, (Path2D)shape, null, pc2d);
 304         } else {
 305             feedConsumer(rdrCtx, shape.getPathIterator(null), pc2d);
 306         }
 307     }
 308 
 309     private static void feedConsumer(final RendererContext rdrCtx, final PathIterator pi,
 310                                      final PathConsumer2D pc2d)
 311     {
 312         // mark context as DIRTY:
 313         rdrCtx.dirty = true;
 314 
 315         final float[] coords = rdrCtx.float6;
 316 
 317         // ported from DuctusRenderingEngine.feedConsumer() but simplified:
 318         // - removed skip flag = !subpathStarted
 319         // - removed pathClosed (ie subpathStarted not set to false)
 320         boolean subpathStarted = false;
 321 
 322         for (; !pi.isDone(); pi.next()) {
 323             switch (pi.currentSegment(coords)) {
 324             case PathIterator.SEG_MOVETO:
 325                 /* Checking SEG_MOVETO coordinates if they are out of the
 326                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 327                  * and Infinity values. Skipping next path segment in case of
 328                  * invalid data.
 329                  */
 330                 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 331                     coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 332                 {
 333                     pc2d.moveTo(coords[0], coords[1]);
 334                     subpathStarted = true;
 335                 }
 336                 break;
 337             case PathIterator.SEG_LINETO:
 338                 /* Checking SEG_LINETO coordinates if they are out of the
 339                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 340                  * and Infinity values. Ignoring current path segment in case
 341                  * of invalid data. If segment is skipped its endpoint
 342                  * (if valid) is used to begin new subpath.
 343                  */
 344                 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 345                     coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 346                 {
 347                     if (subpathStarted) {
 348                         pc2d.lineTo(coords[0], coords[1]);
 349                     } else {
 350                         pc2d.moveTo(coords[0], coords[1]);
 351                         subpathStarted = true;
 352                     }
 353                 }
 354                 break;
 355             case PathIterator.SEG_QUADTO:
 356                 // Quadratic curves take two points
 357                 /* Checking SEG_QUADTO coordinates if they are out of the
 358                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 359                  * and Infinity values. Ignoring current path segment in case
 360                  * of invalid endpoints's data. Equivalent to the SEG_LINETO
 361                  * if endpoint coordinates are valid but there are invalid data
 362                  * among other coordinates
 363                  */
 364                 if (coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
 365                     coords[3] < UPPER_BND && coords[3] > LOWER_BND)
 366                 {
 367                     if (subpathStarted) {
 368                         if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 369                             coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 370                         {
 371                             pc2d.quadTo(coords[0], coords[1],
 372                                         coords[2], coords[3]);
 373                         } else {
 374                             pc2d.lineTo(coords[2], coords[3]);
 375                         }
 376                     } else {
 377                         pc2d.moveTo(coords[2], coords[3]);
 378                         subpathStarted = true;
 379                     }
 380                 }
 381                 break;
 382             case PathIterator.SEG_CUBICTO:
 383                 // Cubic curves take three points
 384                 /* Checking SEG_CUBICTO coordinates if they are out of the
 385                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 386                  * and Infinity values. Ignoring current path segment in case
 387                  * of invalid endpoints's data. Equivalent to the SEG_LINETO
 388                  * if endpoint coordinates are valid but there are invalid data
 389                  * among other coordinates
 390                  */
 391                 if (coords[4] < UPPER_BND && coords[4] > LOWER_BND &&
 392                     coords[5] < UPPER_BND && coords[5] > LOWER_BND)
 393                 {
 394                     if (subpathStarted) {
 395                         if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 396                             coords[1] < UPPER_BND && coords[1] > LOWER_BND &&
 397                             coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
 398                             coords[3] < UPPER_BND && coords[3] > LOWER_BND)
 399                         {
 400                             pc2d.curveTo(coords[0], coords[1],
 401                                          coords[2], coords[3],
 402                                          coords[4], coords[5]);
 403                         } else {
 404                             pc2d.lineTo(coords[4], coords[5]);
 405                         }
 406                     } else {
 407                         pc2d.moveTo(coords[4], coords[5]);
 408                         subpathStarted = true;
 409                     }
 410                 }
 411                 break;
 412             case PathIterator.SEG_CLOSE:
 413                 if (subpathStarted) {
 414                     pc2d.closePath();
 415                     // do not set subpathStarted to false
 416                     // in case of missing moveTo() after close()
 417                 }
 418                 break;
 419             default:
 420             }
 421         }
 422         pc2d.pathDone();
 423 
 424         // mark context as CLEAN:
 425         rdrCtx.dirty = false;
 426     }
 427 
 428     private static void feedConsumer(final RendererContext rdrCtx,
 429                                      final Path2D p2d,
 430                                      final BaseTransform xform,
 431                                      final PathConsumer2D pc2d)
 432     {
 433         // mark context as DIRTY:
 434         rdrCtx.dirty = true;
 435 
 436         final float[] coords = rdrCtx.float6;
 437 
 438         // ported from DuctusRenderingEngine.feedConsumer() but simplified:
 439         // - removed skip flag = !subpathStarted
 440         // - removed pathClosed (ie subpathStarted not set to false)
 441         boolean subpathStarted = false;
 442 
 443         final float[] pCoords = p2d.getFloatCoordsNoClone();
 444         final byte[] pTypes = p2d.getCommandsNoClone();
 445         final int nsegs = p2d.getNumCommands();
 446 
 447         for (int i = 0, coff = 0; i < nsegs; i++) {
 448             switch (pTypes[i]) {
 449             case PathIterator.SEG_MOVETO:
 450                 if (xform == null) {
 451                     coords[0] = pCoords[coff];
 452                     coords[1] = pCoords[coff+1];
 453                 } else {
 454                     xform.transform(pCoords, coff, coords, 0, 1);
 455                 }
 456                 coff += 2;
 457                 /* Checking SEG_MOVETO coordinates if they are out of the
 458                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 459                  * and Infinity values. Skipping next path segment in case of
 460                  * invalid data.
 461                  */
 462                 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 463                     coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 464                 {
 465                     pc2d.moveTo(coords[0], coords[1]);
 466                     subpathStarted = true;
 467                 }
 468                 break;
 469             case PathIterator.SEG_LINETO:
 470                 if (xform == null) {
 471                     coords[0] = pCoords[coff];
 472                     coords[1] = pCoords[coff+1];
 473                 } else {
 474                     xform.transform(pCoords, coff, coords, 0, 1);
 475                 }
 476                 coff += 2;
 477                 /* Checking SEG_LINETO coordinates if they are out of the
 478                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 479                  * and Infinity values. Ignoring current path segment in case
 480                  * of invalid data. If segment is skipped its endpoint
 481                  * (if valid) is used to begin new subpath.
 482                  */
 483                 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 484                     coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 485                 {
 486                     if (subpathStarted) {
 487                         pc2d.lineTo(coords[0], coords[1]);
 488                     } else {
 489                         pc2d.moveTo(coords[0], coords[1]);
 490                         subpathStarted = true;
 491                     }
 492                 }
 493                 break;
 494             case PathIterator.SEG_QUADTO:
 495                 if (xform == null) {
 496                     coords[0] = pCoords[coff];
 497                     coords[1] = pCoords[coff+1];
 498                     coords[2] = pCoords[coff+2];
 499                     coords[3] = pCoords[coff+3];
 500                 } else {
 501                     xform.transform(pCoords, coff, coords, 0, 2);
 502                 }
 503                 coff += 4;
 504                 // Quadratic curves take two points
 505                 /* Checking SEG_QUADTO coordinates if they are out of the
 506                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 507                  * and Infinity values. Ignoring current path segment in case
 508                  * of invalid endpoints's data. Equivalent to the SEG_LINETO
 509                  * if endpoint coordinates are valid but there are invalid data
 510                  * among other coordinates
 511                  */
 512                 if (coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
 513                     coords[3] < UPPER_BND && coords[3] > LOWER_BND)
 514                 {
 515                     if (subpathStarted) {
 516                         if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 517                             coords[1] < UPPER_BND && coords[1] > LOWER_BND)
 518                         {
 519                             pc2d.quadTo(coords[0], coords[1],
 520                                         coords[2], coords[3]);
 521                         } else {
 522                             pc2d.lineTo(coords[2], coords[3]);
 523                         }
 524                     } else {
 525                         pc2d.moveTo(coords[2], coords[3]);
 526                         subpathStarted = true;
 527                     }
 528                 }
 529                 break;
 530             case PathIterator.SEG_CUBICTO:
 531                 if (xform == null) {
 532                     coords[0] = pCoords[coff];
 533                     coords[1] = pCoords[coff+1];
 534                     coords[2] = pCoords[coff+2];
 535                     coords[3] = pCoords[coff+3];
 536                     coords[4] = pCoords[coff+4];
 537                     coords[5] = pCoords[coff+5];
 538                 } else {
 539                     xform.transform(pCoords, coff, coords, 0, 3);
 540                 }
 541                 coff += 6;
 542                 // Cubic curves take three points
 543                 /* Checking SEG_CUBICTO coordinates if they are out of the
 544                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 545                  * and Infinity values. Ignoring current path segment in case
 546                  * of invalid endpoints's data. Equivalent to the SEG_LINETO
 547                  * if endpoint coordinates are valid but there are invalid data
 548                  * among other coordinates
 549                  */
 550                 if (coords[4] < UPPER_BND && coords[4] > LOWER_BND &&
 551                     coords[5] < UPPER_BND && coords[5] > LOWER_BND)
 552                 {
 553                     if (subpathStarted) {
 554                         if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 555                             coords[1] < UPPER_BND && coords[1] > LOWER_BND &&
 556                             coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
 557                             coords[3] < UPPER_BND && coords[3] > LOWER_BND)
 558                         {
 559                             pc2d.curveTo(coords[0], coords[1],
 560                                          coords[2], coords[3],
 561                                          coords[4], coords[5]);
 562                         } else {
 563                             pc2d.lineTo(coords[4], coords[5]);
 564                         }
 565                     } else {
 566                         pc2d.moveTo(coords[4], coords[5]);
 567                         subpathStarted = true;
 568                     }
 569                 }
 570                 break;
 571             case PathIterator.SEG_CLOSE:
 572                 if (subpathStarted) {
 573                     pc2d.closePath();
 574                     // do not set subpathStarted to false
 575                     // in case of missing moveTo() after close()
 576                 }
 577                 break;
 578             default:
 579             }
 580         }
 581         pc2d.pathDone();
 582 
 583         // mark context as CLEAN:
 584         rdrCtx.dirty = false;
 585     }
 586 }