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