1 /* 2 * Copyright (c) 2007, 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.marlin; 27 28 import com.sun.javafx.geom.Path2D; 29 import com.sun.javafx.geom.transform.BaseTransform; 30 import com.sun.marlin.DHelpers.IndexStack; 31 import com.sun.marlin.DHelpers.PolyStack; 32 33 public final class DTransformingPathConsumer2D { 34 35 private final DRendererContext rdrCtx; 36 37 // recycled ClosedPathDetector instance from detectClosedPath() 38 private final ClosedPathDetector cpDetector; 39 40 // recycled PathClipFilter instance from pathClipper() 41 private final PathClipFilter pathClipper; 42 43 // recycled DPathConsumer2D instance from wrapPath2D() 44 private final Path2DWrapper wp_Path2DWrapper = new Path2DWrapper(); 45 46 // recycled DPathConsumer2D instances from deltaTransformConsumer() 47 private final DeltaScaleFilter dt_DeltaScaleFilter = new DeltaScaleFilter(); 48 private final DeltaTransformFilter dt_DeltaTransformFilter = new DeltaTransformFilter(); 49 50 // recycled DPathConsumer2D instances from inverseDeltaTransformConsumer() 51 private final DeltaScaleFilter iv_DeltaScaleFilter = new DeltaScaleFilter(); 52 private final DeltaTransformFilter iv_DeltaTransformFilter = new DeltaTransformFilter(); 53 54 // recycled PathTracer instances from tracer...() methods 55 private final PathTracer tracerInput = new PathTracer("[Input]"); 56 private final PathTracer tracerCPDetector = new PathTracer("ClosedPathDetector"); 57 private final PathTracer tracerFiller = new PathTracer("Filler"); 58 private final PathTracer tracerStroker = new PathTracer("Stroker"); 59 60 DTransformingPathConsumer2D(final DRendererContext rdrCtx) { 61 // used by RendererContext 62 this.rdrCtx = rdrCtx; 63 this.cpDetector = new ClosedPathDetector(rdrCtx); 64 this.pathClipper = new PathClipFilter(rdrCtx); 65 } 66 67 public DPathConsumer2D wrapPath2D(Path2D p2d) { 68 return wp_Path2DWrapper.init(p2d); 69 } 70 71 public DPathConsumer2D traceInput(DPathConsumer2D out) { 72 return tracerInput.init(out); 73 } 74 75 public DPathConsumer2D traceClosedPathDetector(DPathConsumer2D out) { 76 return tracerCPDetector.init(out); 77 } 78 79 public DPathConsumer2D traceFiller(DPathConsumer2D out) { 80 return tracerFiller.init(out); 81 } 82 83 public DPathConsumer2D traceStroker(DPathConsumer2D out) { 84 return tracerStroker.init(out); 85 } 86 87 public DPathConsumer2D detectClosedPath(DPathConsumer2D out) { 88 return cpDetector.init(out); 89 } 90 91 public DPathConsumer2D pathClipper(DPathConsumer2D out, 92 final double rdrOffX, 93 final double rdrOffY) 94 { 95 return pathClipper.init(out, rdrOffX, rdrOffY); 96 } 97 98 public DPathConsumer2D deltaTransformConsumer(DPathConsumer2D out, 99 BaseTransform at, 100 final double rdrOffX, 101 final double rdrOffY) 102 { 103 if (at == null) { 104 return out; 105 } 106 final double mxx = at.getMxx(); 107 final double mxy = at.getMxy(); 108 final double myx = at.getMyx(); 109 final double myy = at.getMyy(); 110 111 if (mxy == 0.0d && myx == 0.0d) { 112 if (mxx == 1.0d && myy == 1.0d) { 113 return out; 114 } else { 115 // Scale only 116 if (rdrCtx.doClip) { 117 // adjust clip rectangle (ymin, ymax, xmin, xmax): 118 adjustClipOffset(rdrCtx.clipRect, rdrOffX, rdrOffY); 119 adjustClipScale(rdrCtx.clipRect, mxx, myy); 120 } 121 return dt_DeltaScaleFilter.init(out, mxx, myy); 122 } 123 } else { 124 if (rdrCtx.doClip) { 125 // adjust clip rectangle (ymin, ymax, xmin, xmax): 126 adjustClipOffset(rdrCtx.clipRect, rdrOffX, rdrOffY); 127 adjustClipInverseDelta(rdrCtx.clipRect, mxx, mxy, myx, myy); 128 } 129 return dt_DeltaTransformFilter.init(out, mxx, mxy, myx, myy); 130 } 131 } 132 133 private static void adjustClipOffset(final double[] clipRect, 134 final double rdrOffX, 135 final double rdrOffY) 136 { 137 clipRect[0] += rdrOffY; 138 clipRect[1] += rdrOffY; 139 clipRect[2] += rdrOffX; 140 clipRect[3] += rdrOffX; 141 } 142 143 private static void adjustClipScale(final double[] clipRect, 144 final double mxx, final double myy) 145 { 146 // Adjust the clipping rectangle (iv_DeltaScaleFilter): 147 clipRect[0] /= myy; 148 clipRect[1] /= myy; 149 clipRect[2] /= mxx; 150 clipRect[3] /= mxx; 151 } 152 153 private static void adjustClipInverseDelta(final double[] clipRect, 154 final double mxx, final double mxy, 155 final double myx, final double myy) 156 { 157 // Adjust the clipping rectangle (iv_DeltaTransformFilter): 158 final double det = mxx * myy - mxy * myx; 159 final double imxx = myy / det; 160 final double imxy = -mxy / det; 161 final double imyx = -myx / det; 162 final double imyy = mxx / det; 163 164 double xmin, xmax, ymin, ymax; 165 double x, y; 166 // xmin, ymin: 167 x = clipRect[2] * imxx + clipRect[0] * imxy; 168 y = clipRect[2] * imyx + clipRect[0] * imyy; 169 170 xmin = xmax = x; 171 ymin = ymax = y; 172 173 // xmax, ymin: 174 x = clipRect[3] * imxx + clipRect[0] * imxy; 175 y = clipRect[3] * imyx + clipRect[0] * imyy; 176 177 if (x < xmin) { xmin = x; } else if (x > xmax) { xmax = x; } 178 if (y < ymin) { ymin = y; } else if (y > ymax) { ymax = y; } 179 180 // xmin, ymax: 181 x = clipRect[2] * imxx + clipRect[1] * imxy; 182 y = clipRect[2] * imyx + clipRect[1] * imyy; 183 184 if (x < xmin) { xmin = x; } else if (x > xmax) { xmax = x; } 185 if (y < ymin) { ymin = y; } else if (y > ymax) { ymax = y; } 186 187 // xmax, ymax: 188 x = clipRect[3] * imxx + clipRect[1] * imxy; 189 y = clipRect[3] * imyx + clipRect[1] * imyy; 190 191 if (x < xmin) { xmin = x; } else if (x > xmax) { xmax = x; } 192 if (y < ymin) { ymin = y; } else if (y > ymax) { ymax = y; } 193 194 clipRect[0] = ymin; 195 clipRect[1] = ymax; 196 clipRect[2] = xmin; 197 clipRect[3] = xmax; 198 } 199 200 public DPathConsumer2D inverseDeltaTransformConsumer(DPathConsumer2D out, 201 BaseTransform at) 202 { 203 if (at == null) { 204 return out; 205 } 206 double mxx = at.getMxx(); 207 double mxy = at.getMxy(); 208 double myx = at.getMyx(); 209 double myy = at.getMyy(); 210 211 if (mxy == 0.0d && myx == 0.0d) { 212 if (mxx == 1.0d && myy == 1.0d) { 213 return out; 214 } else { 215 return iv_DeltaScaleFilter.init(out, 1.0d/mxx, 1.0d/myy); 216 } 217 } else { 218 final double det = mxx * myy - mxy * myx; 219 return iv_DeltaTransformFilter.init(out, 220 myy / det, 221 -mxy / det, 222 -myx / det, 223 mxx / det); 224 } 225 } 226 227 static final class DeltaScaleFilter implements DPathConsumer2D { 228 private DPathConsumer2D out; 229 private double sx, sy; 230 231 DeltaScaleFilter() {} 232 233 DeltaScaleFilter init(DPathConsumer2D out, 234 double mxx, double myy) 235 { 236 this.out = out; 237 sx = mxx; 238 sy = myy; 239 return this; // fluent API 240 } 241 242 @Override 243 public void moveTo(double x0, double y0) { 244 out.moveTo(x0 * sx, y0 * sy); 245 } 246 247 @Override 248 public void lineTo(double x1, double y1) { 249 out.lineTo(x1 * sx, y1 * sy); 250 } 251 252 @Override 253 public void quadTo(double x1, double y1, 254 double x2, double y2) 255 { 256 out.quadTo(x1 * sx, y1 * sy, 257 x2 * sx, y2 * sy); 258 } 259 260 @Override 261 public void curveTo(double x1, double y1, 262 double x2, double y2, 263 double x3, double y3) 264 { 265 out.curveTo(x1 * sx, y1 * sy, 266 x2 * sx, y2 * sy, 267 x3 * sx, y3 * sy); 268 } 269 270 @Override 271 public void closePath() { 272 out.closePath(); 273 } 274 275 @Override 276 public void pathDone() { 277 out.pathDone(); 278 } 279 } 280 281 static final class DeltaTransformFilter implements DPathConsumer2D { 282 private DPathConsumer2D out; 283 private double mxx, mxy, myx, myy; 284 285 DeltaTransformFilter() {} 286 287 DeltaTransformFilter init(DPathConsumer2D out, 288 double mxx, double mxy, 289 double myx, double myy) 290 { 291 this.out = out; 292 this.mxx = mxx; 293 this.mxy = mxy; 294 this.myx = myx; 295 this.myy = myy; 296 return this; // fluent API 297 } 298 299 @Override 300 public void moveTo(double x0, double y0) { 301 out.moveTo(x0 * mxx + y0 * mxy, 302 x0 * myx + y0 * myy); 303 } 304 305 @Override 306 public void lineTo(double x1, double y1) { 307 out.lineTo(x1 * mxx + y1 * mxy, 308 x1 * myx + y1 * myy); 309 } 310 311 @Override 312 public void quadTo(double x1, double y1, 313 double x2, double y2) 314 { 315 out.quadTo(x1 * mxx + y1 * mxy, 316 x1 * myx + y1 * myy, 317 x2 * mxx + y2 * mxy, 318 x2 * myx + y2 * myy); 319 } 320 321 @Override 322 public void curveTo(double x1, double y1, 323 double x2, double y2, 324 double x3, double y3) 325 { 326 out.curveTo(x1 * mxx + y1 * mxy, 327 x1 * myx + y1 * myy, 328 x2 * mxx + y2 * mxy, 329 x2 * myx + y2 * myy, 330 x3 * mxx + y3 * mxy, 331 x3 * myx + y3 * myy); 332 } 333 334 @Override 335 public void closePath() { 336 out.closePath(); 337 } 338 339 @Override 340 public void pathDone() { 341 out.pathDone(); 342 } 343 } 344 345 static final class Path2DWrapper implements DPathConsumer2D { 346 private Path2D p2d; 347 348 Path2DWrapper() {} 349 350 Path2DWrapper init(Path2D p2d) { 351 this.p2d = p2d; 352 return this; 353 } 354 355 @Override 356 public void moveTo(double x0, double y0) { 357 p2d.moveTo((float)x0, (float)y0); 358 } 359 360 @Override 361 public void lineTo(double x1, double y1) { 362 p2d.lineTo((float)x1, (float)y1); 363 } 364 365 @Override 366 public void closePath() { 367 p2d.closePath(); 368 } 369 370 @Override 371 public void pathDone() {} 372 373 @Override 374 public void curveTo(double x1, double y1, 375 double x2, double y2, 376 double x3, double y3) 377 { 378 p2d.curveTo((float)x1, (float)y1, (float)x2, (float)y2, 379 (float)x3, (float)y3); 380 } 381 382 @Override 383 public void quadTo(double x1, double y1, double x2, double y2) { 384 p2d.quadTo((float)x1, (float)y1, (float)x2, (float)y2); 385 } 386 } 387 388 static final class ClosedPathDetector implements DPathConsumer2D { 389 390 private final DRendererContext rdrCtx; 391 private final PolyStack stack; 392 393 private DPathConsumer2D out; 394 395 ClosedPathDetector(final DRendererContext rdrCtx) { 396 this.rdrCtx = rdrCtx; 397 this.stack = (rdrCtx.stats != null) ? 398 new PolyStack(rdrCtx, 399 rdrCtx.stats.stat_cpd_polystack_types, 400 rdrCtx.stats.stat_cpd_polystack_curves, 401 rdrCtx.stats.hist_cpd_polystack_curves, 402 rdrCtx.stats.stat_array_cpd_polystack_curves, 403 rdrCtx.stats.stat_array_cpd_polystack_types) 404 : new PolyStack(rdrCtx); 405 } 406 407 ClosedPathDetector init(DPathConsumer2D out) { 408 this.out = out; 409 return this; // fluent API 410 } 411 412 /** 413 * Disposes this instance: 414 * clean up before reusing this instance 415 */ 416 void dispose() { 417 stack.dispose(); 418 } 419 420 @Override 421 public void pathDone() { 422 // previous path is not closed: 423 finish(false); 424 out.pathDone(); 425 426 // TODO: fix possible leak if exception happened 427 // Dispose this instance: 428 dispose(); 429 } 430 431 @Override 432 public void closePath() { 433 // path is closed 434 finish(true); 435 out.closePath(); 436 } 437 438 @Override 439 public void moveTo(double x0, double y0) { 440 // previous path is not closed: 441 finish(false); 442 out.moveTo(x0, y0); 443 } 444 445 private void finish(final boolean closed) { 446 rdrCtx.closedPath = closed; 447 stack.pullAll(out); 448 } 449 450 @Override 451 public void lineTo(double x1, double y1) { 452 stack.pushLine(x1, y1); 453 } 454 455 @Override 456 public void curveTo(double x3, double y3, 457 double x2, double y2, 458 double x1, double y1) 459 { 460 stack.pushCubic(x1, y1, x2, y2, x3, y3); 461 } 462 463 @Override 464 public void quadTo(double x2, double y2, double x1, double y1) { 465 stack.pushQuad(x1, y1, x2, y2); 466 } 467 } 468 469 static final class PathClipFilter implements DPathConsumer2D { 470 471 private DPathConsumer2D out; 472 473 // Bounds of the drawing region, at pixel precision. 474 private final double[] clipRect; 475 476 private final double[] corners = new double[8]; 477 private boolean init_corners = false; 478 479 private final IndexStack stack; 480 481 // the current outcode of the current sub path 482 private int cOutCode = 0; 483 484 // the cumulated (and) outcode of the complete path 485 private int gOutCode = MarlinConst.OUTCODE_MASK_T_B_L_R; 486 487 private boolean outside = false; 488 489 // The current point OUTSIDE 490 private double cx0, cy0; 491 492 PathClipFilter(final DRendererContext rdrCtx) { 493 this.clipRect = rdrCtx.clipRect; 494 this.stack = (rdrCtx.stats != null) ? 495 new IndexStack(rdrCtx, 496 rdrCtx.stats.stat_pcf_idxstack_indices, 497 rdrCtx.stats.hist_pcf_idxstack_indices, 498 rdrCtx.stats.stat_array_pcf_idxstack_indices) 499 : new IndexStack(rdrCtx); 500 } 501 502 PathClipFilter init(final DPathConsumer2D out, 503 final double rdrOffX, 504 final double rdrOffY) 505 { 506 this.out = out; 507 508 // add a small rounding error: 509 final double margin = 1e-3d; 510 511 final double[] _clipRect = this.clipRect; 512 // Adjust the clipping rectangle with the renderer offsets 513 _clipRect[0] -= margin - rdrOffY; 514 _clipRect[1] += margin + rdrOffY; 515 _clipRect[2] -= margin - rdrOffX; 516 _clipRect[3] += margin + rdrOffX; 517 518 this.init_corners = true; 519 this.gOutCode = MarlinConst.OUTCODE_MASK_T_B_L_R; 520 521 return this; // fluent API 522 } 523 524 /** 525 * Disposes this instance: 526 * clean up before reusing this instance 527 */ 528 void dispose() { 529 stack.dispose(); 530 } 531 532 private void finishPath() { 533 if (outside) { 534 // criteria: inside or totally outside ? 535 if (gOutCode == 0) { 536 finish(); 537 } else { 538 this.outside = false; 539 stack.reset(); 540 } 541 } 542 } 543 544 private void finish() { 545 this.outside = false; 546 547 if (!stack.isEmpty()) { 548 if (init_corners) { 549 init_corners = false; 550 551 final double[] _corners = corners; 552 final double[] _clipRect = clipRect; 553 // Top Left (0): 554 _corners[0] = _clipRect[2]; 555 _corners[1] = _clipRect[0]; 556 // Bottom Left (1): 557 _corners[2] = _clipRect[2]; 558 _corners[3] = _clipRect[1]; 559 // Top right (2): 560 _corners[4] = _clipRect[3]; 561 _corners[5] = _clipRect[0]; 562 // Bottom Right (3): 563 _corners[6] = _clipRect[3]; 564 _corners[7] = _clipRect[1]; 565 } 566 stack.pullAll(corners, out); 567 } 568 out.lineTo(cx0, cy0); 569 } 570 571 @Override 572 public void pathDone() { 573 finishPath(); 574 575 out.pathDone(); 576 577 // TODO: fix possible leak if exception happened 578 // Dispose this instance: 579 dispose(); 580 } 581 582 @Override 583 public void closePath() { 584 finishPath(); 585 586 out.closePath(); 587 } 588 589 @Override 590 public void moveTo(final double x0, final double y0) { 591 finishPath(); 592 593 final int outcode = DHelpers.outcode(x0, y0, clipRect); 594 this.cOutCode = outcode; 595 this.outside = false; 596 out.moveTo(x0, y0); 597 } 598 599 @Override 600 public void lineTo(final double xe, final double ye) { 601 final int outcode0 = this.cOutCode; 602 final int outcode1 = DHelpers.outcode(xe, ye, clipRect); 603 this.cOutCode = outcode1; 604 605 final int sideCode = (outcode0 & outcode1); 606 607 // basic rejection criteria: 608 if (sideCode == 0) { 609 this.gOutCode = 0; 610 } else { 611 this.gOutCode &= sideCode; 612 // keep last point coordinate before entering the clip again: 613 this.outside = true; 614 this.cx0 = xe; 615 this.cy0 = ye; 616 617 clip(sideCode, outcode0, outcode1); 618 return; 619 } 620 if (outside) { 621 finish(); 622 } 623 // clipping disabled: 624 out.lineTo(xe, ye); 625 } 626 627 private void clip(final int sideCode, 628 final int outcode0, 629 final int outcode1) 630 { 631 // corner or cross-boundary on left or right side: 632 if ((outcode0 != outcode1) 633 && ((sideCode & MarlinConst.OUTCODE_MASK_L_R) != 0)) 634 { 635 // combine outcodes: 636 final int mergeCode = (outcode0 | outcode1); 637 final int tbCode = mergeCode & MarlinConst.OUTCODE_MASK_T_B; 638 final int lrCode = mergeCode & MarlinConst.OUTCODE_MASK_L_R; 639 final int off = (lrCode == MarlinConst.OUTCODE_LEFT) ? 0 : 2; 640 641 // add corners to outside stack: 642 switch (tbCode) { 643 case MarlinConst.OUTCODE_TOP: 644 // System.out.println("TOP "+ ((off == 0) ? "LEFT" : "RIGHT")); 645 stack.push(off); // top 646 return; 647 case MarlinConst.OUTCODE_BOTTOM: 648 // System.out.println("BOTTOM "+ ((off == 0) ? "LEFT" : "RIGHT")); 649 stack.push(off + 1); // bottom 650 return; 651 default: 652 // both TOP / BOTTOM: 653 if ((outcode0 & MarlinConst.OUTCODE_TOP) != 0) { 654 // System.out.println("TOP + BOTTOM "+ ((off == 0) ? "LEFT" : "RIGHT")); 655 // top to bottom 656 stack.push(off); // top 657 stack.push(off + 1); // bottom 658 } else { 659 // System.out.println("BOTTOM + TOP "+ ((off == 0) ? "LEFT" : "RIGHT")); 660 // bottom to top 661 stack.push(off + 1); // bottom 662 stack.push(off); // top 663 } 664 } 665 } 666 } 667 668 @Override 669 public void curveTo(final double x1, final double y1, 670 final double x2, final double y2, 671 final double xe, final double ye) 672 { 673 final int outcode0 = this.cOutCode; 674 final int outcode3 = DHelpers.outcode(xe, ye, clipRect); 675 this.cOutCode = outcode3; 676 677 int sideCode = outcode0 & outcode3; 678 679 if (sideCode == 0) { 680 this.gOutCode = 0; 681 } else { 682 sideCode &= DHelpers.outcode(x1, y1, clipRect); 683 sideCode &= DHelpers.outcode(x2, y2, clipRect); 684 this.gOutCode &= sideCode; 685 686 // basic rejection criteria: 687 if (sideCode != 0) { 688 // keep last point coordinate before entering the clip again: 689 this.outside = true; 690 this.cx0 = xe; 691 this.cy0 = ye; 692 693 clip(sideCode, outcode0, outcode3); 694 return; 695 } 696 } 697 if (outside) { 698 finish(); 699 } 700 // clipping disabled: 701 out.curveTo(x1, y1, x2, y2, xe, ye); 702 } 703 704 @Override 705 public void quadTo(final double x1, final double y1, 706 final double xe, final double ye) 707 { 708 final int outcode0 = this.cOutCode; 709 final int outcode2 = DHelpers.outcode(xe, ye, clipRect); 710 this.cOutCode = outcode2; 711 712 int sideCode = outcode0 & outcode2; 713 714 if (sideCode == 0) { 715 this.gOutCode = 0; 716 } else { 717 sideCode &= DHelpers.outcode(x1, y1, clipRect); 718 this.gOutCode &= sideCode; 719 720 // basic rejection criteria: 721 if (sideCode != 0) { 722 // keep last point coordinate before entering the clip again: 723 this.outside = true; 724 this.cx0 = xe; 725 this.cy0 = ye; 726 727 clip(sideCode, outcode0, outcode2); 728 return; 729 } 730 } 731 if (outside) { 732 finish(); 733 } 734 // clipping disabled: 735 out.quadTo(x1, y1, xe, ye); 736 } 737 } 738 739 static final class PathTracer implements DPathConsumer2D { 740 private final String prefix; 741 private DPathConsumer2D out; 742 743 PathTracer(String name) { 744 this.prefix = name + ": "; 745 } 746 747 PathTracer init(DPathConsumer2D out) { 748 this.out = out; 749 return this; // fluent API 750 } 751 752 @Override 753 public void moveTo(double x0, double y0) { 754 log("moveTo (" + x0 + ", " + y0 + ')'); 755 out.moveTo(x0, y0); 756 } 757 758 @Override 759 public void lineTo(double x1, double y1) { 760 log("lineTo (" + x1 + ", " + y1 + ')'); 761 out.lineTo(x1, y1); 762 } 763 764 @Override 765 public void curveTo(double x1, double y1, 766 double x2, double y2, 767 double x3, double y3) 768 { 769 log("curveTo P1(" + x1 + ", " + y1 + ") P2(" + x2 + ", " + y2 + ") P3(" + x3 + ", " + y3 + ')'); 770 out.curveTo(x1, y1, x2, y2, x3, y3); 771 } 772 773 @Override 774 public void quadTo(double x1, double y1, double x2, double y2) { 775 log("quadTo P1(" + x1 + ", " + y1 + ") P2(" + x2 + ", " + y2 + ')'); 776 out.quadTo(x1, y1, x2, y2); 777 } 778 779 @Override 780 public void closePath() { 781 log("closePath"); 782 out.closePath(); 783 } 784 785 @Override 786 public void pathDone() { 787 log("pathDone"); 788 out.pathDone(); 789 } 790 791 private void log(final String message) { 792 System.out.println(prefix + message); 793 } 794 } 795 }