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