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