/* * @(#)BezierFigure.java * * Copyright (c) 1996-2010 The authors and contributors of JHotDraw. * You may not use, copy or modify this file, except in compliance with the * accompanying license terms. */ package org.jhotdraw.draw; import org.jhotdraw.geom.GrowStroke; import org.jhotdraw.geom.Geom; import org.jhotdraw.geom.BezierPath; import javax.annotation.Nullable; import org.jhotdraw.draw.connector.Connector; import org.jhotdraw.draw.connector.ChopBezierConnector; import org.jhotdraw.draw.handle.TransformHandleKit; import org.jhotdraw.draw.handle.BezierNodeHandle; import org.jhotdraw.draw.handle.BezierOutlineHandle; import org.jhotdraw.draw.handle.BezierScaleHandle; import org.jhotdraw.util.*; import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import java.util.*; import javax.swing.undo.*; import java.io.*; import org.jhotdraw.draw.handle.Handle; import static org.jhotdraw.draw.AttributeKeys.*; import org.jhotdraw.xml.DOMInput; import org.jhotdraw.xml.DOMOutput; /** * A {@link Figure} which draws an opened or a closed bezier path. * <p> * A bezier figure can be used to draw arbitrary shapes using a {@link BezierPath}. It can be used * to draw an open path or a closed shape. * <p> * A BezierFigure can have straight path segments and curved segments. A straight path segment can * be added by clicking on the drawing area. Curved segments can be added by dragging the mouse * pointer over the drawing area. * * <hr> * <b>Design Patterns</b> * * <p> * <em>Decorator</em><br> * The start and end point of a {@code BezierFigure} can be decorated with a line decoration.<br> * Component: {@link BezierFigure}; Decorator: {@link org.jhotdraw.draw.decoration.LineDecoration}. * <hr> * * @see org.jhotdraw.geom.BezierPath * * @author Werner Randelshofer * @version $Id$ */ public class BezierFigure extends AbstractAttributedFigure { private static final long serialVersionUID = 1L; /** * The BezierPath. */ protected BezierPath path; /** * The cappedPath BezierPath is derived from variable path. We cache it to increase the drawing * speed of the figure. */ @Nullable private transient BezierPath cappedPath; /** * Creates an empty <code>BezierFigure</code>, for example without any * <code>BezierPath.Node</code>s. The BezierFigure will not draw anything, if at least two nodes * are added to it. The <code>BezierPath</code> created by this constructor is not closed. */ public BezierFigure() { this(false); } /** * Creates an empty BezierFigure, for example without any <code>BezierPath.Node</code>s. The * BezierFigure will not draw anything, unless at least two nodes are added to it. * * @param isClosed Specifies whether the <code>BezierPath</code> shall be closed. */ public BezierFigure(boolean isClosed) { path = new BezierPath(); set(PATH_CLOSED, isClosed); //path.setClosed(isClosed); } // DRAWING // SHAPE AND BOUNDS // ATTRIBUTES // EDITING // CONNECTING /** * Returns the Figures connector for the specified location. By default a * {@link ChopBezierConnector} is returned. */ @Override public Connector findConnector(Point2D.Double p, ConnectionFigure prototype) { return new ChopBezierConnector(this); } @Override public Connector findCompatibleConnector(Connector c, boolean isStart) { return new ChopBezierConnector(this); } // COMPOSITE FIGURES // CLONING // EVENT HANDLING @Override protected void drawStroke(Graphics2D g) { if (isClosed()) { double grow = AttributeKeys.getPerpendicularDrawGrowth(this, AttributeKeys.getScaleFactorFromGraphics(g)); if (grow == 0d) { g.draw(path); } else { GrowStroke gs = new GrowStroke(grow, AttributeKeys.getStrokeTotalWidth(this, AttributeKeys.getScaleFactorFromGraphics(g)) * get(STROKE_MITER_LIMIT)); g.draw(gs.createStrokedShape(path)); } } else { g.draw(getCappedPath()); } drawCaps(g); } protected void drawCaps(Graphics2D g) { if (getNodeCount() > 1) { if (get(START_DECORATION) != null) { BezierPath cp = getCappedPath(); Point2D.Double p1 = path.get(0, 0); Point2D.Double p2 = cp.get(0, 0); if (p2.equals(p1)) { p2 = path.get(1, 0); } get(START_DECORATION).draw(g, this, p1, p2); } if (get(END_DECORATION) != null) { BezierPath cp = getCappedPath(); Point2D.Double p1 = path.get(path.size() - 1, 0); Point2D.Double p2 = cp.get(path.size() - 1, 0); if (p2.equals(p1)) { p2 = path.get(path.size() - 2, 0); } get(END_DECORATION).draw(g, this, p1, p2); } } } @Override protected void drawFill(Graphics2D g) { if (isClosed() || get(UNCLOSED_PATH_FILLED)) { double grow = AttributeKeys.getPerpendicularFillGrowth(this, AttributeKeys.getScaleFactorFromGraphics(g)); if (grow == 0d) { g.fill(path); } else { GrowStroke gs = new GrowStroke(grow, AttributeKeys.getStrokeTotalWidth(this, AttributeKeys.getScaleFactorFromGraphics(g)) * get(STROKE_MITER_LIMIT)); g.fill(gs.createStrokedShape(path)); } } } @Override public boolean contains(Point2D.Double p) { double tolerance = Math.max(2f, AttributeKeys.getStrokeTotalWidth(this, 1.0) / 2d); if (isClosed() || get(FILL_COLOR) != null && get(UNCLOSED_PATH_FILLED)) { if (path.contains(p)) { return true; } double grow = AttributeKeys.getPerpendicularHitGrowth(this, 1.0) * 2d; GrowStroke gs = new GrowStroke(grow, AttributeKeys.getStrokeTotalWidth(this, 1.0) * get(STROKE_MITER_LIMIT)); if (gs.createStrokedShape(path).contains(p)) { return true; } else { if (isClosed()) { return false; } } } if (!isClosed()) { if (getCappedPath().outlineContains(p, tolerance)) { return true; } if (get(START_DECORATION) != null) { BezierPath cp = getCappedPath(); Point2D.Double p1 = path.get(0, 0); Point2D.Double p2 = cp.get(0, 0); // FIXME - Check here, if caps path contains the point if (Geom.lineContainsPoint(p1.x, p1.y, p2.x, p2.y, p.x, p.y, tolerance)) { return true; } } if (get(END_DECORATION) != null) { BezierPath cp = getCappedPath(); Point2D.Double p1 = path.get(path.size() - 1, 0); Point2D.Double p2 = cp.get(path.size() - 1, 0); // FIXME - Check here, if caps path contains the point if (Geom.lineContainsPoint(p1.x, p1.y, p2.x, p2.y, p.x, p.y, tolerance)) { return true; } } } return false; } @Override public Collection<Handle> createHandles(int detailLevel) { LinkedList<Handle> handles = new LinkedList<>(); switch (detailLevel % 2) { case -1: // Mouse hover handles handles.add(new BezierOutlineHandle(this, true)); break; case 0: handles.add(new BezierOutlineHandle(this)); for (int i = 0, n = path.size(); i < n; i++) { handles.add(new BezierNodeHandle(this, i)); } break; case 1: TransformHandleKit.addTransformHandles(this, handles); handles.add(new BezierScaleHandle(this)); break; } return handles; } @Override public Rectangle2D.Double getBounds() { Rectangle2D.Double bounds = path.getBounds2D(); return bounds; } @Override public Rectangle2D.Double getDrawingArea(double factor) { Rectangle2D.Double r = super.getDrawingArea(factor); if (getNodeCount() > 1) { if (get(START_DECORATION) != null) { Point2D.Double p1 = getPoint(0, 0); Point2D.Double p2 = getPoint(1, 0); r.add(get(START_DECORATION).getDrawingArea(this, p1, p2)); } if (get(END_DECORATION) != null) { Point2D.Double p1 = getPoint(getNodeCount() - 1, 0); Point2D.Double p2 = getPoint(getNodeCount() - 2, 0); r.add(get(END_DECORATION).getDrawingArea(this, p1, p2)); } } return r; } @Override protected void validate() { super.validate(); path.invalidatePath(); cappedPath = null; } /** * Returns a clone of the bezier path of this figure. */ public BezierPath getBezierPath() { return path.clone(); } public void setBezierPath(BezierPath newValue) { path = newValue.clone(); this.setClosed(newValue.isClosed()); } public Point2D.Double getPointOnPath(float relative, double flatness) { return path.getPointOnPath(relative, flatness); } public boolean isClosed() { return get(PATH_CLOSED); } public void setClosed(boolean newValue) { set(PATH_CLOSED, newValue); setConnectable(newValue); } @Override public <T> void set(AttributeKey<T> key, T newValue) { if (key == PATH_CLOSED) { path.setClosed((Boolean) newValue); } else if (key == WINDING_RULE) { path.setWindingRule(newValue == AttributeKeys.WindingRule.EVEN_ODD ? Path2D.Double.WIND_EVEN_ODD : Path2D.Double.WIND_NON_ZERO); } super.set(key, newValue); invalidate(); } /** * Sets the location of the first and the last <code>BezierPath.Node</code> of the BezierFigure. * If the BezierFigure has not at least two nodes, nodes are added to the figure until the * BezierFigure has at least two nodes. */ @Override public void setBounds(Point2D.Double anchor, Point2D.Double lead) { setStartPoint(anchor); setEndPoint(lead); invalidate(); } @Override public void transform(AffineTransform tx) { path.transform(tx); invalidate(); } @Override public void invalidate() { super.invalidate(); path.invalidatePath(); cappedPath = null; } /** * Returns a path which is cappedPath at the ends, to prevent it from drawing under the end * caps. */ protected BezierPath getCappedPath() { if (cappedPath == null) { cappedPath = path.clone(); if (isClosed()) { cappedPath.setClosed(true); } else { if (cappedPath.size() > 1) { if (get(START_DECORATION) != null) { BezierPath.Node p0 = cappedPath.get(0); BezierPath.Node p1 = cappedPath.get(1); Point2D.Double pp; if ((p0.getMask() & BezierPath.C2_MASK) != 0) { pp = p0.getControlPoint(2); } else if ((p1.getMask() & BezierPath.C1_MASK) != 0) { pp = p1.getControlPoint(1); } else { pp = p1.getControlPoint(0); } double radius = get(START_DECORATION).getDecorationRadius(this); double lineLength = Geom.length(p0.getControlPoint(0), pp); cappedPath.set(0, 0, Geom.cap(pp, p0.getControlPoint(0), -Math.min(radius, lineLength))); } if (get(END_DECORATION) != null) { BezierPath.Node p0 = cappedPath.get(cappedPath.size() - 1); BezierPath.Node p1 = cappedPath.get(cappedPath.size() - 2); Point2D.Double pp; if ((p0.getMask() & BezierPath.C1_MASK) != 0) { pp = p0.getControlPoint(1); } else if ((p1.getMask() & BezierPath.C2_MASK) != 0) { pp = p1.getControlPoint(2); } else { pp = p1.getControlPoint(0); } double radius = get(END_DECORATION).getDecorationRadius(this); double lineLength = Geom.length(p0.getControlPoint(0), pp); cappedPath.set(cappedPath.size() - 1, 0, Geom.cap(pp, p0.getControlPoint(0), -Math.min(radius, lineLength))); } cappedPath.invalidatePath(); } } } return cappedPath; } public void layout() { } /** * Adds a control point. */ public void addNode(BezierPath.Node p) { addNode(getNodeCount(), p); } /** * Adds a node to the list of points. */ public void addNode(final int index, BezierPath.Node p) { path.add(index, p); invalidate(); } /** * Sets a control point. */ public void setNode(int index, BezierPath.Node p) { path.set(index, p); invalidate(); } /** * Gets a control point. */ public BezierPath.Node getNode(int index) { return (BezierPath.Node) path.get(index).clone(); } /** * Convenience method for getting the point coordinate of the first control point of the * specified node. */ public Point2D.Double getPoint(int index) { return path.get(index).getControlPoint(0); } /** * Gets the point coordinate of a control point. */ public Point2D.Double getPoint(int index, int coord) { return path.get(index).getControlPoint(coord); } /** * Sets the point coordinate of control point 0 at the specified node. */ public void setPoint(int index, Point2D.Double p) { BezierPath.Node node = path.get(index); double dx = p.x - node.x[0]; double dy = p.y - node.y[0]; for (int i = 0; i < node.x.length; i++) { node.x[i] += dx; node.y[i] += dy; } invalidate(); } /** * Sets the point coordinate of a control point. */ public void setPoint(int index, int coord, Point2D.Double p) { BezierPath.Node cp = new BezierPath.Node(path.get(index)); cp.setControlPoint(coord, p); setNode(index, cp); } /** * Convenience method for setting the point coordinate of the start point. If the BezierFigure * has not at least two nodes, nodes are added to the figure until the BezierFigure has at least * two nodes. */ public void setStartPoint(Point2D.Double p) { // Add two nodes if we haven't at least two nodes for (int i = getNodeCount(); i < 2; i++) { addNode(0, new BezierPath.Node(p.x, p.y)); } setPoint(0, p); } /** * Convenience method for setting the point coordinate of the end point. If the BezierFigure has * not at least two nodes, nodes are added to the figure until the BezierFigure has at least two * nodes. */ public void setEndPoint(Point2D.Double p) { // Add two nodes if we haven't at least two nodes for (int i = getNodeCount(); i < 2; i++) { addNode(0, new BezierPath.Node(p.x, p.y)); } setPoint(getNodeCount() - 1, p); } /** * Convenience method for getting the start point. */ @Override public Point2D.Double getStartPoint() { return getPoint(0, 0); } /** * Convenience method for getting the end point. */ @Override public Point2D.Double getEndPoint() { return getPoint(getNodeCount() - 1, 0); } /** * Finds a control point index. Returns -1 if no control point could be found. FIXME - Move this * to BezierPath */ public int findNode(Point2D.Double p) { BezierPath tp = path; for (int i = 0; i < tp.size(); i++) { BezierPath.Node p2 = tp.get(i); if (p2.x[0] == p.x && p2.y[0] == p.y) { return i; } } return -1; } /** * Gets the segment of the polyline that is hit by the given Point2D.Double. * * @param find a Point on the bezier path * @param tolerance a tolerance, tolerance should take into account the line width, plus 2 * divided by the zoom factor. * @return the index of the segment or -1 if no segment was hit. */ public int findSegment(Point2D.Double find, double tolerance) { return getBezierPath().findSegment(find, tolerance); } /** * Joins two segments into one if the given Point2D.Double hits a node of the polyline. * * @return true if the two segments were joined. * * @param join a Point at a node on the bezier path * @param tolerance a tolerance, tolerance should take into account the line width, plus 2 * divided by the zoom factor. */ public boolean joinSegments(Point2D.Double join, double tolerance) { int i = findSegment(join, tolerance); if (i != -1 && i > 1) { removeNode(i); return true; } return false; } /** * Splits the segment at the given Point2D.Double if a segment was hit. * * @return the index of the segment or -1 if no segment was hit. * * @param split a Point on (or near) a line segment on the bezier path * @param tolerance a tolerance, tolerance should take into account the line width, plus 2 * divided by the zoom factor. */ public int splitSegment(Point2D.Double split, double tolerance) { int i = findSegment(split, tolerance); if (i != -1) { addNode(i + 1, new BezierPath.Node(split)); } return i + 1; } /** * Removes the Node at the specified index. */ public BezierPath.Node removeNode(int index) { return path.remove(index); } /** * Removes the Point2D.Double at the specified index. */ protected void removeAllNodes() { path.clear(); } /** * Gets the node count. */ public int getNodeCount() { return path.size(); } @Override public BezierFigure clone() { BezierFigure that = (BezierFigure) super.clone(); that.path = this.path.clone(); that.invalidate(); return that; } @Override public void restoreTransformTo(Object geometry) { path.setTo((BezierPath) geometry); } @Override public Object getTransformRestoreData() { return path.clone(); } public Point2D.Double chop(Point2D.Double p) { if (isClosed()) { double grow = AttributeKeys.getPerpendicularHitGrowth(this, 1.0); if (grow == 0d) { return path.chop(p); } else { GrowStroke gs = new GrowStroke(grow, AttributeKeys.getStrokeTotalWidth(this, 1.0) * get(STROKE_MITER_LIMIT)); return Geom.chop(gs.createStrokedShape(path), p); } } else { return path.chop(p); } } public Point2D.Double getCenter() { return path.getCenter(); } public Point2D.Double getOutermostPoint() { return path.get(path.indexOfOutermostNode()).getControlPoint(0); } /** * Joins two segments into one if the given Point2D.Double hits a node of the polyline. * * @return true if the two segments were joined. */ public int joinSegments(Point2D.Double join, float tolerance) { return path.joinSegments(join, tolerance); } /** * Splits the segment at the given Point2D.Double if a segment was hit. * * @return the index of the segment or -1 if no segment was hit. */ public int splitSegment(Point2D.Double split, float tolerance) { return path.splitSegment(split, tolerance); } /** * Handles a mouse click. */ @Override public boolean handleMouseClick(Point2D.Double p, MouseEvent evt, DrawingView view) { if (evt.getClickCount() == 2 && view.getHandleDetailLevel() % 2 == 0) { willChange(); final int index = splitSegment(p, 5f / view.getScaleFactor()); if (index != -1) { final BezierPath.Node newNode = getNode(index); fireUndoableEditHappened(new AbstractUndoableEdit() { private static final long serialVersionUID = 1L; @Override public String getPresentationName() { ResourceBundleUtil labels = ResourceBundleUtil.getBundle("org.jhotdraw.draw.Labels"); return labels.getString("edit.bezierPath.splitSegment.text"); } @Override public void redo() throws CannotRedoException { super.redo(); willChange(); addNode(index, newNode); changed(); } @Override public void undo() throws CannotUndoException { super.undo(); willChange(); removeNode(index); changed(); } }); changed(); evt.consume(); return true; } } return false; } @Override public void write(DOMOutput out) throws IOException { writePoints(out); writeAttributes(out); } protected void writePoints(DOMOutput out) throws IOException { out.openElement("points"); if (isClosed()) { out.addAttribute("closed", true); } for (int i = 0, n = getNodeCount(); i < n; i++) { BezierPath.Node node = getNode(i); out.openElement("p"); out.addAttribute("mask", node.mask, 0); out.addAttribute("colinear", true); out.addAttribute("x", node.x[0]); out.addAttribute("y", node.y[0]); out.addAttribute("c1x", node.x[1], node.x[0]); out.addAttribute("c1y", node.y[1], node.y[0]); out.addAttribute("c2x", node.x[2], node.x[0]); out.addAttribute("c2y", node.y[2], node.y[0]); out.closeElement(); } out.closeElement(); } @Override public void read(DOMInput in) throws IOException { readPoints(in); readAttributes(in); } protected void readPoints(DOMInput in) throws IOException { path.clear(); in.openElement("points"); setClosed(in.getAttribute("closed", false)); for (int i = 0, n = in.getElementCount("p"); i < n; i++) { in.openElement("p", i); BezierPath.Node node = new BezierPath.Node( in.getAttribute("mask", 0), in.getAttribute("x", 0d), in.getAttribute("y", 0d), in.getAttribute("c1x", in.getAttribute("x", 0d)), in.getAttribute("c1y", in.getAttribute("y", 0d)), in.getAttribute("c2x", in.getAttribute("x", 0d)), in.getAttribute("c2y", in.getAttribute("y", 0d))); node.keepColinear = in.getAttribute("colinear", true); path.add(node); path.invalidatePath(); in.closeElement(); } in.closeElement(); } }