/* * Copyright (c) 2008, 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.piccolox.util; import java.awt.BasicStroke; import java.awt.Shape; import java.awt.Stroke; import java.awt.geom.GeneralPath; import java.awt.geom.PathIterator; import java.awt.geom.Rectangle2D; import java.io.Serializable; import sun.dc.path.FastPathProducer; import sun.dc.path.PathConsumer; import sun.dc.path.PathException; import sun.dc.pr.PathDasher; import sun.dc.pr.PathStroker; import sun.dc.pr.Rasterizer; import edu.umd.cs.piccolo.util.PAffineTransform; import edu.umd.cs.piccolo.util.PDebug; import edu.umd.cs.piccolo.util.PPaintContext; import edu.umd.cs.piccolo.util.PPickPath; /** * <b>PFixedWidthStroke</b> is the same as {@link java.awt.BasicStroke} except * that PFixedWidthStroke has a fixed width on the screen so that even when the * canvas view is zooming its width stays the same in canvas coordinates. Note * that this stroke draws in the inside of the stroked shape, instead of the * normal draw on center behavior. * <P> * * @see edu.umd.cs.piccolo.nodes.PPath * @version 1.0 * @author Jesse Grosjean */ public class PFixedWidthStroke implements Stroke, Serializable { private static PAffineTransform TEMP_TRANSFORM = new PAffineTransform(); private static GeneralPath TEMP_PATH = new GeneralPath(GeneralPath.WIND_NON_ZERO); final static int JOIN_MITER = BasicStroke.JOIN_MITER; final static int JOIN_ROUND = BasicStroke.JOIN_ROUND; final static int JOIN_BEVEL = BasicStroke.JOIN_BEVEL; final static int CAP_BUTT = BasicStroke.CAP_BUTT; final static int CAP_ROUND = BasicStroke.CAP_ROUND; final static int CAP_SQUARE = BasicStroke.CAP_SQUARE; private float width; private int join; private int cap; private float miterlimit; private float dash[]; private float dash_phase; private static final int RasterizerCaps[] = { Rasterizer.BUTT, Rasterizer.ROUND, Rasterizer.SQUARE }; private static final int RasterizerCorners[] = { Rasterizer.MITER, Rasterizer.ROUND, Rasterizer.BEVEL }; private class FillAdapter implements PathConsumer { boolean closed; GeneralPath path; public FillAdapter() { path = TEMP_PATH; path.reset(); } public Shape getShape() { return path; } public void beginPath() { } public void beginSubpath(float x0, float y0) { if (closed) { path.closePath(); closed = false; } path.moveTo(x0, y0); } public void appendLine(float x1, float y1) { path.lineTo(x1, y1); } public void appendQuadratic(float xm, float ym, float x1, float y1) { path.quadTo(xm, ym, x1, y1); } public void appendCubic(float xm, float ym, float xn, float yn, float x1, float y1) { path.curveTo(xm, ym, xn, yn, x1, y1); } public void closedSubpath() { closed = true; } public void endPath() { if (closed) { path.closePath(); closed = false; } } public void useProxy(FastPathProducer proxy) throws PathException { proxy.sendTo(this); } public long getCPathConsumer() { return 0; } public void dispose() { } public PathConsumer getConsumer() { return null; } } public PFixedWidthStroke() { this(1.0f, CAP_SQUARE, JOIN_MITER, 10.0f, null, 0.0f); } public PFixedWidthStroke(float width) { this(width, CAP_SQUARE, JOIN_MITER, 10.0f, null, 0.0f); } public PFixedWidthStroke(float width, int cap, int join) { this(width, cap, join, 10.0f, null, 0.0f); } public PFixedWidthStroke(float width, int cap, int join, float miterlimit) { this(width, cap, join, miterlimit, null, 0.0f); } public PFixedWidthStroke(float width, int cap, int join, float miterlimit, float dash[], float dash_phase) { if (width < 0.0f) { throw new IllegalArgumentException("negative width"); } if (cap != CAP_BUTT && cap != CAP_ROUND && cap != CAP_SQUARE) { throw new IllegalArgumentException("illegal end cap value"); } if (join == JOIN_MITER) { if (miterlimit < 1.0f) { throw new IllegalArgumentException("miter limit < 1"); } } else if (join != JOIN_ROUND && join != JOIN_BEVEL) { throw new IllegalArgumentException("illegal line join value"); } if (dash != null) { if (dash_phase < 0.0f) { throw new IllegalArgumentException("negative dash phase"); } boolean allzero = true; for (int i = 0; i < dash.length; i++) { float d = dash[i]; if (d > 0.0) { allzero = false; } else if (d < 0.0) { throw new IllegalArgumentException("negative dash length"); } } if (allzero) { throw new IllegalArgumentException("dash lengths all zero"); } } this.width = width; this.cap = cap; this.join = join; this.miterlimit = miterlimit; if (dash != null) { this.dash = (float[]) dash.clone(); } this.dash_phase = dash_phase; } public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } public Shape createStrokedShape(Shape s) { FillAdapter filler = new FillAdapter(); PathStroker stroker = new PathStroker(filler); PathConsumer consumer; // Fixed Width Additions, always stroke path inside shape. // Also keeps dashes fixed when zooming (thanks to Shawn Castrianni - // Dec 2006) float fixedScale = 1.0f; if (PDebug.getProcessingOutput()) { if (PPaintContext.CURRENT_PAINT_CONTEXT != null) { fixedScale = 1.0f / (float) PPaintContext.CURRENT_PAINT_CONTEXT.getScale(); } } else { if (PPickPath.CURRENT_PICK_PATH != null) { fixedScale = 1.0f / (float) PPickPath.CURRENT_PICK_PATH.getScale(); } } float fixedWidth = width * fixedScale; Rectangle2D bounds = s.getBounds2D(); double scale = 1.0; if (bounds.getWidth() > bounds.getHeight()) { if (bounds.getWidth() != 0) { scale = (bounds.getWidth() - fixedWidth) / bounds.getWidth(); } } else { if (bounds.getHeight() != 0) { scale = (bounds.getHeight() - fixedWidth) / bounds.getHeight(); } } TEMP_TRANSFORM.setToIdentity(); TEMP_TRANSFORM.scaleAboutPoint(scale, bounds.getCenterX(), bounds.getCenterY()); stroker.setPenDiameter(fixedWidth); PathIterator pi = s.getPathIterator(TEMP_TRANSFORM); stroker.setPenT4(null); stroker.setCaps(RasterizerCaps[cap]); stroker.setCorners(RasterizerCorners[join], miterlimit); if (dash != null) { // Fixed Width Additions float fixedDash[] = new float[dash.length]; for (int i = 0; i < dash.length; i++) { fixedDash[i] = dash[i] * fixedScale; } float fixedDashPhase = dash_phase * fixedScale; PathDasher dasher = new PathDasher(stroker); dasher.setDash(fixedDash, fixedDashPhase); dasher.setDashT4(null); consumer = dasher; } else { consumer = stroker; } try { consumer.beginPath(); boolean pathClosed = false; float mx = 0.0f; float my = 0.0f; float point[] = new float[6]; while (!pi.isDone()) { int type = pi.currentSegment(point); if (pathClosed == true) { pathClosed = false; if (type != PathIterator.SEG_MOVETO) { // Force current point back to last moveto point consumer.beginSubpath(mx, my); } } switch (type) { case PathIterator.SEG_MOVETO: mx = point[0]; my = point[1]; consumer.beginSubpath(point[0], point[1]); break; case PathIterator.SEG_LINETO: consumer.appendLine(point[0], point[1]); break; case PathIterator.SEG_QUADTO: // Quadratic curves take two points consumer.appendQuadratic(point[0], point[1], point[2], point[3]); break; case PathIterator.SEG_CUBICTO: // Cubic curves take three points consumer.appendCubic(point[0], point[1], point[2], point[3], point[4], point[5]); break; case PathIterator.SEG_CLOSE: consumer.closedSubpath(); pathClosed = true; break; } pi.next(); } consumer.endPath(); consumer.dispose(); // hack to fix memory leak, shouldn't be // neccessary but is. } catch (PathException e) { throw new InternalError("Unable to Stroke shape (" + e.getMessage() + ")"); } return filler.getShape(); } public boolean equals(Object obj) { if (!(obj instanceof PFixedWidthStroke)) { return false; } PFixedWidthStroke bs = (PFixedWidthStroke) obj; if (width != bs.width) { return false; } if (join != bs.join) { return false; } if (cap != bs.cap) { return false; } if (miterlimit != bs.miterlimit) { return false; } if (dash != null) { if (dash_phase != bs.dash_phase) { return false; } if (!java.util.Arrays.equals(dash, bs.dash)) { return false; } } else if (bs.dash != null) { return false; } return true; } private float[] getDashArray() { if (dash == null) { return null; } return (float[]) dash.clone(); } private float getDashPhase() { return dash_phase; } private int getEndCap() { return cap; } private int getLineJoin() { return join; } private float getLineWidth() { return width; } private float getMiterLimit() { return miterlimit; } public int hashCode() { int hash = Float.floatToIntBits(width); hash = hash * 31 + join; hash = hash * 31 + cap; hash = hash * 31 + Float.floatToIntBits(miterlimit); if (dash != null) { hash = hash * 31 + Float.floatToIntBits(dash_phase); for (int i = 0; i < dash.length; i++) { hash = hash * 31 + Float.floatToIntBits(dash[i]); } } return hash; } }