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