/* * Copyright (c) 2002-@year@, 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. * * Neither the name of the University of Maryland nor 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. * * Piccolo was written at the Human-Computer Interaction Laboratory www.cs.umd.edu/hcil by Jesse Grosjean * under the supervision of Ben Bederson. The Piccolo website is www.cs.umd.edu/hcil/piccolo. */ 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.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 { /** * 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"; 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"; 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"; 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; public static PPath createRectangle(float x, float y, float width, float height) { TEMP_RECTANGLE.setFrame(x, y, width, height); PPath result = new PPath(TEMP_RECTANGLE); result.setPaint(Color.white); return result; } public static PPath createRoundRectangle(float x, float y, float width, float height, float arcWidth, float arcHeight) { TEMP_ROUNDRECTANGLE.setRoundRect(x, y, width, height, arcWidth, arcHeight); PPath result = new PPath(TEMP_ROUNDRECTANGLE); result.setPaint(Color.white); return result; } public static PPath createEllipse(float x, float y, float width, float height) { TEMP_ELLIPSE.setFrame(x, y, width, height); PPath result = new PPath(TEMP_ELLIPSE); result.setPaint(Color.white); return result; } public static PPath createLine(float x1, float y1, float x2, float y2) { PPath result = new PPath(); result.moveTo(x1, y1); result.lineTo(x2, y2); result.setPaint(Color.white); return result; } public static PPath createPolyline(Point2D[] points) { PPath result = new PPath(); result.setPathToPolyline(points); result.setPaint(Color.white); return result; } public static PPath createPolyline(float[] xp, float[] yp) { PPath result = new PPath(); result.setPathToPolyline(xp, yp); result.setPaint(Color.white); return result; } public PPath() { strokePaint = DEFAULT_STROKE_PAINT; stroke = DEFAULT_STROKE; path = new GeneralPath(); } public PPath(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. */ public PPath(Shape aShape, Stroke aStroke) { this(); stroke = aStroke; if (aShape != null) append(aShape, false); } //**************************************************************** // Stroke //**************************************************************** public Paint getStrokePaint() { return strokePaint; } public void setStrokePaint(Paint aPaint) { Paint old = strokePaint; strokePaint = aPaint; invalidatePaint(); firePropertyChange(PROPERTY_CODE_STROKE_PAINT ,PROPERTY_STROKE_PAINT, old, strokePaint); } public Stroke getStroke() { return stroke; } public void setStroke(Stroke aStroke) { Stroke old = stroke; stroke = aStroke; updateBoundsFromPath(); invalidatePaint(); firePropertyChange(PROPERTY_CODE_STROKE ,PROPERTY_STROKE, old, stroke); } //**************************************************************** // Bounds //**************************************************************** public void startResizeBounds() { resizePath = new GeneralPath(path); } 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. */ protected void internalUpdateBounds(double x, double y, double width, double height) { if (updatingBoundsFromPath) return; if (path == null) return; if (resizePath != null) { path.reset(); path.append(resizePath, false); } Rectangle2D pathBounds = path.getBounds2D(); Rectangle2D pathStrokeBounds = getPathBoundsWithStroke(); double strokeOutset = Math.max(pathStrokeBounds.getWidth() - pathBounds.getWidth(), pathStrokeBounds.getHeight() - pathBounds.getHeight()); x += strokeOutset / 2; y += strokeOutset / 2; width -= strokeOutset; height -= strokeOutset; double scaleX = (width == 0 || pathBounds.getWidth() == 0) ? 1 : width / pathBounds.getWidth(); double scaleY = (height == 0 || pathBounds.getHeight() == 0) ? 1 : height / pathBounds.getHeight(); TEMP_TRANSFORM.setToIdentity(); TEMP_TRANSFORM.translate(x, y); TEMP_TRANSFORM.scale(scaleX, scaleY); TEMP_TRANSFORM.translate(-pathBounds.getX(), -pathBounds.getY()); path.transform(TEMP_TRANSFORM); } public boolean intersects(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; } public Rectangle2D getPathBoundsWithStroke() { if (stroke != null) { return stroke.createStrokedShape(path).getBounds2D(); } else { return path.getBounds2D(); } } public void updateBoundsFromPath() { updatingBoundsFromPath = true; if (path == null) { resetBounds(); } else { Rectangle2D b = getPathBoundsWithStroke(); setBounds(b.getX(), b.getY(), b.getWidth(), b.getHeight()); } updatingBoundsFromPath = false; } //**************************************************************** // Painting //**************************************************************** protected void paint(PPaintContext paintContext) { Paint p = getPaint(); 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); } } //**************************************************************** // Path Support set java.awt.GeneralPath documentation for more // information on using these methods. //**************************************************************** public GeneralPath getPathReference() { return path; } public void moveTo(float x, float y) { path.moveTo(x, y); firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path); updateBoundsFromPath(); invalidatePaint(); } public void lineTo(float x, float y) { path.lineTo(x, y); firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path); updateBoundsFromPath(); invalidatePaint(); } public void quadTo(float x1, float y1, float x2, float y2) { path.quadTo(x1, y1, x2, y2); firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path); updateBoundsFromPath(); invalidatePaint(); } public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) { path.curveTo(x1, y1, x2, y2, x3, y3); firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path); updateBoundsFromPath(); invalidatePaint(); } public void append(Shape aShape, boolean connect) { path.append(aShape, connect); firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path); updateBoundsFromPath(); invalidatePaint(); } public void setPathTo(Shape aShape) { path.reset(); append(aShape, false); } public void setPathToRectangle(float x, float y, float width, float height) { TEMP_RECTANGLE.setFrame(x, y, width, height); setPathTo(TEMP_RECTANGLE); } public void setPathToEllipse(float x, float y, float width, float height) { TEMP_ELLIPSE.setFrame(x, y, width, height); setPathTo(TEMP_ELLIPSE); } public void setPathToPolyline(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(); } public void setPathToPolyline(float[] xp, 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(); } public void closePath() { path.closePath(); firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path); updateBoundsFromPath(); invalidatePaint(); } public void reset() { path.reset(); firePropertyChange(PROPERTY_CODE_PATH, PROPERTY_PATH, null, path); updateBoundsFromPath(); invalidatePaint(); } //**************************************************************** // Serialization //**************************************************************** private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); PUtil.writeStroke(stroke, out); PUtil.writePath(path, out); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); stroke = PUtil.readStroke(in); path = PUtil.readPath(in); } //**************************************************************** // Debugging - methods for debugging //**************************************************************** /** * Returns a string representing the state of this node. This method is * intended to be used only for debugging purposes, and the content and * format of the returned string may vary between implementations. The * returned string may be empty but may not be <code>null</code>. * * @return a string representation of this node's state */ protected String paramString() { StringBuffer result = new StringBuffer(); result.append("path=" + (path == null ? "null" : path.toString())); result.append(",stroke=" + (stroke == null ? "null" : stroke.toString())); result.append(",strokePaint=" + (strokePaint == null ? "null" : strokePaint.toString())); result.append(','); result.append(super.paramString()); return result.toString(); } }