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