1 /*
   2  * Copyright (c) 2010, 2016, 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 javafx.scene.shape;
  27 
  28 import javafx.beans.Observable;
  29 import javafx.beans.property.BooleanProperty;
  30 import javafx.beans.property.DoubleProperty;
  31 import javafx.beans.property.ObjectProperty;
  32 import javafx.beans.property.Property;
  33 import javafx.collections.ListChangeListener.Change;
  34 import javafx.collections.ObservableList;
  35 import javafx.css.CssMetaData;
  36 import javafx.css.Styleable;
  37 import javafx.css.StyleableBooleanProperty;
  38 import javafx.css.StyleableDoubleProperty;
  39 import javafx.css.StyleableObjectProperty;
  40 import javafx.css.StyleableProperty;
  41 import javafx.scene.Node;
  42 import javafx.scene.paint.Color;
  43 import javafx.scene.paint.Paint;
  44 import java.util.ArrayList;
  45 import java.util.Collections;
  46 import java.util.List;
  47 import com.sun.javafx.util.Utils;
  48 import com.sun.javafx.beans.event.AbstractNotifyListener;
  49 import com.sun.javafx.collections.TrackableObservableList;
  50 import javafx.css.converter.BooleanConverter;
  51 import javafx.css.converter.EnumConverter;
  52 import javafx.css.converter.PaintConverter;
  53 import javafx.css.converter.SizeConverter;
  54 import com.sun.javafx.geom.Area;
  55 import com.sun.javafx.geom.BaseBounds;
  56 import com.sun.javafx.geom.PathIterator;
  57 import com.sun.javafx.geom.transform.Affine3D;
  58 import com.sun.javafx.geom.transform.BaseTransform;
  59 import com.sun.javafx.jmx.MXNodeAlgorithm;
  60 import com.sun.javafx.jmx.MXNodeAlgorithmContext;
  61 import com.sun.javafx.scene.DirtyBits;
  62 import com.sun.javafx.scene.NodeHelper;
  63 import com.sun.javafx.scene.shape.ShapeHelper;
  64 import com.sun.javafx.sg.prism.NGShape;
  65 import com.sun.javafx.tk.Toolkit;
  66 import java.lang.ref.Reference;
  67 import java.lang.ref.WeakReference;
  68 
  69 
  70 /**
  71  * The {@code Shape} class provides definitions of common properties for
  72  * objects that represent some form of geometric shape.  These properties
  73  * include:
  74  * <ul>
  75  * <li>The {@link Paint} to be applied to the fillable interior of the
  76  * shape (see {@link #setFill setFill}).
  77  * <li>The {@link Paint} to be applied to stroke the outline of the
  78  * shape (see {@link #setStroke setStroke}).
  79  * <li>The decorative properties of the stroke, including:
  80  * <ul>
  81  * <li>The width of the border stroke.
  82  * <li>Whether the border is drawn as an exterior padding to the edges
  83  * of the shape, as an interior edging that follows the inside of the border,
  84  * or as a wide path that follows along the border straddling it equally
  85  * both inside and outside (see {@link StrokeType}).
  86  * <li>Decoration styles for the joins between path segments and the
  87  * unclosed ends of paths.
  88  * <li>Dashing attributes.
  89  * </ul>
  90  * </ul>
  91  * <h4>Interaction with coordinate systems</h4>
  92  * Most nodes tend to have only integer translations applied to them and
  93  * quite often they are defined using integer coordinates as well.  For
  94  * this common case, fills of shapes with straight line edges tend to be
  95  * crisp since they line up with the cracks between pixels that fall on
  96  * integer device coordinates and thus tend to naturally cover entire pixels.
  97  * <p>
  98  * On the other hand, stroking those same shapes can often lead to fuzzy
  99  * outlines because the default stroking attributes specify both that the
 100  * default stroke width is 1.0 coordinates which often maps to exactly 1
 101  * device pixel and also that the stroke should straddle the border of the
 102  * shape, falling half on either side of the border.
 103  * Since the borders in many common shapes tend to fall directly on integer
 104  * coordinates and those integer coordinates often map precisely to integer
 105  * device locations, the borders tend to result in 50% coverage over the
 106  * pixel rows and columns on either side of the border of the shape rather
 107  * than 100% coverage on one or the other.  Thus, fills may typically be
 108  * crisp, but strokes are often fuzzy.
 109  * <p>
 110  * Two common solutions to avoid these fuzzy outlines are to use wider
 111  * strokes that cover more pixels completely - typically a stroke width of
 112  * 2.0 will achieve this if there are no scale transforms in effect - or
 113  * to specify either the {@link StrokeType#INSIDE} or {@link StrokeType#OUTSIDE}
 114  * stroke styles - which will bias the default single unit stroke onto one
 115  * of the full pixel rows or columns just inside or outside the border of
 116  * the shape.
 117  * @since JavaFX 2.0
 118  */
 119 public abstract class Shape extends Node {
 120 
 121     static {
 122         // This is used by classes in different packages to get access to
 123         // private and package private methods.
 124         ShapeHelper.setShapeAccessor(new ShapeHelper.ShapeAccessor() {
 125             @Override
 126             public void doUpdatePeer(Node node) {
 127                 ((Shape) node).doUpdatePeer();
 128             }
 129 
 130             @Override
 131             public void doMarkDirty(Node node, DirtyBits dirtyBit) {
 132                 ((Shape) node).doMarkDirty(dirtyBit);
 133             }
 134 
 135             @Override
 136             public Paint doCssGetFillInitialValue(Shape shape) {
 137                 return shape.doCssGetFillInitialValue();
 138             }
 139 
 140             @Override
 141             public Paint doCssGetStrokeInitialValue(Shape shape) {
 142                 return shape.doCssGetStrokeInitialValue();
 143             }
 144 
 145             @Override
 146             public NGShape.Mode getMode(Shape shape) {
 147                 return shape.getMode();
 148             }
 149 
 150             @Override
 151             public void setMode(Shape shape, NGShape.Mode mode) {
 152                 shape.setMode(mode);
 153             }
 154 
 155             @Override
 156             public void setShapeChangeListener(Shape shape, Runnable listener) {
 157                 shape.setShapeChangeListener(listener);
 158             }
 159         });
 160     }
 161 
 162     /**
 163      * Creates an empty instance of Shape.
 164      */
 165     public Shape() {
 166     }
 167 
 168     StrokeLineJoin convertLineJoin(StrokeLineJoin t) {
 169         return t;
 170     }
 171 
 172     public final void setStrokeType(StrokeType value) {
 173         strokeTypeProperty().set(value);
 174     }
 175 
 176     public final StrokeType getStrokeType() {
 177         return (strokeAttributes == null) ? DEFAULT_STROKE_TYPE
 178                                           : strokeAttributes.getType();
 179     }
 180 
 181     /**
 182      * Defines the direction (inside, centered, or outside) that the strokeWidth
 183      * is applied to the boundary of the shape.
 184      *
 185      * <p>
 186      * The image shows a shape without stroke and with a thick stroke applied
 187      * inside, centered and outside.
 188      * </p><p>
 189      * <img src="doc-files/stroketype.png"/>
 190      * </p>
 191      *
 192      * @see StrokeType
 193      * @defaultValue CENTERED
 194      */
 195     public final ObjectProperty<StrokeType> strokeTypeProperty() {
 196         return getStrokeAttributes().typeProperty();
 197     }
 198 
 199     public final void setStrokeWidth(double value) {
 200         strokeWidthProperty().set(value);
 201     }
 202 
 203     public final double getStrokeWidth() {
 204         return (strokeAttributes == null) ? DEFAULT_STROKE_WIDTH
 205                                           : strokeAttributes.getWidth();
 206     }
 207 
 208     /**
 209      * Defines a square pen line width. A value of 0.0 specifies a hairline
 210      * stroke. A value of less than 0.0 will be treated as 0.0.
 211      *
 212      * @defaultValue 1.0
 213      */
 214     public final DoubleProperty strokeWidthProperty() {
 215         return getStrokeAttributes().widthProperty();
 216     }
 217 
 218     public final void setStrokeLineJoin(StrokeLineJoin value) {
 219         strokeLineJoinProperty().set(value);
 220     }
 221 
 222     public final StrokeLineJoin getStrokeLineJoin() {
 223         return (strokeAttributes == null)
 224                 ? DEFAULT_STROKE_LINE_JOIN
 225                 : strokeAttributes.getLineJoin();
 226     }
 227 
 228     /**
 229      * Defines the decoration applied where path segments meet.
 230      * The value must have one of the following values:
 231      * {@code StrokeLineJoin.MITER}, {@code StrokeLineJoin.BEVEL},
 232      * and {@code StrokeLineJoin.ROUND}. The image shows a shape
 233      * using the values in the mentioned order.
 234      * </p><p>
 235      * <img src="doc-files/strokelinejoin.png"/>
 236      * </p>
 237      *
 238      * @see StrokeLineJoin
 239      * @defaultValue MITER
 240      */
 241     public final ObjectProperty<StrokeLineJoin> strokeLineJoinProperty() {
 242         return getStrokeAttributes().lineJoinProperty();
 243     }
 244 
 245     public final void setStrokeLineCap(StrokeLineCap value) {
 246         strokeLineCapProperty().set(value);
 247     }
 248 
 249     public final StrokeLineCap getStrokeLineCap() {
 250         return (strokeAttributes == null) ? DEFAULT_STROKE_LINE_CAP
 251                                           : strokeAttributes.getLineCap();
 252     }
 253 
 254     /**
 255      * The end cap style of this {@code Shape} as one of the following
 256      * values that define possible end cap styles:
 257      * {@code StrokeLineCap.BUTT}, {@code StrokeLineCap.ROUND},
 258      * and  {@code StrokeLineCap.SQUARE}. The image shows a line
 259      * using the values in the mentioned order.
 260      * </p><p>
 261      * <img src="doc-files/strokelinecap.png"/>
 262      * </p>
 263      *
 264      * @see StrokeLineCap
 265      * @defaultValue SQUARE
 266      */
 267     public final ObjectProperty<StrokeLineCap> strokeLineCapProperty() {
 268         return getStrokeAttributes().lineCapProperty();
 269     }
 270 
 271     public final void setStrokeMiterLimit(double value) {
 272         strokeMiterLimitProperty().set(value);
 273     }
 274 
 275     public final double getStrokeMiterLimit() {
 276         return (strokeAttributes == null) ? DEFAULT_STROKE_MITER_LIMIT
 277                                           : strokeAttributes.getMiterLimit();
 278     }
 279 
 280     /**
 281      * Defines the limit for the {@code StrokeLineJoin.MITER} line join style.
 282      * A value of less than 1.0 will be treated as 1.0.
 283      *
 284      * <p>
 285      * The image demonstrates the behavior. Miter length ({@code A}) is computed
 286      * as the distance of the most inside point to the most outside point of
 287      * the joint, with the stroke width as a unit. If the miter length is bigger
 288      * than the given miter limit, the miter is cut at the edge of the shape
 289      * ({@code B}). For the situation in the image it means that the miter
 290      * will be cut at {@code B} for limit values less than {@code 4.65}.
 291      * </p><p>
 292      * <img src="doc-files/strokemiterlimit.png"/>
 293      * </p>
 294      *
 295      * @defaultValue 10.0
 296      */
 297     public final DoubleProperty strokeMiterLimitProperty() {
 298         return getStrokeAttributes().miterLimitProperty();
 299     }
 300 
 301     public final void setStrokeDashOffset(double value) {
 302         strokeDashOffsetProperty().set(value);
 303     }
 304 
 305     public final double getStrokeDashOffset() {
 306         return (strokeAttributes == null) ? DEFAULT_STROKE_DASH_OFFSET
 307                                           : strokeAttributes.getDashOffset();
 308     }
 309 
 310     /**
 311      * Defines a distance specified in user coordinates that represents
 312      * an offset into the dashing pattern. In other words, the dash phase
 313      * defines the point in the dashing pattern that will correspond
 314      * to the beginning of the stroke.
 315      *
 316      * <p>
 317      * The image shows a stroke with dash array {@code [25, 20, 5, 20]} and
 318      * a stroke with the same pattern and offset {@code 45} which shifts
 319      * the pattern about the length of the first dash segment and
 320      * the following space.
 321      * </p><p>
 322      * <img src="doc-files/strokedashoffset.png"/>
 323      * </p>
 324      *
 325      * @defaultValue 0
 326      */
 327     public final DoubleProperty strokeDashOffsetProperty() {
 328         return getStrokeAttributes().dashOffsetProperty();
 329     }
 330 
 331     /**
 332      * Defines the array representing the lengths of the dash segments.
 333      * Alternate entries in the array represent the user space lengths
 334      * of the opaque and transparent segments of the dashes.
 335      * As the pen moves along the outline of the {@code Shape} to be stroked,
 336      * the user space distance that the pen travels is accumulated.
 337      * The distance value is used to index into the dash array.
 338      * The pen is opaque when its current cumulative distance maps
 339      * to an even element of the dash array (counting from {@code 0}) and
 340      * transparent otherwise.
 341      * <p>
 342      * An empty strokeDashArray indicates a solid line with no spaces.
 343      * An odd length strokeDashArray behaves the same as an even length
 344      * array constructed by implicitly repeating the indicated odd length
 345      * array twice in succession ({@code [20, 5, 15]} behaves as if it
 346      * were {@code [20, 5, 15, 20, 5, 15]}).
 347      * <p>
 348      * Note that each dash segment will be capped by the decoration specified
 349      * by the current stroke line cap.
 350      *
 351      * <p>
 352      * The image shows a shape with stroke dash array {@code [25, 20, 5, 20]}
 353      * and 3 different values for the stroke line cap:
 354      * {@code StrokeLineCap.BUTT}, {@code StrokeLineCap.SQUARE} (the default),
 355      * and {@code StrokeLineCap.ROUND}
 356      * </p><p>
 357      * <img src="doc-files/strokedasharray.png"/>
 358      * </p>
 359      *
 360      * @defaultValue empty
 361      */
 362     public final ObservableList<Double> getStrokeDashArray() {
 363         return getStrokeAttributes().dashArrayProperty();
 364     }
 365 
 366     private NGShape.Mode computeMode() {
 367         if (getFill() != null && getStroke() != null) {
 368             return NGShape.Mode.STROKE_FILL;
 369         } else if (getFill() != null) {
 370             return NGShape.Mode.FILL;
 371         } else if (getStroke() != null) {
 372             return NGShape.Mode.STROKE;
 373         } else {
 374             return NGShape.Mode.EMPTY;
 375         }
 376     }
 377 
 378     NGShape.Mode getMode() {
 379         return mode;
 380     }
 381 
 382     void setMode(NGShape.Mode mode) {
 383         mode = mode;
 384     }
 385 
 386     private NGShape.Mode mode = NGShape.Mode.FILL;
 387 
 388     private void checkModeChanged() {
 389         NGShape.Mode newMode = computeMode();
 390         if (mode != newMode) {
 391             mode = newMode;
 392 
 393             NodeHelper.markDirty(this, DirtyBits.SHAPE_MODE);
 394             impl_geomChanged();
 395         }
 396     }
 397 
 398     /**
 399      * Defines parameters to fill the interior of an {@code Shape}
 400      * using the settings of the {@code Paint} context.
 401      * The default value is {@code Color.BLACK} for all shapes except
 402      * Line, Polyline, and Path. The default value is {@code null} for
 403      * those shapes.
 404      */
 405     private ObjectProperty<Paint> fill;
 406 
 407 
 408     public final void setFill(Paint value) {
 409         fillProperty().set(value);
 410     }
 411 
 412     public final Paint getFill() {
 413         return fill == null ? Color.BLACK : fill.get();
 414     }
 415 
 416     Paint old_fill;
 417     public final ObjectProperty<Paint> fillProperty() {
 418         if (fill == null) {
 419             fill = new StyleableObjectProperty<Paint>(Color.BLACK) {
 420 
 421                 boolean needsListener = false;
 422 
 423                 @Override public void invalidated() {
 424 
 425                     Paint _fill = get();
 426 
 427                     if (needsListener) {
 428                         Toolkit.getPaintAccessor().
 429                                 removeListener(old_fill, platformImageChangeListener);
 430                     }
 431                     needsListener = _fill != null &&
 432                             Toolkit.getPaintAccessor().isMutable(_fill);
 433                     old_fill = _fill;
 434 
 435                     if (needsListener) {
 436                         Toolkit.getPaintAccessor().
 437                                 addListener(_fill, platformImageChangeListener);
 438                     }
 439 
 440                     NodeHelper.markDirty(Shape.this, DirtyBits.SHAPE_FILL);
 441                     checkModeChanged();
 442                 }
 443 
 444                 @Override
 445                 public CssMetaData<Shape,Paint> getCssMetaData() {
 446                     return StyleableProperties.FILL;
 447                 }
 448 
 449                 @Override
 450                 public Object getBean() {
 451                     return Shape.this;
 452                 }
 453 
 454                 @Override
 455                 public String getName() {
 456                     return "fill";
 457                 }
 458             };
 459         }
 460         return fill;
 461     }
 462 
 463     /**
 464      * Defines parameters of a stroke that is drawn around the outline of
 465      * a {@code Shape} using the settings of the specified {@code Paint}.
 466      * The default value is {@code null} for all shapes except
 467      * Line, Polyline, and Path. The default value is {@code Color.BLACK} for
 468      * those shapes.
 469      */
 470     private ObjectProperty<Paint> stroke;
 471 
 472 
 473     public final void setStroke(Paint value) {
 474         strokeProperty().set(value);
 475     }
 476 
 477     private final AbstractNotifyListener platformImageChangeListener =
 478             new AbstractNotifyListener() {
 479         @Override
 480         public void invalidated(Observable valueModel) {
 481             NodeHelper.markDirty(Shape.this, DirtyBits.SHAPE_FILL);
 482             NodeHelper.markDirty(Shape.this, DirtyBits.SHAPE_STROKE);
 483             impl_geomChanged();
 484             checkModeChanged();
 485         }
 486     };
 487 
 488     public final Paint getStroke() {
 489         return stroke == null ? null : stroke.get();
 490     }
 491 
 492     Paint old_stroke;
 493     public final ObjectProperty<Paint> strokeProperty() {
 494         if (stroke == null) {
 495             stroke = new StyleableObjectProperty<Paint>() {
 496 
 497                 boolean needsListener = false;
 498 
 499                 @Override public void invalidated() {
 500 
 501                     Paint _stroke = get();
 502 
 503                     if (needsListener) {
 504                         Toolkit.getPaintAccessor().
 505                                 removeListener(old_stroke, platformImageChangeListener);
 506                     }
 507                     needsListener = _stroke != null &&
 508                             Toolkit.getPaintAccessor().isMutable(_stroke);
 509                     old_stroke = _stroke;
 510 
 511                     if (needsListener) {
 512                         Toolkit.getPaintAccessor().
 513                                 addListener(_stroke, platformImageChangeListener);
 514                     }
 515 
 516                     NodeHelper.markDirty(Shape.this, DirtyBits.SHAPE_STROKE);
 517                     checkModeChanged();
 518                 }
 519 
 520                 @Override
 521                 public CssMetaData<Shape,Paint> getCssMetaData() {
 522                     return StyleableProperties.STROKE;
 523                 }
 524 
 525                 @Override
 526                 public Object getBean() {
 527                     return Shape.this;
 528                 }
 529 
 530                 @Override
 531                 public String getName() {
 532                     return "stroke";
 533                 }
 534             };
 535         }
 536         return stroke;
 537     }
 538 
 539     /**
 540      * Defines whether antialiasing hints are used or not for this {@code Shape}.
 541      * If the value equals true the rendering hints are applied.
 542      *
 543      * @defaultValue true
 544      */
 545     private BooleanProperty smooth;
 546 
 547 
 548     public final void setSmooth(boolean value) {
 549         smoothProperty().set(value);
 550     }
 551 
 552     public final boolean isSmooth() {
 553         return smooth == null ? true : smooth.get();
 554     }
 555 
 556     public final BooleanProperty smoothProperty() {
 557         if (smooth == null) {
 558             smooth = new StyleableBooleanProperty(true) {
 559 
 560                 @Override
 561                 public void invalidated() {
 562                     NodeHelper.markDirty(Shape.this, DirtyBits.NODE_SMOOTH);
 563                 }
 564 
 565                 @Override
 566                 public CssMetaData<Shape,Boolean> getCssMetaData() {
 567                     return StyleableProperties.SMOOTH;
 568                 }
 569 
 570                 @Override
 571                 public Object getBean() {
 572                     return Shape.this;
 573                 }
 574 
 575                 @Override
 576                 public String getName() {
 577                     return "smooth";
 578                 }
 579             };
 580         }
 581         return smooth;
 582     }
 583 
 584     /***************************************************************************
 585      *                                                                         *
 586      *                         Stylesheet Handling                             *
 587      *                                                                         *
 588      **************************************************************************/
 589 
 590     /*
 591      * Some sub-class of Shape, such as {@link Line}, override the
 592      * default value for the {@link Shape#fill} property. This allows
 593      * CSS to get the correct initial value.
 594      *
 595      * Note: This method MUST only be called via its accessor method.
 596      */
 597     private Paint doCssGetFillInitialValue() {
 598         return Color.BLACK;
 599     }
 600 
 601     /*
 602      * Some sub-class of Shape, such as {@link Line}, override the
 603      * default value for the {@link Shape#stroke} property. This allows
 604      * CSS to get the correct initial value.
 605      *
 606      * Note: This method MUST only be called via its accessor method.
 607      */
 608     private Paint doCssGetStrokeInitialValue() {
 609         return null;
 610     }
 611 
 612 
 613     /*
 614      * Super-lazy instantiation pattern from Bill Pugh.
 615      */
 616      private static class StyleableProperties {
 617 
 618         /**
 619         * @css -fx-fill: <a href="../doc-files/cssref.html#typepaint">&lt;paint&gt;</a>
 620         * @see Shape#fill
 621         */
 622         private static final CssMetaData<Shape,Paint> FILL =
 623             new CssMetaData<Shape,Paint>("-fx-fill",
 624                 PaintConverter.getInstance(), Color.BLACK) {
 625 
 626             @Override
 627             public boolean isSettable(Shape node) {
 628                 return node.fill == null || !node.fill.isBound();
 629             }
 630 
 631             @Override
 632             public StyleableProperty<Paint> getStyleableProperty(Shape node) {
 633                 return (StyleableProperty<Paint>)node.fillProperty();
 634             }
 635 
 636             @Override
 637             public Paint getInitialValue(Shape node) {
 638                 // Some shapes have a different initial value for fill.
 639                 // Give a way to have them return the correct initial value.
 640                 return ShapeHelper.cssGetFillInitialValue(node);
 641             }
 642 
 643         };
 644 
 645         /**
 646         * @css -fx-smooth: <a href="../doc-files/cssref.html#typeboolean">&lt;boolean&gt;</a>
 647         * @see Shape#smooth
 648         */
 649         private static final CssMetaData<Shape,Boolean> SMOOTH =
 650             new CssMetaData<Shape,Boolean>("-fx-smooth",
 651                 BooleanConverter.getInstance(), Boolean.TRUE) {
 652 
 653             @Override
 654             public boolean isSettable(Shape node) {
 655                 return node.smooth == null || !node.smooth.isBound();
 656             }
 657 
 658             @Override
 659             public StyleableProperty<Boolean> getStyleableProperty(Shape node) {
 660                 return (StyleableProperty<Boolean>)node.smoothProperty();
 661             }
 662 
 663         };
 664 
 665         /**
 666         * @css -fx-stroke: <a href="../doc-files/cssref.html#typepaint">&lt;paint&gt;</a>
 667         * @see Shape#stroke
 668         */
 669         private static final CssMetaData<Shape,Paint> STROKE =
 670             new CssMetaData<Shape,Paint>("-fx-stroke",
 671                 PaintConverter.getInstance()) {
 672 
 673             @Override
 674             public boolean isSettable(Shape node) {
 675                 return node.stroke == null || !node.stroke.isBound();
 676             }
 677 
 678             @Override
 679             public StyleableProperty<Paint> getStyleableProperty(Shape node) {
 680                 return (StyleableProperty<Paint>)node.strokeProperty();
 681             }
 682 
 683             @Override
 684             public Paint getInitialValue(Shape node) {
 685                 // Some shapes have a different initial value for stroke.
 686                 // Give a way to have them return the correct initial value.
 687                 return ShapeHelper.cssGetStrokeInitialValue(node);
 688             }
 689 
 690 
 691         };
 692 
 693         /**
 694         * @css -fx-stroke-dash-array: <a href="#typesize" class="typelink">&lt;size&gt;</a>
 695         *                    [<a href="#typesize" class="typelink">&lt;size&gt;</a>]+
 696         * <p>
 697         * Note:
 698         * Because {@link StrokeAttributes#dashArray} is not itself a
 699         * {@link Property},
 700         * the <code>getProperty()</code> method of this CssMetaData
 701         * returns the {@link StrokeAttributes#dashArray} wrapped in an
 702         * {@link ObjectProperty}. This is inconsistent with other
 703         * StyleableProperties which return the actual {@link Property}.
 704         * </p>
 705         * @see StrokeAttributes#dashArray
 706         */
 707         private static final CssMetaData<Shape,Number[]> STROKE_DASH_ARRAY =
 708             new CssMetaData<Shape,Number[]>("-fx-stroke-dash-array",
 709                 SizeConverter.SequenceConverter.getInstance(),
 710                 new Double[0]) {
 711 
 712             @Override
 713             public boolean isSettable(Shape node) {
 714                 return true;
 715             }
 716 
 717             @Override
 718             public StyleableProperty<Number[]> getStyleableProperty(final Shape node) {
 719                 return (StyleableProperty<Number[]>)node.getStrokeAttributes().cssDashArrayProperty();
 720             }
 721 
 722         };
 723 
 724         /**
 725         * @css -fx-stroke-dash-offset: <a href="#typesize" class="typelink">&lt;size&gt;</a>
 726         * @see #strokeDashOffsetProperty()
 727         */
 728         private static final CssMetaData<Shape,Number> STROKE_DASH_OFFSET =
 729             new CssMetaData<Shape,Number>("-fx-stroke-dash-offset",
 730                 SizeConverter.getInstance(), 0.0) {
 731 
 732             @Override
 733             public boolean isSettable(Shape node) {
 734                 return node.strokeAttributes == null ||
 735                         node.strokeAttributes.canSetDashOffset();
 736             }
 737 
 738             @Override
 739             public StyleableProperty<Number> getStyleableProperty(Shape node) {
 740                 return (StyleableProperty<Number>)node.strokeDashOffsetProperty();
 741             }
 742 
 743         };
 744 
 745         /**
 746         * @css -fx-stroke-line-cap: [ square | butt | round ]
 747         * @see #strokeLineCapProperty()
 748         */
 749         private static final CssMetaData<Shape,StrokeLineCap> STROKE_LINE_CAP =
 750             new CssMetaData<Shape,StrokeLineCap>("-fx-stroke-line-cap",
 751                 new EnumConverter<StrokeLineCap>(StrokeLineCap.class),
 752                 StrokeLineCap.SQUARE) {
 753 
 754             @Override
 755             public boolean isSettable(Shape node) {
 756                 return node.strokeAttributes == null ||
 757                         node.strokeAttributes.canSetLineCap();
 758             }
 759 
 760             @Override
 761             public StyleableProperty<StrokeLineCap> getStyleableProperty(Shape node) {
 762                 return (StyleableProperty<StrokeLineCap>)node.strokeLineCapProperty();
 763             }
 764 
 765         };
 766 
 767         /**
 768         * @css -fx-stroke-line-join: [ miter | bevel | round ]
 769         * @see #strokeLineJoinProperty()
 770         */
 771         private static final CssMetaData<Shape,StrokeLineJoin> STROKE_LINE_JOIN =
 772             new CssMetaData<Shape,StrokeLineJoin>("-fx-stroke-line-join",
 773                 new EnumConverter<StrokeLineJoin>(StrokeLineJoin.class),
 774                 StrokeLineJoin.MITER) {
 775 
 776             @Override
 777             public boolean isSettable(Shape node) {
 778                 return node.strokeAttributes == null ||
 779                         node.strokeAttributes.canSetLineJoin();
 780             }
 781 
 782             @Override
 783             public StyleableProperty<StrokeLineJoin> getStyleableProperty(Shape node) {
 784                 return (StyleableProperty<StrokeLineJoin>)node.strokeLineJoinProperty();
 785             }
 786 
 787         };
 788 
 789         /**
 790         * @css -fx-stroke-type: [ inside | outside | centered ]
 791         * @see #strokeTypeProperty()
 792         */
 793         private static final CssMetaData<Shape,StrokeType> STROKE_TYPE =
 794             new CssMetaData<Shape,StrokeType>("-fx-stroke-type",
 795                 new EnumConverter<StrokeType>(StrokeType.class),
 796                 StrokeType.CENTERED) {
 797 
 798             @Override
 799             public boolean isSettable(Shape node) {
 800                 return node.strokeAttributes == null ||
 801                         node.strokeAttributes.canSetType();
 802             }
 803 
 804             @Override
 805             public StyleableProperty<StrokeType> getStyleableProperty(Shape node) {
 806                 return (StyleableProperty<StrokeType>)node.strokeTypeProperty();
 807             }
 808 
 809 
 810         };
 811 
 812         /**
 813         * @css -fx-stroke-miter-limit: <a href="#typesize" class="typelink">&lt;size&gt;</a>
 814         * @see #strokeMiterLimitProperty()
 815         */
 816         private static final CssMetaData<Shape,Number> STROKE_MITER_LIMIT =
 817             new CssMetaData<Shape,Number>("-fx-stroke-miter-limit",
 818                 SizeConverter.getInstance(), 10.0) {
 819 
 820             @Override
 821             public boolean isSettable(Shape node) {
 822                 return node.strokeAttributes == null ||
 823                         node.strokeAttributes.canSetMiterLimit();
 824             }
 825 
 826             @Override
 827             public StyleableProperty<Number> getStyleableProperty(Shape node) {
 828                 return (StyleableProperty<Number>)node.strokeMiterLimitProperty();
 829             }
 830 
 831         };
 832 
 833         /**
 834         * @css -fx-stroke-width: <a href="#typesize" class="typelink">&lt;size&gt;</a>
 835         * @see #strokeWidthProperty()
 836         */
 837         private static final CssMetaData<Shape,Number> STROKE_WIDTH =
 838             new CssMetaData<Shape,Number>("-fx-stroke-width",
 839                 SizeConverter.getInstance(), 1.0) {
 840 
 841             @Override
 842             public boolean isSettable(Shape node) {
 843                 return node.strokeAttributes == null ||
 844                         node.strokeAttributes.canSetWidth();
 845             }
 846 
 847             @Override
 848             public StyleableProperty<Number> getStyleableProperty(Shape node) {
 849                 return (StyleableProperty<Number>)node.strokeWidthProperty();
 850             }
 851 
 852         };
 853          private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 854          static {
 855 
 856             final List<CssMetaData<? extends Styleable, ?>> styleables =
 857                 new ArrayList<CssMetaData<? extends Styleable, ?>>(Node.getClassCssMetaData());
 858             styleables.add(FILL);
 859             styleables.add(SMOOTH);
 860             styleables.add(STROKE);
 861             styleables.add(STROKE_DASH_ARRAY);
 862             styleables.add(STROKE_DASH_OFFSET);
 863             styleables.add(STROKE_LINE_CAP);
 864             styleables.add(STROKE_LINE_JOIN);
 865             styleables.add(STROKE_TYPE);
 866             styleables.add(STROKE_MITER_LIMIT);
 867             styleables.add(STROKE_WIDTH);
 868             STYLEABLES = Collections.unmodifiableList(styleables);
 869          }
 870     }
 871 
 872     /**
 873      * @return The CssMetaData associated with this class, which may include the
 874      * CssMetaData of its super classes.
 875      * @since JavaFX 8.0
 876      */
 877     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 878         return StyleableProperties.STYLEABLES;
 879     }
 880 
 881     /**
 882      * {@inheritDoc}
 883      *
 884      * @since JavaFX 8.0
 885      */
 886 
 887 
 888     @Override
 889     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 890         return getClassCssMetaData();
 891     }
 892 
 893     /**
 894      * @treatAsPrivate implementation detail
 895      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 896      */
 897     @Deprecated
 898     @Override
 899     public BaseBounds impl_computeGeomBounds(BaseBounds bounds,
 900                                              BaseTransform tx) {
 901         return computeShapeBounds(bounds, tx, ShapeHelper.configShape(this));
 902     }
 903 
 904     /**
 905      * @treatAsPrivate implementation detail
 906      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 907      */
 908     @Deprecated
 909     @Override
 910     protected boolean impl_computeContains(double localX, double localY) {
 911         return computeShapeContains(localX, localY, ShapeHelper.configShape(this));
 912     }
 913 
 914     private static final double MIN_STROKE_WIDTH = 0.0f;
 915     private static final double MIN_STROKE_MITER_LIMIT = 1.0f;
 916 
 917     private void updatePGShape() {
 918         final NGShape peer = NodeHelper.getPeer(this);
 919         if (strokeAttributesDirty && (getStroke() != null)) {
 920             // set attributes of stroke only when stroke paint is not null
 921             final float[] pgDashArray =
 922                     (hasStrokeDashArray())
 923                             ? toPGDashArray(getStrokeDashArray())
 924                             : DEFAULT_PG_STROKE_DASH_ARRAY;
 925 
 926             peer.setDrawStroke(
 927                         (float)Utils.clampMin(getStrokeWidth(),
 928                                               MIN_STROKE_WIDTH),
 929                         getStrokeType(),
 930                         getStrokeLineCap(),
 931                         convertLineJoin(getStrokeLineJoin()),
 932                         (float)Utils.clampMin(getStrokeMiterLimit(),
 933                                               MIN_STROKE_MITER_LIMIT),
 934                         pgDashArray, (float)getStrokeDashOffset());
 935 
 936            strokeAttributesDirty = false;
 937         }
 938 
 939         if (NodeHelper.isDirty(this, DirtyBits.SHAPE_MODE)) {
 940             peer.setMode(mode);
 941         }
 942 
 943         if (NodeHelper.isDirty(this, DirtyBits.SHAPE_FILL)) {
 944             Paint localFill = getFill();
 945             peer.setFillPaint(localFill == null ? null :
 946                     Toolkit.getPaintAccessor().getPlatformPaint(localFill));
 947         }
 948 
 949         if (NodeHelper.isDirty(this, DirtyBits.SHAPE_STROKE)) {
 950             Paint localStroke = getStroke();
 951             peer.setDrawPaint(localStroke == null ? null :
 952                     Toolkit.getPaintAccessor().getPlatformPaint(localStroke));
 953         }
 954 
 955         if (NodeHelper.isDirty(this, DirtyBits.NODE_SMOOTH)) {
 956             peer.setSmooth(isSmooth());
 957         }
 958     }
 959 
 960     /*
 961      * Note: This method MUST only be called via its accessor method.
 962      */
 963     private void doMarkDirty(DirtyBits dirtyBits) {
 964         final Runnable listener = shapeChangeListener != null ? shapeChangeListener.get() : null;
 965         if (listener != null && NodeHelper.isDirtyEmpty(this)) {
 966             listener.run();
 967         }
 968     }
 969 
 970     private Reference<Runnable> shapeChangeListener;
 971 
 972     void setShapeChangeListener(Runnable listener) {
 973         if (shapeChangeListener != null) shapeChangeListener.clear();
 974         shapeChangeListener = listener != null ? new WeakReference(listener) : null;
 975     }
 976 
 977     /*
 978      * Note: This method MUST only be called via its accessor method.
 979      */
 980     private void doUpdatePeer() {
 981         updatePGShape();
 982     }
 983 
 984     /**
 985      * Helper function for rectangular shapes such as Rectangle and Ellipse
 986      * for computing their bounds.
 987      */
 988     BaseBounds computeBounds(BaseBounds bounds, BaseTransform tx,
 989                                    double upad, double dpad,
 990                                    double x, double y,
 991                                    double w, double h)
 992     {
 993         // if the w or h is < 0 then bounds is empty
 994         if (w < 0.0f || h < 0.0f) return bounds.makeEmpty();
 995 
 996         double x0 = x;
 997         double y0 = y;
 998         double x1 = w;
 999         double y1 = h;
1000         double _dpad = dpad;
1001         if (tx.isTranslateOrIdentity()) {
1002             x1 += x0;
1003             y1 += y0;
1004             if (tx.getType() == BaseTransform.TYPE_TRANSLATION) {
1005                 final double dx = tx.getMxt();
1006                 final double dy = tx.getMyt();
1007                 x0 += dx;
1008                 y0 += dy;
1009                 x1 += dx;
1010                 y1 += dy;
1011             }
1012             _dpad += upad;
1013         } else {
1014             x0 -= upad;
1015             y0 -= upad;
1016             x1 += upad*2;
1017             y1 += upad*2;
1018             // Each corner is transformed by an equation similar to:
1019             //     x' = x * mxx + y * mxy + mxt
1020             //     y' = x * myx + y * myy + myt
1021             // Since all of the corners are translated by mxt,myt we
1022             // can ignore them when doing the min/max calculations
1023             // and add them in once when we are done.  We then have
1024             // to do min/max operations on 4 points defined as:
1025             //     x' = x * mxx + y * mxy
1026             //     y' = x * myx + y * myy
1027             // Furthermore, the four corners that we will be transforming
1028             // are not four independent coordinates, they are in a
1029             // rectangular formation.  To that end, if we translated
1030             // the transform to x,y and scaled it by width,height then
1031             // we could compute the min/max of the unit rectangle 0,0,1x1.
1032             // The transform would then be adjusted as follows:
1033             // First, the translation to x,y only affects the mxt,myt
1034             // components of the transform which we can hold off on adding
1035             // until we are done with the min/max.  The adjusted translation
1036             // components would be:
1037             //     mxt' = x * mxx + y * mxy + mxt
1038             //     myt' = x * myx + y * myy + myt
1039             // Second, the scale affects the components as follows:
1040             //     mxx' = mxx * width
1041             //     mxy' = mxy * height
1042             //     myx' = myx * width
1043             //     myy' = myy * height
1044             // The min/max of that rectangle then degenerates to:
1045             //     x00' = 0 * mxx' + 0 * mxy' = 0
1046             //     y00' = 0 * myx' + 0 * myy' = 0
1047             //     x01' = 0 * mxx' + 1 * mxy' = mxy'
1048             //     y01' = 0 * myx' + 1 * myy' = myy'
1049             //     x10' = 1 * mxx' + 0 * mxy' = mxx'
1050             //     y10' = 1 * myx' + 0 * myy' = myx'
1051             //     x11' = 1 * mxx' + 1 * mxy' = mxx' + mxy'
1052             //     y11' = 1 * myx' + 1 * myy' = myx' + myy'
1053             double mxx = tx.getMxx();
1054             double mxy = tx.getMxy();
1055             double myx = tx.getMyx();
1056             double myy = tx.getMyy();
1057             // Computed translated translation components
1058             final double mxt = (x0 * mxx + y0 * mxy + tx.getMxt());
1059             final double myt = (x0 * myx + y0 * myy + tx.getMyt());
1060             // Scale non-translation components by w/h
1061             mxx *= x1;
1062             mxy *= y1;
1063             myx *= x1;
1064             myy *= y1;
1065             x0 = (Math.min(Math.min(0,mxx),Math.min(mxy,mxx+mxy)))+mxt;
1066             y0 = (Math.min(Math.min(0,myx),Math.min(myy,myx+myy)))+myt;
1067             x1 = (Math.max(Math.max(0,mxx),Math.max(mxy,mxx+mxy)))+mxt;
1068             y1 = (Math.max(Math.max(0,myx),Math.max(myy,myx+myy)))+myt;
1069         }
1070         x0 -= _dpad;
1071         y0 -= _dpad;
1072         x1 += _dpad;
1073         y1 += _dpad;
1074 
1075         bounds = bounds.deriveWithNewBounds((float)x0, (float)y0, 0.0f,
1076                 (float)x1, (float)y1, 0.0f);
1077         return bounds;
1078     }
1079 
1080     BaseBounds computeShapeBounds(BaseBounds bounds, BaseTransform tx,
1081                                 com.sun.javafx.geom.Shape s)
1082     {
1083         // empty mode means no bounds!
1084         if (mode == NGShape.Mode.EMPTY) {
1085             return bounds.makeEmpty();
1086         }
1087 
1088         float[] bbox = {
1089             Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
1090             Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY,
1091         };
1092         boolean includeShape = (mode != NGShape.Mode.STROKE);
1093         boolean includeStroke = (mode != NGShape.Mode.FILL);
1094         if (includeStroke && (getStrokeType() == StrokeType.INSIDE)) {
1095             includeShape = true;
1096             includeStroke = false;
1097         }
1098 
1099         if (includeStroke) {
1100             final StrokeType type = getStrokeType();
1101             double sw = Utils.clampMin(getStrokeWidth(), MIN_STROKE_WIDTH);
1102             StrokeLineCap cap = getStrokeLineCap();
1103             StrokeLineJoin join = convertLineJoin(getStrokeLineJoin());
1104             float miterlimit =
1105                 (float) Utils.clampMin(getStrokeMiterLimit(), MIN_STROKE_MITER_LIMIT);
1106             // Note that we ignore dashing for computing bounds and testing
1107             // point containment, both to save time in bounds calculations
1108             // and so that animated dashing does not keep perturbing the bounds...
1109             Toolkit.getToolkit().accumulateStrokeBounds(
1110                     s,
1111                     bbox, type, sw,
1112                     cap, join, miterlimit, tx);
1113             // Account for "minimum pen size" by expanding by 0.5 device
1114             // pixels all around...
1115             bbox[0] -= 0.5;
1116             bbox[1] -= 0.5;
1117             bbox[2] += 0.5;
1118             bbox[3] += 0.5;
1119         } else if (includeShape) {
1120             com.sun.javafx.geom.Shape.accumulate(bbox, s, tx);
1121         }
1122 
1123         if (bbox[2] < bbox[0] || bbox[3] < bbox[1]) {
1124             // They are probably +/-INFINITY which would yield NaN if subtracted
1125             // Let's just return a "safe" empty bbox..
1126             return bounds.makeEmpty();
1127         }
1128         bounds = bounds.deriveWithNewBounds(bbox[0], bbox[1], 0.0f,
1129                 bbox[2], bbox[3], 0.0f);
1130         return bounds;
1131     }
1132 
1133     boolean computeShapeContains(double localX, double localY,
1134                                  com.sun.javafx.geom.Shape s) {
1135         if (mode == NGShape.Mode.EMPTY) {
1136             return false;
1137         }
1138 
1139         boolean includeShape = (mode != NGShape.Mode.STROKE);
1140         boolean includeStroke = (mode != NGShape.Mode.FILL);
1141         if (includeStroke && includeShape &&
1142             (getStrokeType() == StrokeType.INSIDE))
1143         {
1144             includeStroke = false;
1145         }
1146 
1147         if (includeShape) {
1148             if (s.contains((float)localX, (float)localY)) {
1149                 return true;
1150             }
1151         }
1152 
1153         if (includeStroke) {
1154             StrokeType type = getStrokeType();
1155             double sw = Utils.clampMin(getStrokeWidth(), MIN_STROKE_WIDTH);
1156             StrokeLineCap cap = getStrokeLineCap();
1157             StrokeLineJoin join = convertLineJoin(getStrokeLineJoin());
1158             float miterlimit =
1159                 (float) Utils.clampMin(getStrokeMiterLimit(), MIN_STROKE_MITER_LIMIT);
1160             // Note that we ignore dashing for computing bounds and testing
1161             // point containment, both to save time in bounds calculations
1162             // and so that animated dashing does not keep perturbing the bounds...
1163             return Toolkit.getToolkit().strokeContains(s, localX, localY,
1164                                                        type, sw, cap,
1165                                                        join, miterlimit);
1166         }
1167 
1168         return false;
1169     }
1170 
1171     private boolean strokeAttributesDirty = true;
1172 
1173     private StrokeAttributes strokeAttributes;
1174 
1175     private StrokeAttributes getStrokeAttributes() {
1176         if (strokeAttributes == null) {
1177             strokeAttributes = new StrokeAttributes();
1178         }
1179 
1180         return strokeAttributes;
1181     }
1182 
1183     private boolean hasStrokeDashArray() {
1184         return (strokeAttributes != null) && strokeAttributes.hasDashArray();
1185     }
1186 
1187     private static float[] toPGDashArray(final List<Double> dashArray) {
1188         final int size = dashArray.size();
1189         final float[] pgDashArray = new float[size];
1190         for (int i = 0; i < size; i++) {
1191             pgDashArray[i] = dashArray.get(i).floatValue();
1192         }
1193 
1194         return pgDashArray;
1195     }
1196 
1197     private static final StrokeType DEFAULT_STROKE_TYPE = StrokeType.CENTERED;
1198     private static final double DEFAULT_STROKE_WIDTH = 1.0;
1199     private static final StrokeLineJoin DEFAULT_STROKE_LINE_JOIN =
1200             StrokeLineJoin.MITER;
1201     private static final StrokeLineCap DEFAULT_STROKE_LINE_CAP =
1202             StrokeLineCap.SQUARE;
1203     private static final double DEFAULT_STROKE_MITER_LIMIT = 10.0;
1204     private static final double DEFAULT_STROKE_DASH_OFFSET = 0;
1205     private static final float[] DEFAULT_PG_STROKE_DASH_ARRAY = new float[0];
1206 
1207     private final class StrokeAttributes {
1208         private ObjectProperty<StrokeType> type;
1209         private DoubleProperty width;
1210         private ObjectProperty<StrokeLineJoin> lineJoin;
1211         private ObjectProperty<StrokeLineCap> lineCap;
1212         private DoubleProperty miterLimit;
1213         private DoubleProperty dashOffset;
1214         private ObservableList<Double> dashArray;
1215 
1216         public final StrokeType getType() {
1217             return (type == null) ? DEFAULT_STROKE_TYPE : type.get();
1218         }
1219 
1220         public final ObjectProperty<StrokeType> typeProperty() {
1221             if (type == null) {
1222                 type = new StyleableObjectProperty<StrokeType>(DEFAULT_STROKE_TYPE) {
1223 
1224                     @Override
1225                     public void invalidated() {
1226                         StrokeAttributes.this.invalidated(
1227                                 StyleableProperties.STROKE_TYPE);
1228                     }
1229 
1230                     @Override
1231                     public CssMetaData<Shape,StrokeType> getCssMetaData() {
1232                         return StyleableProperties.STROKE_TYPE;
1233                     }
1234 
1235                     @Override
1236                     public Object getBean() {
1237                         return Shape.this;
1238                     }
1239 
1240                     @Override
1241                     public String getName() {
1242                         return "strokeType";
1243                     }
1244                 };
1245             }
1246             return type;
1247         }
1248 
1249         public double getWidth() {
1250             return (width == null) ? DEFAULT_STROKE_WIDTH : width.get();
1251         }
1252 
1253         public final DoubleProperty widthProperty() {
1254             if (width == null) {
1255                 width = new StyleableDoubleProperty(DEFAULT_STROKE_WIDTH) {
1256 
1257                     @Override
1258                     public void invalidated() {
1259                         StrokeAttributes.this.invalidated(
1260                                 StyleableProperties.STROKE_WIDTH);
1261                     }
1262 
1263                     @Override
1264                     public CssMetaData<Shape,Number> getCssMetaData() {
1265                         return StyleableProperties.STROKE_WIDTH;
1266                     }
1267 
1268                     @Override
1269                     public Object getBean() {
1270                         return Shape.this;
1271                     }
1272 
1273                     @Override
1274                     public String getName() {
1275                         return "strokeWidth";
1276                     }
1277                 };
1278             }
1279             return width;
1280         }
1281 
1282         public StrokeLineJoin getLineJoin() {
1283             return (lineJoin == null) ? DEFAULT_STROKE_LINE_JOIN
1284                                       : lineJoin.get();
1285         }
1286 
1287         public final ObjectProperty<StrokeLineJoin> lineJoinProperty() {
1288             if (lineJoin == null) {
1289                 lineJoin = new StyleableObjectProperty<StrokeLineJoin>(
1290                                        DEFAULT_STROKE_LINE_JOIN) {
1291 
1292                     @Override
1293                     public void invalidated() {
1294                         StrokeAttributes.this.invalidated(
1295                                 StyleableProperties.STROKE_LINE_JOIN);
1296                     }
1297 
1298                     @Override
1299                     public CssMetaData<Shape,StrokeLineJoin> getCssMetaData() {
1300                         return StyleableProperties.STROKE_LINE_JOIN;
1301                     }
1302 
1303                     @Override
1304                     public Object getBean() {
1305                         return Shape.this;
1306                     }
1307 
1308                     @Override
1309                     public String getName() {
1310                         return "strokeLineJoin";
1311                     }
1312                 };
1313             }
1314             return lineJoin;
1315         }
1316 
1317         public StrokeLineCap getLineCap() {
1318             return (lineCap == null) ? DEFAULT_STROKE_LINE_CAP
1319                                      : lineCap.get();
1320         }
1321 
1322         public final ObjectProperty<StrokeLineCap> lineCapProperty() {
1323             if (lineCap == null) {
1324                 lineCap = new StyleableObjectProperty<StrokeLineCap>(
1325                                       DEFAULT_STROKE_LINE_CAP) {
1326 
1327                     @Override
1328                     public void invalidated() {
1329                         StrokeAttributes.this.invalidated(
1330                                 StyleableProperties.STROKE_LINE_CAP);
1331                     }
1332 
1333                     @Override
1334                     public CssMetaData<Shape,StrokeLineCap> getCssMetaData() {
1335                         return StyleableProperties.STROKE_LINE_CAP;
1336                     }
1337 
1338                     @Override
1339                     public Object getBean() {
1340                         return Shape.this;
1341                     }
1342 
1343                     @Override
1344                     public String getName() {
1345                         return "strokeLineCap";
1346                     }
1347                 };
1348             }
1349 
1350             return lineCap;
1351         }
1352 
1353         public double getMiterLimit() {
1354             return (miterLimit == null) ? DEFAULT_STROKE_MITER_LIMIT
1355                                         : miterLimit.get();
1356         }
1357 
1358         public final DoubleProperty miterLimitProperty() {
1359             if (miterLimit == null) {
1360                 miterLimit = new StyleableDoubleProperty(
1361                                          DEFAULT_STROKE_MITER_LIMIT) {
1362                     @Override
1363                     public void invalidated() {
1364                         StrokeAttributes.this.invalidated(
1365                                 StyleableProperties.STROKE_MITER_LIMIT);
1366                     }
1367 
1368                     @Override
1369                     public CssMetaData<Shape,Number> getCssMetaData() {
1370                         return StyleableProperties.STROKE_MITER_LIMIT;
1371                     }
1372 
1373                     @Override
1374                     public Object getBean() {
1375                         return Shape.this;
1376                     }
1377 
1378                     @Override
1379                     public String getName() {
1380                         return "strokeMiterLimit";
1381                     }
1382                 };
1383             }
1384 
1385             return miterLimit;
1386         }
1387 
1388         public double getDashOffset() {
1389             return (dashOffset == null) ? DEFAULT_STROKE_DASH_OFFSET
1390                                         : dashOffset.get();
1391         }
1392 
1393         public final DoubleProperty dashOffsetProperty() {
1394             if (dashOffset == null) {
1395                 dashOffset = new StyleableDoubleProperty(
1396                                          DEFAULT_STROKE_DASH_OFFSET) {
1397 
1398                     @Override
1399                     public void invalidated() {
1400                         StrokeAttributes.this.invalidated(
1401                                 StyleableProperties.STROKE_DASH_OFFSET);
1402                     }
1403 
1404                     @Override
1405                     public CssMetaData<Shape,Number> getCssMetaData() {
1406                         return StyleableProperties.STROKE_DASH_OFFSET;
1407                     }
1408 
1409                     @Override
1410                     public Object getBean() {
1411                         return Shape.this;
1412                     }
1413 
1414                     @Override
1415                     public String getName() {
1416                         return "strokeDashOffset";
1417                     }
1418                 };
1419             }
1420 
1421             return dashOffset;
1422         }
1423 
1424         // TODO: Need to handle set from css - should clear array and add all.
1425         public ObservableList<Double> dashArrayProperty() {
1426             if (dashArray == null) {
1427                 dashArray = new TrackableObservableList<Double>() {
1428                     @Override
1429                     protected void onChanged(Change<Double> c) {
1430                         StrokeAttributes.this.invalidated(
1431                                 StyleableProperties.STROKE_DASH_ARRAY);
1432                     }
1433                 };
1434             }
1435             return dashArray;
1436         }
1437 
1438         private ObjectProperty<Number[]> cssDashArray = null;
1439         private ObjectProperty<Number[]> cssDashArrayProperty() {
1440             if (cssDashArray == null) {
1441                 cssDashArray = new StyleableObjectProperty<Number[]>()
1442                 {
1443 
1444                     @Override
1445                     public void set(Number[] v) {
1446 
1447                         ObservableList<Double> list = dashArrayProperty();
1448                         list.clear();
1449                         if (v != null && v.length > 0) {
1450                             for (int n=0; n<v.length; n++) {
1451                                 list.add(v[n].doubleValue());
1452                             }
1453                         }
1454 
1455                         // no need to hold onto the array
1456                     }
1457 
1458                     @Override
1459                     public Double[] get() {
1460                         List<Double> list = dashArrayProperty();
1461                         return list.toArray(new Double[list.size()]);
1462                     }
1463 
1464                     @Override
1465                     public Object getBean() {
1466                         return Shape.this;
1467                     }
1468 
1469                     @Override
1470                     public String getName() {
1471                         return "cssDashArray";
1472                     }
1473 
1474                     @Override
1475                     public CssMetaData<Shape,Number[]> getCssMetaData() {
1476                         return StyleableProperties.STROKE_DASH_ARRAY;
1477                     }
1478                 };
1479             }
1480 
1481             return cssDashArray;
1482         }
1483 
1484         public boolean canSetType() {
1485             return (type == null) || !type.isBound();
1486         }
1487 
1488         public boolean canSetWidth() {
1489             return (width == null) || !width.isBound();
1490         }
1491 
1492         public boolean canSetLineJoin() {
1493             return (lineJoin == null) || !lineJoin.isBound();
1494         }
1495 
1496         public boolean canSetLineCap() {
1497             return (lineCap == null) || !lineCap.isBound();
1498         }
1499 
1500         public boolean canSetMiterLimit() {
1501             return (miterLimit == null) || !miterLimit.isBound();
1502         }
1503 
1504         public boolean canSetDashOffset() {
1505             return (dashOffset == null) || !dashOffset.isBound();
1506         }
1507 
1508         public boolean hasDashArray() {
1509             return (dashArray != null);
1510         }
1511 
1512         private void invalidated(final CssMetaData<Shape, ?> propertyCssKey) {
1513             NodeHelper.markDirty(Shape.this, DirtyBits.SHAPE_STROKEATTRS);
1514             strokeAttributesDirty = true;
1515             if (propertyCssKey != StyleableProperties.STROKE_DASH_OFFSET) {
1516                 // all stroke attributes change geometry except for the
1517                 // stroke dash offset
1518                 impl_geomChanged();
1519             }
1520         }
1521     }
1522 
1523     /**
1524      * @treatAsPrivate implementation detail
1525      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
1526      */
1527     @Deprecated
1528     @Override
1529     public Object impl_processMXNode(MXNodeAlgorithm alg, MXNodeAlgorithmContext ctx) {
1530         return alg.processLeafNode(this, ctx);
1531     }
1532 
1533     // PENDING_DOC_REVIEW
1534     /**
1535      * Returns a new {@code Shape} which is created as a union of the specified
1536      * input shapes.
1537      * <p>
1538      * The operation works with geometric areas occupied by the input shapes.
1539      * For a single {@code Shape} such area includes the area occupied by the
1540      * fill if the shape has a non-null fill and the area occupied by the stroke
1541      * if the shape has a non-null stroke. So the area is empty for a shape
1542      * with {@code null} stroke and {@code null} fill. The area of an input
1543      * shape considered by the operation is independent on the type and
1544      * configuration of the paint used for fill or stroke. Before the final
1545      * operation the areas of the input shapes are transformed to the parent
1546      * coordinate space of their respective topmost parent nodes.
1547      * <p>
1548      * The resulting shape will include areas that were contained in any of the
1549      * input shapes.
1550      * <p>
1551 
1552 <PRE>
1553 
1554          shape1       +       shape2       =       result
1555    +----------------+   +----------------+   +----------------+
1556    |################|   |################|   |################|
1557    |##############  |   |  ##############|   |################|
1558    |############    |   |    ############|   |################|
1559    |##########      |   |      ##########|   |################|
1560    |########        |   |        ########|   |################|
1561    |######          |   |          ######|   |######    ######|
1562    |####            |   |            ####|   |####        ####|
1563    |##              |   |              ##|   |##            ##|
1564    +----------------+   +----------------+   +----------------+
1565 
1566 </PRE>
1567 
1568      * @param shape1 the first shape
1569      * @param shape2 the second shape
1570      * @return the created {@code Shape}
1571      */
1572     public static Shape union(final Shape shape1, final Shape shape2) {
1573         final Area result = shape1.getTransformedArea();
1574         result.add(shape2.getTransformedArea());
1575         return createFromGeomShape(result);
1576     }
1577 
1578     // PENDING_DOC_REVIEW
1579     /**
1580      * Returns a new {@code Shape} which is created by subtracting the specified
1581      * second shape from the first shape.
1582      * <p>
1583      * The operation works with geometric areas occupied by the input shapes.
1584      * For a single {@code Shape} such area includes the area occupied by the
1585      * fill if the shape has a non-null fill and the area occupied by the stroke
1586      * if the shape has a non-null stroke. So the area is empty for a shape
1587      * with {@code null} stroke and {@code null} fill. The area of an input
1588      * shape considered by the operation is independent on the type and
1589      * configuration of the paint used for fill or stroke. Before the final
1590      * operation the areas of the input shapes are transformed to the parent
1591      * coordinate space of their respective topmost parent nodes.
1592      * <p>
1593      * The resulting shape will include areas that were contained only in the
1594      * first shape and not in the second shape.
1595      * <p>
1596 
1597 <PRE>
1598 
1599          shape1       -       shape2       =       result
1600    +----------------+   +----------------+   +----------------+
1601    |################|   |################|   |                |
1602    |##############  |   |  ##############|   |##              |
1603    |############    |   |    ############|   |####            |
1604    |##########      |   |      ##########|   |######          |
1605    |########        |   |        ########|   |########        |
1606    |######          |   |          ######|   |######          |
1607    |####            |   |            ####|   |####            |
1608    |##              |   |              ##|   |##              |
1609    +----------------+   +----------------+   +----------------+
1610 
1611 </PRE>
1612 
1613      * @param shape1 the first shape
1614      * @param shape2 the second shape
1615      * @return the created {@code Shape}
1616      */
1617     public static Shape subtract(final Shape shape1, final Shape shape2) {
1618         final Area result = shape1.getTransformedArea();
1619         result.subtract(shape2.getTransformedArea());
1620         return createFromGeomShape(result);
1621     }
1622 
1623     // PENDING_DOC_REVIEW
1624     /**
1625      * Returns a new {@code Shape} which is created as an intersection of the
1626      * specified input shapes.
1627      * <p>
1628      * The operation works with geometric areas occupied by the input shapes.
1629      * For a single {@code Shape} such area includes the area occupied by the
1630      * fill if the shape has a non-null fill and the area occupied by the stroke
1631      * if the shape has a non-null stroke. So the area is empty for a shape
1632      * with {@code null} stroke and {@code null} fill. The area of an input
1633      * shape considered by the operation is independent on the type and
1634      * configuration of the paint used for fill or stroke. Before the final
1635      * operation the areas of the input shapes are transformed to the parent
1636      * coordinate space of their respective topmost parent nodes.
1637      * <p>
1638      * The resulting shape will include only areas that were contained in both
1639      * of the input shapes.
1640      * <p>
1641 
1642 <PRE>
1643 
1644          shape1       +       shape2       =       result
1645    +----------------+   +----------------+   +----------------+
1646    |################|   |################|   |################|
1647    |##############  |   |  ##############|   |  ############  |
1648    |############    |   |    ############|   |    ########    |
1649    |##########      |   |      ##########|   |      ####      |
1650    |########        |   |        ########|   |                |
1651    |######          |   |          ######|   |                |
1652    |####            |   |            ####|   |                |
1653    |##              |   |              ##|   |                |
1654    +----------------+   +----------------+   +----------------+
1655 
1656 </PRE>
1657 
1658      * @param shape1 the first shape
1659      * @param shape2 the second shape
1660      * @return the created {@code Shape}
1661      */
1662     public static Shape intersect(final Shape shape1, final Shape shape2) {
1663         final Area result = shape1.getTransformedArea();
1664         result.intersect(shape2.getTransformedArea());
1665         return createFromGeomShape(result);
1666     }
1667 
1668     private Area getTransformedArea() {
1669         return getTransformedArea(calculateNodeToSceneTransform(this));
1670     }
1671 
1672     private Area getTransformedArea(final BaseTransform transform) {
1673         if (mode == NGShape.Mode.EMPTY) {
1674             return new Area();
1675         }
1676 
1677         final com.sun.javafx.geom.Shape fillShape = ShapeHelper.configShape(this);
1678         if ((mode == NGShape.Mode.FILL)
1679                 || (mode == NGShape.Mode.STROKE_FILL)
1680                        && (getStrokeType() == StrokeType.INSIDE)) {
1681             return createTransformedArea(fillShape, transform);
1682         }
1683 
1684         final StrokeType strokeType = getStrokeType();
1685         final double strokeWidth =
1686                 Utils.clampMin(getStrokeWidth(), MIN_STROKE_WIDTH);
1687         final StrokeLineCap strokeLineCap = getStrokeLineCap();
1688         final StrokeLineJoin strokeLineJoin = convertLineJoin(getStrokeLineJoin());
1689         final float strokeMiterLimit =
1690                 (float) Utils.clampMin(getStrokeMiterLimit(),
1691                                        MIN_STROKE_MITER_LIMIT);
1692         final float[] dashArray =
1693                 (hasStrokeDashArray())
1694                         ? toPGDashArray(getStrokeDashArray())
1695                         : DEFAULT_PG_STROKE_DASH_ARRAY;
1696 
1697         final com.sun.javafx.geom.Shape strokeShape =
1698                 Toolkit.getToolkit().createStrokedShape(
1699                         fillShape, strokeType, strokeWidth, strokeLineCap,
1700                         strokeLineJoin, strokeMiterLimit,
1701                         dashArray, (float) getStrokeDashOffset());
1702 
1703         if (mode == NGShape.Mode.STROKE) {
1704             return createTransformedArea(strokeShape, transform);
1705         }
1706 
1707         // fill and stroke
1708         final Area combinedArea = new Area(fillShape);
1709         combinedArea.add(new Area(strokeShape));
1710 
1711         return createTransformedArea(combinedArea, transform);
1712     }
1713 
1714     private static BaseTransform calculateNodeToSceneTransform(Node node) {
1715         final Affine3D cumulativeTransformation = new Affine3D();
1716 
1717         do {
1718             cumulativeTransformation.preConcatenate(
1719                     node.impl_getLeafTransform());
1720             node = node.getParent();
1721         } while (node != null);
1722 
1723         return cumulativeTransformation;
1724     }
1725 
1726     private static Area createTransformedArea(
1727             final com.sun.javafx.geom.Shape geomShape,
1728             final BaseTransform transform) {
1729         return transform.isIdentity()
1730                    ? new Area(geomShape)
1731                    : new Area(geomShape.getPathIterator(transform));
1732     }
1733 
1734     private static Path createFromGeomShape(
1735             final com.sun.javafx.geom.Shape geomShape) {
1736         final Path path = new Path();
1737         final ObservableList<PathElement> elements = path.getElements();
1738 
1739         final PathIterator iterator = geomShape.getPathIterator(null);
1740         final float coords[] = new float[6];
1741 
1742         while (!iterator.isDone()) {
1743             final int segmentType = iterator.currentSegment(coords);
1744             switch (segmentType) {
1745                 case PathIterator.SEG_MOVETO:
1746                     elements.add(new MoveTo(coords[0], coords[1]));
1747                     break;
1748                 case PathIterator.SEG_LINETO:
1749                     elements.add(new LineTo(coords[0], coords[1]));
1750                     break;
1751                 case PathIterator.SEG_QUADTO:
1752                     elements.add(new QuadCurveTo(coords[0], coords[1],
1753                                                  coords[2], coords[3]));
1754                     break;
1755                 case PathIterator.SEG_CUBICTO:
1756                     elements.add(new CubicCurveTo(coords[0], coords[1],
1757                                                   coords[2], coords[3],
1758                                                   coords[4], coords[5]));
1759                     break;
1760                 case PathIterator.SEG_CLOSE:
1761                     elements.add(new ClosePath());
1762                     break;
1763             }
1764 
1765             iterator.next();
1766         }
1767 
1768         path.setFillRule((iterator.getWindingRule()
1769                              == PathIterator.WIND_EVEN_ODD)
1770                                  ? FillRule.EVEN_ODD
1771                                  : FillRule.NON_ZERO);
1772 
1773         path.setFill(Color.BLACK);
1774         path.setStroke(null);
1775 
1776         return path;
1777     }
1778 }