1 /* 2 * Copyright (c) 2010, 2018, 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"><paint></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"><boolean></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"><paint></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"><size></a> 718 * [<a href="#typesize" class="typelink"><size></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"><size></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"><size></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"><size></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 }