/* * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. * * * * * * * * * * * * * * * * * * * * */ package javafx.scene.shape; import javafx.beans.Observable; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.Property; import javafx.collections.ListChangeListener.Change; import javafx.collections.ObservableList; import javafx.css.CssMetaData; import javafx.css.Styleable; import javafx.css.StyleableBooleanProperty; import javafx.css.StyleableDoubleProperty; import javafx.css.StyleableObjectProperty; import javafx.css.StyleableProperty; import javafx.scene.Node; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import java.util.ArrayList; import java.util.Collections; import java.util.List; import com.sun.javafx.util.Utils; import com.sun.javafx.beans.event.AbstractNotifyListener; import com.sun.javafx.collections.TrackableObservableList; import com.sun.javafx.css.converters.BooleanConverter; import com.sun.javafx.css.converters.EnumConverter; import com.sun.javafx.css.converters.PaintConverter; import com.sun.javafx.css.converters.SizeConverter; import com.sun.javafx.geom.Area; import com.sun.javafx.geom.BaseBounds; import com.sun.javafx.geom.PathIterator; import com.sun.javafx.geom.transform.Affine3D; import com.sun.javafx.geom.transform.BaseTransform; import com.sun.javafx.jmx.MXNodeAlgorithm; import com.sun.javafx.jmx.MXNodeAlgorithmContext; import com.sun.javafx.scene.DirtyBits; import com.sun.javafx.sg.prism.NGNode; import com.sun.javafx.sg.prism.NGShape; import com.sun.javafx.tk.Toolkit; import java.lang.ref.Reference; import java.lang.ref.WeakReference; /** * The {@code Shape} class provides definitions of common properties for * objects that represent some form of geometric shape. These properties * include: *
* On the other hand, stroking those same shapes can often lead to fuzzy * outlines because the default stroking attributes specify both that the * default stroke width is 1.0 coordinates which often maps to exactly 1 * device pixel and also that the stroke should straddle the border of the * shape, falling half on either side of the border. * Since the borders in many common shapes tend to fall directly on integer * coordinates and those integer coordinates often map precisely to integer * device locations, the borders tend to result in 50% coverage over the * pixel rows and columns on either side of the border of the shape rather * than 100% coverage on one or the other. Thus, fills may typically be * crisp, but strokes are often fuzzy. *
* Two common solutions to avoid these fuzzy outlines are to use wider * strokes that cover more pixels completely - typically a stroke width of * 2.0 will achieve this if there are no scale transforms in effect - or * to specify either the {@link StrokeType#INSIDE} or {@link StrokeType#OUTSIDE} * stroke styles - which will bias the default single unit stroke onto one * of the full pixel rows or columns just inside or outside the border of * the shape. * @since JavaFX 2.0 */ public abstract class Shape extends Node { /** * @treatAsPrivate implementation detail * @deprecated This is an internal API that is not intended for use and will be removed in the next version */ @Deprecated @Override protected NGNode impl_createPeer() { throw new AssertionError( "Subclasses of Shape must implement impl_createPGNode"); } StrokeLineJoin convertLineJoin(StrokeLineJoin t) { return t; } public final void setStrokeType(StrokeType value) { strokeTypeProperty().set(value); } public final StrokeType getStrokeType() { return (strokeAttributes == null) ? DEFAULT_STROKE_TYPE : strokeAttributes.getType(); } /** * Defines the direction (inside, centered, or outside) that the strokeWidth * is applied to the boundary of the shape. * *
* The image shows a shape without stroke and with a thick stroke applied * inside, centered and outside. *
*
*
*
*
*
*
* The image demonstrates the behavior. Miter length ({@code A}) is computed * as the distance of the most inside point to the most outside point of * the joint, with the stroke width as a unit. If the miter length is bigger * than the given miter limit, the miter is cut at the edge of the shape * ({@code B}). For the situation in the image it means that the miter * will be cut at {@code B} for limit values less than {@code 4.65}. *
*
*
* The image shows a stroke with dash array {@code [25, 20, 5, 20]} and * a stroke with the same pattern and offset {@code 45} which shifts * the pattern about the length of the first dash segment and * the following space. *
*
*
* An empty strokeDashArray indicates a solid line with no spaces. * An odd length strokeDashArray behaves the same as an even length * array constructed by implicitly repeating the indicated odd length * array twice in succession ({@code [20, 5, 15]} behaves as if it * were {@code [20, 5, 15, 20, 5, 15]}). *
* Note that each dash segment will be capped by the decoration specified * by the current stroke line cap. * *
* The image shows a shape with stroke dash array {@code [25, 20, 5, 20]} * and 3 different values for the stroke line cap: * {@code StrokeLineCap.BUTT}, {@code StrokeLineCap.SQUARE} (the default), * and {@code StrokeLineCap.ROUND} *
*
*
* Note:
* Because {@link StrokeAttributes#dashArray} is not itself a
* {@link Property},
* the getProperty()
method of this CssMetaData
* returns the {@link StrokeAttributes#dashArray} wrapped in an
* {@link ObjectProperty}. This is inconsistent with other
* StyleableProperties which return the actual {@link Property}.
*
* The operation works with geometric areas occupied by the input shapes. * For a single {@code Shape} such area includes the area occupied by the * fill if the shape has a non-null fill and the area occupied by the stroke * if the shape has a non-null stroke. So the area is empty for a shape * with {@code null} stroke and {@code null} fill. The area of an input * shape considered by the operation is independent on the type and * configuration of the paint used for fill or stroke. Before the final * operation the areas of the input shapes are transformed to the parent * coordinate space of their respective topmost parent nodes. *
* The resulting shape will include areas that were contained in any of the * input shapes. *
shape1 + shape2 = result +----------------+ +----------------+ +----------------+ |################| |################| |################| |############## | | ##############| |################| |############ | | ############| |################| |########## | | ##########| |################| |######## | | ########| |################| |###### | | ######| |###### ######| |#### | | ####| |#### ####| |## | | ##| |## ##| +----------------+ +----------------+ +----------------+* @param shape1 the first shape * @param shape2 the second shape * @return the created {@code Shape} */ public static Shape union(final Shape shape1, final Shape shape2) { final Area result = shape1.getTransformedArea(); result.add(shape2.getTransformedArea()); return createFromGeomShape(result); } // PENDING_DOC_REVIEW /** * Returns a new {@code Shape} which is created by subtracting the specified * second shape from the first shape. *
* The operation works with geometric areas occupied by the input shapes. * For a single {@code Shape} such area includes the area occupied by the * fill if the shape has a non-null fill and the area occupied by the stroke * if the shape has a non-null stroke. So the area is empty for a shape * with {@code null} stroke and {@code null} fill. The area of an input * shape considered by the operation is independent on the type and * configuration of the paint used for fill or stroke. Before the final * operation the areas of the input shapes are transformed to the parent * coordinate space of their respective topmost parent nodes. *
* The resulting shape will include areas that were contained only in the * first shape and not in the second shape. *
shape1 - shape2 = result +----------------+ +----------------+ +----------------+ |################| |################| | | |############## | | ##############| |## | |############ | | ############| |#### | |########## | | ##########| |###### | |######## | | ########| |######## | |###### | | ######| |###### | |#### | | ####| |#### | |## | | ##| |## | +----------------+ +----------------+ +----------------+* @param shape1 the first shape * @param shape2 the second shape * @return the created {@code Shape} */ public static Shape subtract(final Shape shape1, final Shape shape2) { final Area result = shape1.getTransformedArea(); result.subtract(shape2.getTransformedArea()); return createFromGeomShape(result); } // PENDING_DOC_REVIEW /** * Returns a new {@code Shape} which is created as an intersection of the * specified input shapes. *
* The operation works with geometric areas occupied by the input shapes. * For a single {@code Shape} such area includes the area occupied by the * fill if the shape has a non-null fill and the area occupied by the stroke * if the shape has a non-null stroke. So the area is empty for a shape * with {@code null} stroke and {@code null} fill. The area of an input * shape considered by the operation is independent on the type and * configuration of the paint used for fill or stroke. Before the final * operation the areas of the input shapes are transformed to the parent * coordinate space of their respective topmost parent nodes. *
* The resulting shape will include only areas that were contained in both * of the input shapes. *
shape1 + shape2 = result +----------------+ +----------------+ +----------------+ |################| |################| |################| |############## | | ##############| | ############ | |############ | | ############| | ######## | |########## | | ##########| | #### | |######## | | ########| | | |###### | | ######| | | |#### | | ####| | | |## | | ##| | | +----------------+ +----------------+ +----------------+* @param shape1 the first shape * @param shape2 the second shape * @return the created {@code Shape} */ public static Shape intersect(final Shape shape1, final Shape shape2) { final Area result = shape1.getTransformedArea(); result.intersect(shape2.getTransformedArea()); return createFromGeomShape(result); } private Area getTransformedArea() { return getTransformedArea(calculateNodeToSceneTransform(this)); } private Area getTransformedArea(final BaseTransform transform) { if (impl_mode == NGShape.Mode.EMPTY) { return new Area(); } final com.sun.javafx.geom.Shape fillShape = impl_configShape(); if ((impl_mode == NGShape.Mode.FILL) || (impl_mode == NGShape.Mode.STROKE_FILL) && (getStrokeType() == StrokeType.INSIDE)) { return createTransformedArea(fillShape, transform); } final StrokeType strokeType = getStrokeType(); final double strokeWidth = Utils.clampMin(getStrokeWidth(), MIN_STROKE_WIDTH); final StrokeLineCap strokeLineCap = getStrokeLineCap(); final StrokeLineJoin strokeLineJoin = convertLineJoin(getStrokeLineJoin()); final float strokeMiterLimit = (float) Utils.clampMin(getStrokeMiterLimit(), MIN_STROKE_MITER_LIMIT); final float[] dashArray = (hasStrokeDashArray()) ? toPGDashArray(getStrokeDashArray()) : DEFAULT_PG_STROKE_DASH_ARRAY; final com.sun.javafx.geom.Shape strokeShape = Toolkit.getToolkit().createStrokedShape( fillShape, strokeType, strokeWidth, strokeLineCap, strokeLineJoin, strokeMiterLimit, dashArray, (float) getStrokeDashOffset()); if (impl_mode == NGShape.Mode.STROKE) { return createTransformedArea(strokeShape, transform); } // fill and stroke final Area combinedArea = new Area(fillShape); combinedArea.add(new Area(strokeShape)); return createTransformedArea(combinedArea, transform); } private static BaseTransform calculateNodeToSceneTransform(Node node) { final Affine3D cumulativeTransformation = new Affine3D(); do { cumulativeTransformation.preConcatenate( node.impl_getLeafTransform()); node = node.getParent(); } while (node != null); return cumulativeTransformation; } private static Area createTransformedArea( final com.sun.javafx.geom.Shape geomShape, final BaseTransform transform) { return transform.isIdentity() ? new Area(geomShape) : new Area(geomShape.getPathIterator(transform)); } private static Path createFromGeomShape( final com.sun.javafx.geom.Shape geomShape) { final Path path = new Path(); final ObservableList