/* * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org * Copyright (c) 1998-2008, University of Maryland * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted provided * that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this list of conditions * and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions * and the following disclaimer in the documentation and/or other materials provided with the * distribution. * * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its * contributors may be used to endorse or promote products derived from this software without specific * prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package edu.umd.cs.piccolo.nodes; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Shape; import java.awt.Stroke; import java.awt.geom.Ellipse2D; import java.awt.geom.GeneralPath; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import edu.umd.cs.piccolo.PNode; import edu.umd.cs.piccolo.activities.PColorActivity; import edu.umd.cs.piccolo.activities.PInterpolatingActivity; import edu.umd.cs.piccolo.util.PAffineTransform; import edu.umd.cs.piccolo.util.PPaintContext; import edu.umd.cs.piccolo.util.PUtil; /** * <b>PPath</b> is a wrapper around a java.awt.geom.GeneralPath. The setBounds * method works by scaling the path to fit into the specified bounds. This * normally works well, but if the specified base bounds get too small then it * is impossible to expand the path shape again since all its numbers have * tended to zero, so application code may need to take this into consideration. * <P> * One option that applications have is to call <code>startResizeBounds</code> * before starting an interaction that may make the bounds very small, and * calling <code>endResizeBounds</code> when this interaction is finished. When * this is done PPath will use a copy of the original path to do the resizing so * the numbers in the path wont loose resolution. * <P> * This class also provides methods for constructing common shapes using a * general path. * <P> * * @version 1.0 * @author Jesse Grosjean */ public class PPath extends PNode { /** * Allows for future serialization code to understand versioned binary * formats. */ private static final long serialVersionUID = 1L; /** * The property name that identifies a change of this node's stroke paint * (see {@link #getStrokePaint getStrokePaint}). Both old and new value will * be set correctly to Paint objects in any property change event. */ public static final String PROPERTY_STROKE_PAINT = "strokePaint"; /** * The property code that identifies a change of this node's stroke paint * (see {@link #getStrokePaint getStrokePaint}). Both old and new value will * be set correctly to Paint objects in any property change event. */ public static final int PROPERTY_CODE_STROKE_PAINT = 1 << 16; /** * The property name that identifies a change of this node's stroke (see * {@link #getStroke getStroke}). Both old and new value will be set * correctly to Stroke objects in any property change event. */ public static final String PROPERTY_STROKE = "stroke"; /** * The property code that identifies a change of this node's stroke (see * {@link #getStroke getStroke}). Both old and new value will be set * correctly to Stroke objects in any property change event. */ public static final int PROPERTY_CODE_STROKE = 1 << 17; /** * The property name that identifies a change of this node's path (see * {@link #getPathReference getPathReference}). In any property change event * the new value will be a reference to this node's path, but old value will * always be null. */ public static final String PROPERTY_PATH = "path"; /** * The property code that identifies a change of this node's path (see * {@link #getPathReference getPathReference}). In any property change event * the new value will be a reference to this node's path, but old value will * always be null. */ public static final int PROPERTY_CODE_PATH = 1 << 18; private static final Rectangle2D.Float TEMP_RECTANGLE = new Rectangle2D.Float(); private static final RoundRectangle2D.Float TEMP_ROUNDRECTANGLE = new RoundRectangle2D.Float(); private static final Ellipse2D.Float TEMP_ELLIPSE = new Ellipse2D.Float(); private static final PAffineTransform TEMP_TRANSFORM = new PAffineTransform(); private static final BasicStroke DEFAULT_STROKE = new BasicStroke(1.0f); private static final Color DEFAULT_STROKE_PAINT = Color.black; private transient GeneralPath path; private transient GeneralPath resizePath; private transient Stroke stroke; private transient boolean updatingBoundsFromPath; private Paint strokePaint; /** * Creates a PPath object in the shape of a rectangle. * * @param x left of the rectangle * @param y top of the rectangle * @param width width of the rectangle * @param height height of the rectangle * * @return created rectangle */ public static PPath createRectangle(final float x, final float y, final float width, final float height) { TEMP_RECTANGLE.setFrame(x, y, width, height); final PPath result = new PPath(TEMP_RECTANGLE); result.setPaint(Color.white); return result; } /** * Creates a PPath object in the shape of a rounded rectangle. * * @param x left of the rectangle * @param y top of the rectangle * @param width width of the rectangle * @param height height of the rectangle * @param arcWidth the arc width at the corners of the rectangle * @param arcHeight the arc height at the corners of the rectangle * * @return created rounded rectangle */ public static PPath createRoundRectangle(final float x, final float y, final float width, final float height, final float arcWidth, final float arcHeight) { TEMP_ROUNDRECTANGLE.setRoundRect(x, y, width, height, arcWidth, arcHeight); final PPath result = new PPath(TEMP_ROUNDRECTANGLE); result.setPaint(Color.white); return result; } /** * Creates a PPath object in the shape of an ellipse. * * @param x left of the ellipse * @param y top of the ellipse * @param width width of the ellipse * @param height height of the ellipse * * @return created ellipse */ public static PPath createEllipse(final float x, final float y, final float width, final float height) { TEMP_ELLIPSE.setFrame(x, y, width, height); final PPath result = new PPath(TEMP_ELLIPSE); result.setPaint(Color.white); return result; } /** * Creates a PPath in the shape of a line. * * @param x1 x component of the first point * @param y1 y component of the first point * @param x2 x component of the second point * @param y2 y component of the second point * * @return created line */ public static PPath createLine(final float x1, final float y1, final float x2, final float y2) { final PPath result = new PPath(); result.moveTo(x1, y1); result.lineTo(x2, y2); result.setPaint(Color.white); return result; } /** * Creates a PPath for the poly-line for the given points. * * @param points array of points for the point lines * * @return created poly-line for the given points */ public static PPath createPolyline(final Point2D[] points) { final PPath result = new PPath(); result.setPathToPolyline(points); result.setPaint(Color.white); return result; } /** * Creates a PPath for the poly-line for the given points. * * @param xp array of x components of the points of the poly-lines * @param yp array of y components of the points of the poly-lines * * @return created poly-line for the given points */ public static PPath createPolyline(final float[] xp, final float[] yp) { final PPath result = new PPath(); result.setPathToPolyline(xp, yp); result.setPaint(Color.white); return result; } /** * Creates an empty PPath with the default paint and stroke. */ public PPath() { strokePaint = DEFAULT_STROKE_PAINT; stroke = DEFAULT_STROKE; path = new GeneralPath(); } /** * Creates an PPath in the given shape with the default paint and stroke. * * @param aShape the desired shape */ public PPath(final Shape aShape) { this(aShape, DEFAULT_STROKE); } /** * Construct this path with the given shape and stroke. This method may be * used to optimize the creation of a large number of PPaths. Normally * PPaths have a default stroke of width one, but when a path has a non null * stroke it takes significantly longer to compute its bounds. This method * allows you to override that default stroke before the bounds are ever * calculated, so if you pass in a null stroke here you won't ever have to * pay that bounds calculation price if you don't need to. * * @param aShape desired shape or null if you desire an empty path * @param aStroke desired stroke */ public PPath(final Shape aShape, final Stroke aStroke) { this(); stroke = aStroke; if (aShape != null) { append(aShape, false); } } /** * Returns the stroke paint of the PPath. * * @return stroke paint of the PPath */ public Paint getStrokePaint() { return strokePaint; } /** * Sets the stroke paint of the path. * * @param newStrokePaint the paint to use as this path's stroke paint */ public void setStrokePaint(final Paint newStrokePaint) { final Paint oldStrokePaint = strokePaint; strokePaint = newStrokePaint; invalidatePaint(); firePropertyChange(PROPERTY_CODE_STROKE_PAINT, PROPERTY_STROKE_PAINT, oldStrokePaint, strokePaint); } /** * Returns the stroke to use when drawing the path. * * @return current stroke of path */ public Stroke getStroke() { return stroke; } /** * Sets the stroke to use when drawing the path. * * @param aStroke stroke to use when drawing the path */ public void setStroke(final Stroke aStroke) { final Stroke old = stroke; stroke = aStroke; updateBoundsFromPath(); invalidatePaint(); firePropertyChange(PROPERTY_CODE_STROKE, PROPERTY_STROKE, old, stroke); } /** Stores the original size of the path before resizing started. */ public void startResizeBounds() { resizePath = new GeneralPath(path); } /** Clears the size of the path before resizing. */ public void endResizeBounds() { resizePath = null; } /** * Set the bounds of this path. This method works by scaling the path to fit * into the specified bounds. This normally works well, but if the specified * base bounds get too small then it is impossible to expand the path shape * again since all its numbers have tended to zero, so application code may * need to take this into consideration. * * @param x new left position of bounds * @param y new top position of bounds * @param width the new width of the bounds * @param height the new height of the bounds */ protected void internalUpdateBounds(final double x, final double y, final double width, final double height) { if (updatingBoundsFromPath || path == null) { return; } if (resizePath != null) { path.reset(); path.append(resizePath, false); } final Rectangle2D pathBounds = path.getBounds2D(); final Rectangle2D pathStrokeBounds = getPathBoundsWithStroke(); final double strokeOutset = Math.max(pathStrokeBounds.getWidth() - pathBounds.getWidth(), pathStrokeBounds .getHeight() - pathBounds.getHeight()); double adjustedX = x + strokeOutset / 2; double adjustedY = y + strokeOutset / 2; double adjustedWidth = width - strokeOutset; double adjustedHeight = height - strokeOutset; final double scaleX; if (adjustedWidth == 0 || pathBounds.getWidth() == 0) { scaleX = 1; } else { scaleX = adjustedWidth / pathBounds.getWidth(); } final double scaleY; if (adjustedHeight == 0 || pathBounds.getHeight() == 0) { scaleY = 1; } else { scaleY = adjustedHeight / pathBounds.getHeight(); } TEMP_TRANSFORM.setToIdentity(); TEMP_TRANSFORM.translate(adjustedX, adjustedY); TEMP_TRANSFORM.scale(scaleX, scaleY); TEMP_TRANSFORM.translate(-pathBounds.getX(), -pathBounds.getY()); path.transform(TEMP_TRANSFORM); } /** * Returns true if path crosses the provided bounds. Takes visibility of * path into account. * * @param aBounds bounds being tested for intersection * @return true if path visibly crosses bounds */ public boolean intersects(final Rectangle2D aBounds) { if (super.intersects(aBounds)) { if (getPaint() != null && path.intersects(aBounds)) { return true; } else if (stroke != null && strokePaint != null) { return stroke.createStrokedShape(path).intersects(aBounds); } } return false; } /** * Calculates the path's bounds taking stroke into account. * * @return bounds of the path taking stroke width into account */ public Rectangle2D getPathBoundsWithStroke() { if (stroke != null) { return stroke.createStrokedShape(path).getBounds2D(); } else { return path.getBounds2D(); } } /** * Recomputes the bounds taking stroke into account. */ public void updateBoundsFromPath() { updatingBoundsFromPath = true; if (path == null) { resetBounds(); } else { final Rectangle2D b = getPathBoundsWithStroke(); setBounds(b.getX(), b.getY(), b.getWidth(), b.getHeight()); } updatingBoundsFromPath = false; } /** * Paints the path in the provided paintContext. Can perform very * differently depending on whether the path is being drawn using its stroke * or its paint. * * It both are provided to the path, fun ensues. * * @param paintContext context in which painting is occurring */ protected void paint(final PPaintContext paintContext) { final Paint p = getPaint(); final Graphics2D g2 = paintContext.getGraphics(); if (p != null) { g2.setPaint(p); g2.fill(path); } if (stroke != null && strokePaint != null) { g2.setPaint(strokePaint); g2.setStroke(stroke); g2.draw(path); } } /** * Provides direct access to the underlying GeneralPath object. * * @return underlying GeneralPath */ public GeneralPath getPathReference() { return path; } /** * Appends a "move" operation to the end of the path. * * @param x the x component of the point to move to * @param y the y component of the point to move to */ public void moveTo(final float x, final float y) { path.moveTo(x, y); firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path); updateBoundsFromPath(); invalidatePaint(); } /** * Draws a line from the last point in the path to point provided. * * @param x the x component of the point * @param y the y component of the point */ public void lineTo(final float x, final float y) { path.lineTo(x, y); firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path); updateBoundsFromPath(); invalidatePaint(); } /** * Adds a curved segment, defined by two new points, to the path by drawing * a Quadratic curve that intersects both the current coordinates and the * coordinates (x2, y2), using the specified point (x1, y1) as a quadratic * parametric control point. * * @param x1 x component of quadratic parametric control point * @param y1 y component of quadratic parametric control point * @param x2 x component of point through which quad curve will pass * @param y2 y component of point through which quad curve will pass */ public void quadTo(final float x1, final float y1, final float x2, final float y2) { path.quadTo(x1, y1, x2, y2); firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path); updateBoundsFromPath(); invalidatePaint(); } /** * Adds a curved segment, defined by three new points, to the path by * drawing a Bézier curve that intersects both the current coordinates and * the coordinates (x3, y3), using the specified points (x1, y1) and (x2, * y2) as Bézier control points. * * @param x1 x component of first Bézier control point * @param y1 y component of first Bézier control point * @param x2 x component of second Bézier control point * @param y2 y component of second Bézier control point * @param x3 x component of point through which curve must pass * @param y3 y component of point through which curve must pass */ public void curveTo(final float x1, final float y1, final float x2, final float y2, final float x3, final float y3) { path.curveTo(x1, y1, x2, y2, x3, y3); firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path); updateBoundsFromPath(); invalidatePaint(); } /** * Appends the provided shape to the end of this path, it may conditionally * connect them together if they are disjoint. * * @param aShape shape to append * @param connect whether to perform a lineTo operation to the beginning of * the shape before appending */ public void append(final Shape aShape, final boolean connect) { path.append(aShape, connect); firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path); updateBoundsFromPath(); invalidatePaint(); } /** * Replaces this PPath's path with the one provided. * * @param aShape shape to replace the current one with */ public void setPathTo(final Shape aShape) { path.reset(); append(aShape, false); } /** * Resets the path to a rectangle with the dimensions and position provided. * * @param x left of the rectangle * @param y top of te rectangle * @param width width of the rectangle * @param height height of the rectangle */ public void setPathToRectangle(final float x, final float y, final float width, final float height) { TEMP_RECTANGLE.setFrame(x, y, width, height); setPathTo(TEMP_RECTANGLE); } /** * Resets the path to an ellipse positioned at the coordinate provided with * the dimensions provided. * * @param x left of the ellipse * @param y top of the ellipse * @param width width of the ellipse * @param height height of the ellipse */ public void setPathToEllipse(final float x, final float y, final float width, final float height) { TEMP_ELLIPSE.setFrame(x, y, width, height); setPathTo(TEMP_ELLIPSE); } /** * Sets the path to a sequence of segments described by the points. * * @param points points to that lie along the generated path */ public void setPathToPolyline(final Point2D[] points) { path.reset(); path.moveTo((float) points[0].getX(), (float) points[0].getY()); for (int i = 1; i < points.length; i++) { path.lineTo((float) points[i].getX(), (float) points[i].getY()); } firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path); updateBoundsFromPath(); invalidatePaint(); } /** * Sets the path to a sequence of segments described by the point components * provided. * * @param xp the x components of the points along the path * @param yp the y components of the points along the path */ public void setPathToPolyline(final float[] xp, final float[] yp) { path.reset(); path.moveTo(xp[0], yp[0]); for (int i = 1; i < xp.length; i++) { path.lineTo(xp[i], yp[i]); } firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path); updateBoundsFromPath(); invalidatePaint(); } /** * Marks the path as closed. Making changes to it impossible. */ public void closePath() { path.closePath(); firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path); updateBoundsFromPath(); invalidatePaint(); } /** * Empties the path. */ public void reset() { path.reset(); firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path); updateBoundsFromPath(); invalidatePaint(); } /** * Writes this PPath object to the output stream provided. Necessary since * stroke and path are not serializable by default. * * @param out output stream into which objects are to be serialized * @throws IOException if serialiazing to output stream fails */ private void writeObject(final ObjectOutputStream out) throws IOException { out.defaultWriteObject(); PUtil.writeStroke(stroke, out); PUtil.writePath(path, out); } /** * Deserializes a PPath object from the provided input stream. This method * is required since Strokes and GeneralPaths are not serializable by * default. * * @param in stream from which to read this PPath's state * @throws IOException when exception occurs reading from input stream * @throws ClassNotFoundException */ private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); stroke = PUtil.readStroke(in); path = PUtil.readPath(in); } /** * added by miura 2012/11/29 * @param destColor * @param duration * @return */ public PInterpolatingActivity animateToStrokeColor(final Color destColor, final long duration) { if (duration == 0) { setPaint(destColor); return null; } else { final PColorActivity.Target t = new PColorActivity.Target() { public Color getColor() { return (Color) getStrokePaint(); } public void setColor(final Color color) { setStrokePaint(color); } }; final PColorActivity ca = new PColorActivity(duration, PUtil.DEFAULT_ACTIVITY_STEP_RATE, t, destColor); addActivity(ca); return ca; } } }