/* * Copyright 2010-2015 Institut Pasteur. * * This file is part of Icy. * * Icy is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Icy is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Icy. If not, see <http://www.gnu.org/licenses/>. */ package icy.util; import icy.painter.Anchor2D; import icy.painter.PathAnchor2D; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.Area; import java.awt.geom.CubicCurve2D; import java.awt.geom.Line2D; import java.awt.geom.Path2D; import java.awt.geom.PathIterator; import java.awt.geom.QuadCurve2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RectangularShape; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * @author Stephane */ public class ShapeUtil { public static interface PathConsumer { /** * Consume the specified path.<br> * Return false to interrupt consumption. */ public boolean consumePath(Path2D path, boolean closed); } public static interface ShapeConsumer { /** * Consume the specified Shape.<br> * Return false to interrupt consumption. */ public boolean consume(Shape shape); } public static enum BooleanOperator { OR, AND, XOR } /** * @deprecated Use {@link BooleanOperator} instead. */ @Deprecated public static enum ShapeOperation { OR { @Override public BooleanOperator getBooleanOperator() { return BooleanOperator.OR; } }, AND { @Override public BooleanOperator getBooleanOperator() { return BooleanOperator.AND; } }, XOR { @Override public BooleanOperator getBooleanOperator() { return BooleanOperator.XOR; } }; public abstract BooleanOperator getBooleanOperator(); } /** * Use the {@link Graphics} clip area and {@link Shape} bounds informations to determine if * the specified {@link Shape} is visible in the specified Graphics object. */ public static boolean isVisible(Graphics g, Shape shape) { if (shape == null) return false; return GraphicsUtil.isVisible(g, shape.getBounds2D()); } /** * Returns <code>true</code> if the specified Shape define a closed Shape (Area).<br> * Returns <code>false</code> if the specified Shape define a open Shape (Path).<br> */ public static boolean isClosed(Shape shape) { final PathIterator path = shape.getPathIterator(null); final double crd[] = new double[6]; while (!path.isDone()) { if (path.currentSegment(crd) == PathIterator.SEG_CLOSE) return true; path.next(); } return false; } /** * Merge the specified list of {@link Shape} with the given {@link BooleanOperator}.<br> * * @param shapes * Shapes we want to merge. * @param operator * {@link BooleanOperator} to apply. * @return {@link Area} shape representing the result of the merge operation. */ public static Shape merge(List<Shape> shapes, BooleanOperator operator) { Shape result = new Area(); // merge shapes for (Shape shape : shapes) { switch (operator) { case OR: result = union(result, shape); break; case AND: result = intersect(result, shape); break; case XOR: result = exclusiveUnion(result, shape); break; } } return result; } /** * @deprecated Use {@link #merge(List, BooleanOperator)} instead. */ @Deprecated public static Shape merge(Shape[] shapes, ShapeOperation operation) { return merge(Arrays.asList(shapes), operation.getBooleanOperator()); } /** * Process union between the 2 shapes and return result in a new Shape. */ public static Shape union(Shape shape1, Shape shape2) { // first compute closed area union final Area area = new Area(getClosedPath(shape1)); area.add(new Area(getClosedPath(shape2))); // then compute open path (polyline) union final Path2D result = new Path2D.Double(getOpenPath(shape1)); result.append(getOpenPath(shape2), false); // then append result result.append(area, false); return result; } /** * @deprecated Use {@link #union(Shape, Shape)} instead */ @Deprecated public static Shape add(Shape shape1, Shape shape2) { return union(shape1, shape2); } /** * Intersects 2 shapes and return result in an {@link Area} type shape.<br> * If one of the specified Shape is not an Area (do not contains any pixel) then an empty Area is returned. */ public static Area intersect(Shape shape1, Shape shape2) { // trivial optimization if (!isClosed(shape1) || !isClosed(shape2)) return new Area(); final Area result = new Area(getClosedPath(shape1)); result.intersect(new Area(getClosedPath(shape2))); return result; } /** * Do exclusive union between the 2 shapes and return result in an {@link Area} type shape.<br> * If one of the specified Shape is not an Area (do not contains any pixel) then it just return the other Shape in * Area format. If both Shape are not Area then an empty Area is returned. */ public static Area exclusiveUnion(Shape shape1, Shape shape2) { // trivial optimization if (!isClosed(shape1)) { if (!isClosed(shape2)) return new Area(); return new Area(shape2); } // trivial optimization if (!isClosed(shape2)) return new Area(shape1); final Area result = new Area(getClosedPath(shape1)); result.exclusiveOr(new Area(getClosedPath(shape2))); return result; } /** * @deprecated Use {@link #exclusiveUnion(Shape, Shape)} instead. */ @Deprecated public static Area xor(Shape shape1, Shape shape2) { return exclusiveUnion(shape1, shape2); } /** * Subtract shape2 from shape1 return result in an {@link Area} type shape. */ public static Area subtract(Shape shape1, Shape shape2) { // trivial optimization if (!isClosed(shape1)) return new Area(); if (!isClosed(shape2)) return new Area(shape1); final Area result = new Area(getClosedPath(shape1)); result.subtract(new Area(getClosedPath(shape2))); return result; } /** * Scale the specified {@link RectangularShape} by specified factor. * * @param shape * the {@link RectangularShape} to scale * @param factor * the scale factor * @param centered * if true then scaling is centered (shape location is modified) */ public static void scale(RectangularShape shape, double factor, boolean centered) { final double w = shape.getWidth(); final double h = shape.getHeight(); final double newW = w * factor; final double newH = h * factor; if (centered) { final double deltaW = (newW - w) / 2; final double deltaH = (newH - h) / 2; shape.setFrame(shape.getX() - deltaW, shape.getY() - deltaH, newW, newH); } else shape.setFrame(shape.getX(), shape.getY(), newW, newH); } /** * Enlarge the specified {@link RectangularShape} by specified width and height. * * @param shape * the {@link RectangularShape} to scale * @param width * the width to add * @param height * the height to add * @param centered * if true then enlargement is centered (shape location is modified) */ public static void enlarge(RectangularShape shape, double width, double height, boolean centered) { final double w = shape.getWidth(); final double h = shape.getHeight(); final double newW = w + width; final double newH = h + height; if (centered) { final double deltaW = (newW - w) / 2; final double deltaH = (newH - h) / 2; shape.setFrame(shape.getX() - deltaW, shape.getY() - deltaH, newW, newH); } else shape.setFrame(shape.getX(), shape.getY(), newW, newH); } /** * Translate a rectangular shape by the specified dx and dy value */ public static void translate(RectangularShape shape, int dx, int dy) { shape.setFrame(shape.getX() + dx, shape.getY() + dy, shape.getWidth(), shape.getHeight()); } /** * Translate a rectangular shape by the specified dx and dy value */ public static void translate(RectangularShape shape, double dx, double dy) { shape.setFrame(shape.getX() + dx, shape.getY() + dy, shape.getWidth(), shape.getHeight()); } /** * Permit to describe any PathIterator in a list of Shape which are returned * to the specified ShapeConsumer */ public static boolean consumeShapeFromPath(PathIterator path, ShapeConsumer consumer) { final Line2D.Double line = new Line2D.Double(); final QuadCurve2D.Double quadCurve = new QuadCurve2D.Double(); final CubicCurve2D.Double cubicCurve = new CubicCurve2D.Double(); double lastX, lastY, curX, curY, movX, movY; final double crd[] = new double[6]; curX = 0; curY = 0; movX = 0; movY = 0; while (!path.isDone()) { final int segType = path.currentSegment(crd); lastX = curX; lastY = curY; switch (segType) { case PathIterator.SEG_MOVETO: curX = crd[0]; curY = crd[1]; movX = curX; movY = curY; break; case PathIterator.SEG_LINETO: curX = crd[0]; curY = crd[1]; line.setLine(lastX, lastY, curX, curY); if (!consumer.consume(line)) return false; break; case PathIterator.SEG_QUADTO: curX = crd[2]; curY = crd[3]; quadCurve.setCurve(lastX, lastY, crd[0], crd[1], curX, curY); if (!consumer.consume(quadCurve)) return false; break; case PathIterator.SEG_CUBICTO: curX = crd[4]; curY = crd[5]; cubicCurve.setCurve(lastX, lastY, crd[0], crd[1], crd[2], crd[3], curX, curY); if (!consumer.consume(cubicCurve)) return false; break; case PathIterator.SEG_CLOSE: line.setLine(lastX, lastY, movX, movY); if (!consumer.consume(line)) return false; break; } path.next(); } return true; } /** * Consume all sub path of the specified {@link PathIterator}.<br> * We consider a new sub path when we meet both a {@link PathIterator#SEG_MOVETO} segment or after a * {@link PathIterator#SEG_CLOSE} segment (except the ending one). */ public static void consumeSubPath(PathIterator pathIt, PathConsumer consumer) { final double crd[] = new double[6]; Path2D current = null; while (!pathIt.isDone()) { switch (pathIt.currentSegment(crd)) { case PathIterator.SEG_MOVETO: // had a previous not closed path ? --> consume it if (current != null) consumer.consumePath(current, false); // create new path current = new Path2D.Double(pathIt.getWindingRule()); current.moveTo(crd[0], crd[1]); break; case PathIterator.SEG_LINETO: current.lineTo(crd[0], crd[1]); break; case PathIterator.SEG_QUADTO: current.quadTo(crd[0], crd[1], crd[2], crd[3]); break; case PathIterator.SEG_CUBICTO: current.curveTo(crd[0], crd[1], crd[2], crd[3], crd[4], crd[5]); break; case PathIterator.SEG_CLOSE: // close path and consume it current.closePath(); consumer.consumePath(current, true); // clear path current = null; break; } pathIt.next(); } // have a last not closed path ? --> consume it if (current != null) consumer.consumePath(current, false); } /** * Consume all sub path of the specified {@link Shape}.<br> * We consider a new sub path when we meet both a {@link PathIterator#SEG_MOVETO} segment or after a * {@link PathIterator#SEG_CLOSE} segment (except the ending one). */ public static void consumeSubPath(Shape shape, PathConsumer consumer) { consumeSubPath(shape.getPathIterator(null), consumer); } /** * Returns only the open path part of the specified Shape.<br> * By default all sub path inside a Shape are considered closed which can be a problem when drawing or using * {@link Path2D#contains(double, double)} method. */ public static Path2D getOpenPath(Shape shape) { final PathIterator pathIt = shape.getPathIterator(null); final Path2D result = new Path2D.Double(pathIt.getWindingRule()); consumeSubPath(pathIt, new PathConsumer() { @Override public boolean consumePath(Path2D path, boolean closed) { if (!closed) result.append(path, false); return true; } }); return result; } /** * Returns only the closed path part of the specified Shape.<br> * By default all sub path inside a Shape are considered closed which can be a problem when drawing or using * {@link Path2D#contains(double, double)} method. */ public static Path2D getClosedPath(Shape shape) { final PathIterator pathIt = shape.getPathIterator(null); final Path2D result = new Path2D.Double(pathIt.getWindingRule()); consumeSubPath(pathIt, new PathConsumer() { @Override public boolean consumePath(Path2D path, boolean closed) { if (closed) result.append(path, false); return true; } }); return result; } /** * Return all PathAnchor points from the specified shape */ public static ArrayList<PathAnchor2D> getAnchorsFromShape(Shape shape, Color color, Color selectedColor) { final PathIterator pathIt = shape.getPathIterator(null); final ArrayList<PathAnchor2D> result = new ArrayList<PathAnchor2D>(); final double crd[] = new double[6]; final double mov[] = new double[2]; while (!pathIt.isDone()) { final int segType = pathIt.currentSegment(crd); PathAnchor2D pt = null; switch (segType) { case PathIterator.SEG_MOVETO: mov[0] = crd[0]; mov[1] = crd[1]; case PathIterator.SEG_LINETO: pt = new PathAnchor2D(crd[0], crd[1], color, selectedColor, segType); break; case PathIterator.SEG_QUADTO: pt = new PathAnchor2D(crd[0], crd[1], crd[2], crd[3], color, selectedColor); break; case PathIterator.SEG_CUBICTO: pt = new PathAnchor2D(crd[0], crd[1], crd[2], crd[3], crd[4], crd[5], color, selectedColor); break; case PathIterator.SEG_CLOSE: pt = new PathAnchor2D(mov[0], mov[1], color, selectedColor, segType); // CLOSE points aren't visible pt.setVisible(false); break; } if (pt != null) result.add(pt); pathIt.next(); } return result; } /** * Return all PathAnchor points from the specified shape */ public static ArrayList<PathAnchor2D> getAnchorsFromShape(Shape shape) { return getAnchorsFromShape(shape, Anchor2D.DEFAULT_NORMAL_COLOR, Anchor2D.DEFAULT_SELECTED_COLOR); } /** * Update specified path from the specified list of PathAnchor2D */ public static Path2D buildPathFromAnchors(Path2D path, List<PathAnchor2D> points, boolean closePath) { path.reset(); for (PathAnchor2D pt : points) { switch (pt.getType()) { case PathIterator.SEG_MOVETO: path.moveTo(pt.getX(), pt.getY()); break; case PathIterator.SEG_LINETO: path.lineTo(pt.getX(), pt.getY()); break; case PathIterator.SEG_QUADTO: path.quadTo(pt.getPosQExtX(), pt.getPosQExtY(), pt.getX(), pt.getY()); break; case PathIterator.SEG_CUBICTO: path.curveTo(pt.getPosCExtX(), pt.getPosCExtY(), pt.getPosQExtX(), pt.getPosQExtY(), pt.getX(), pt.getY()); break; case PathIterator.SEG_CLOSE: path.closePath(); break; } } if ((points.size() > 1) && closePath) path.closePath(); return path; } /** * Update specified path from the specified list of PathAnchor2D */ public static Path2D buildPathFromAnchors(Path2D path, List<PathAnchor2D> points) { return buildPathFromAnchors(path, points, true); } /** * Create and return a path from the specified list of PathAnchor2D */ public static Path2D getPathFromAnchors(List<PathAnchor2D> points, boolean closePath) { return buildPathFromAnchors(new Path2D.Double(), points, closePath); } /** * Create and return a path from the specified list of PathAnchor2D */ public static Path2D getPathFromAnchors(List<PathAnchor2D> points) { return buildPathFromAnchors(new Path2D.Double(), points, true); } /** * @deprecated Use {@link GraphicsUtil#drawPathIterator(PathIterator, Graphics2D)} instead */ @Deprecated public static void drawFromPath(PathIterator path, final Graphics2D g) { GraphicsUtil.drawPathIterator(path, g); } /** * Return true if the specified PathIterator intersects with the specified Rectangle */ public static boolean pathIntersects(PathIterator path, final Rectangle2D rect) { return !consumeShapeFromPath(path, new ShapeConsumer() { @Override public boolean consume(Shape shape) { if (shape.intersects(rect)) return false; return true; } }); } }