package org.simantics.diagram.elements; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import org.simantics.db.ReadGraph; import org.simantics.db.Resource; import org.simantics.db.UndoContext; import org.simantics.db.WriteGraph; import org.simantics.db.common.CommentMetadata; import org.simantics.db.common.request.IndexRoot; import org.simantics.db.common.request.WriteRequest; import org.simantics.db.exception.DatabaseException; import org.simantics.db.exception.ManyObjectsForFunctionalRelationException; import org.simantics.db.exception.NoSingleResultException; import org.simantics.db.exception.ServiceException; import org.simantics.db.request.Write; import org.simantics.diagram.query.DiagramRequests; import org.simantics.diagram.stubs.DiagramResource; import org.simantics.diagram.synchronization.graph.DiagramGraphUtil; import org.simantics.g2d.diagram.DiagramClass; import org.simantics.g2d.diagram.IDiagram; import org.simantics.g2d.diagram.impl.Diagram; import org.simantics.g2d.element.ElementClass; import org.simantics.g2d.element.ElementUtils; import org.simantics.g2d.element.IElement; import org.simantics.g2d.element.impl.Element; import org.simantics.g2d.utils.GeometryUtils; import org.simantics.scl.commands.Command; import org.simantics.scl.commands.Commands; import org.simantics.ui.SimanticsUI; /** * Tools to align, rotate, and flip diagram elements. * * TODO : We need to add capability hints to elements to prevent rotating and mirroring elements that do not support that. * Example: mirrored text does not make any sense. * * * @author Marko Luukkainen (implementation) * @author Tuukka Lehtonen (documentation) */ public final class ElementTransforms { public static enum SIDE { LEFT, RIGHT, TOP, BOTTOM, VERT, HORIZ, VERT_BTW, HORIZ_BTW }; /** * Align the specified set of diagram element resources in line with each * other calculated by the specified side. * *

* Alignment requires at least two elements to do anything. * * @param resources diagram element resources to rotate * @param side the side of each element to use for distancing. Does not support between aligments. */ public static void align(final Resource resources[], final SIDE side) { if (resources.length < 2) return; if (side == SIDE.HORIZ_BTW || side == SIDE.VERT_BTW ) return; SimanticsUI.getSession().asyncRequest(new WriteRequest() { @Override public void perform(WriteGraph graph) throws DatabaseException { graph.markUndoPoint(); IDiagram hints = Diagram.spawnNew(DiagramClass.DEFAULT); List elements = new ArrayList(); for (Resource r : resources) { AlignElement e = create(graph, hints, r); if (e != null) elements.add(e); } if (elements.size() < 2) return; double mx = 0; double my = 0; for (AlignElement e : elements) { mx += e.transform[4]; my += e.transform[5]; } mx /= elements.size(); my /= elements.size(); // prevent moving symbols into the same position int count = 0; for (AlignElement e : elements) { if (side == SIDE.VERT || side == SIDE.LEFT || side == SIDE.RIGHT) { if (Math.abs(e.transform[5] - my) < 0.1) { count++; } } else { if (Math.abs(e.transform[4] - mx) < 0.1) { count++; } } } if (count > 1) return; if (side == SIDE.HORIZ || side == SIDE.VERT) { for (AlignElement e : elements) { if (side == SIDE.VERT) DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,e.transform[5]}); else if (side == SIDE.HORIZ) { DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],e.transform[4],my}); } } } else { double lx, rx; double ty, by; lx = elements.get(0).transform[4] + elements.get(0).rotatedBounds.getMinX(); rx = elements.get(0).transform[4] + elements.get(0).rotatedBounds.getMaxX(); ty = elements.get(0).transform[5] + elements.get(0).rotatedBounds.getMinY(); by = elements.get(0).transform[5] + elements.get(0).rotatedBounds.getMaxY(); for (int i = 1; i < elements.size(); i++) { double tlx, trx; double tty, tby; tlx = elements.get(i).transform[4] + elements.get(i).rotatedBounds.getMinX(); trx = elements.get(i).transform[4] + elements.get(i).rotatedBounds.getMaxX(); tty = elements.get(i).transform[5] + elements.get(i).rotatedBounds.getMinY(); tby = elements.get(i).transform[5] + elements.get(i).rotatedBounds.getMaxY(); if (tlx < lx) lx = tlx; if (trx > rx) rx = trx; if (tty < ty) ty = tty; if (tby > by) by = tby; } for (AlignElement e : elements) { mx = e.transform[4]; my = e.transform[5]; if (side == SIDE.LEFT) { mx = lx - e.rotatedBounds.getMinX() ; } else if (side == SIDE.RIGHT) { mx = rx - e.rotatedBounds.getMaxX(); } else if (side == SIDE.TOP) { my = ty - e.rotatedBounds.getMinY(); } else { my = by - e.rotatedBounds.getMaxY(); } DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,my}); } } } }); } /** * Distance specified set of diagram element resources equally. Distancing * is performed on the specified side of the each element. * *

* Distancing requires at least three elements to work. * * @param resources diagram element resources to rotate * @param side the side of each element to use for distancing */ public static void dist(final Resource resources[], final SIDE side) { if (resources.length < 3) return; SimanticsUI.getSession().asyncRequest(new WriteRequest() { @Override public void perform(WriteGraph graph) throws DatabaseException { graph.markUndoPoint(); IDiagram hints = Diagram.spawnNew(DiagramClass.DEFAULT); List elements = new ArrayList(); for (Resource r : resources) { //System.out.println(r + " " + GraphUtils.getReadableName(graph, r)); AlignElement e = create(graph, hints, r); if (e != null) elements.add(e); } if (elements.size() < 3) return; switch (side) { case LEFT: { Collections.sort(elements, new XComparator()); AlignElement left = elements.get(0); AlignElement right = elements.get(elements.size() - 1); double leftEdge = left.transform[4] + left.rotatedBounds.getMinX(); double rightEdge = right.transform[4] + right.rotatedBounds.getMinX(); double totalDist = rightEdge - leftEdge; double dist = totalDist / (elements.size() - 1); double d = leftEdge; for (int i = 1; i < elements.size() -1; i++) { d += dist; AlignElement e = elements.get(i); double mx = d - e.rotatedBounds.getMinX(); DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,e.transform[5]}); } break; } case VERT: { Collections.sort(elements, new XComparator()); AlignElement left = elements.get(0); AlignElement right = elements.get(elements.size() - 1); double leftEdge = left.transform[4]; double rightEdge = right.transform[4]; double totalDist = rightEdge - leftEdge; double dist = totalDist / (elements.size() - 1); double d = leftEdge; for (int i = 1; i < elements.size() -1; i++) { d += dist; AlignElement e = elements.get(i); double mx = d; DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,e.transform[5]}); } break; } case RIGHT:{ Collections.sort(elements, new XComparator()); AlignElement left = elements.get(0); AlignElement right = elements.get(elements.size() - 1); double leftEdge = left.transform[4] + left.rotatedBounds.getMaxX(); double rightEdge = right.transform[4] + right.rotatedBounds.getMaxX(); double totalDist = rightEdge - leftEdge; double dist = totalDist / (elements.size() - 1); double d = leftEdge; for (int i = 1; i < elements.size() -1; i++) { d += dist; AlignElement e = elements.get(i); double mx = d - e.rotatedBounds.getMaxX(); DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,e.transform[5]}); } break; } case VERT_BTW:{ Collections.sort(elements, new XComparator()); AlignElement left = elements.get(0); AlignElement right = elements.get(elements.size() - 1); double leftEdge = left.transform[4] + left.rotatedBounds.getMaxX(); double rightEdge = right.transform[4] + right.rotatedBounds.getMinX(); double totalDist = rightEdge - leftEdge; double totalElementSize = 0; for (int i = 1; i < elements.size() -1; i++) { totalElementSize += elements.get(i).rotatedBounds.getWidth(); } double totalAvail = totalDist - totalElementSize; double dist = totalAvail / (elements.size() - 1); double d = leftEdge; for (int i = 1; i < elements.size() -1; i++) { d += dist; AlignElement e = elements.get(i); double mx = d - e.rotatedBounds.getMinX(); d += e.bounds.getWidth(); DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],mx,e.transform[5]}); } break; } case BOTTOM: { Collections.sort(elements, new YComparator()); AlignElement top = elements.get(0); AlignElement bottom = elements.get(elements.size() - 1); double topEdge = top.transform[5] + top.rotatedBounds.getMaxY(); double bottomEdge = bottom.transform[5] + bottom.rotatedBounds.getMaxY(); double totalDist = bottomEdge - topEdge; double dist = totalDist / (elements.size() - 1); double d = topEdge; for (int i = 1; i < elements.size() -1; i++) { d += dist; AlignElement e = elements.get(i); double my = d - e.rotatedBounds.getMaxY(); DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],e.transform[4],my}); } break; } case TOP: { Collections.sort(elements, new YComparator()); AlignElement top = elements.get(0); AlignElement bottom = elements.get(elements.size() - 1); double topEdge = top.transform[5] + top.rotatedBounds.getMinY(); double bottomEdge = bottom.transform[5] + bottom.rotatedBounds.getMinY(); double totalDist = bottomEdge - topEdge; double dist = totalDist / (elements.size() - 1); double d = topEdge; for (int i = 1; i < elements.size() -1; i++) { d += dist; AlignElement e = elements.get(i); double my = d - e.rotatedBounds.getMinY(); DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],e.transform[4],my}); } break; } case HORIZ: { Collections.sort(elements, new YComparator()); AlignElement top = elements.get(0); AlignElement bottom = elements.get(elements.size() - 1); double topEdge = top.transform[5]; double bottomEdge = bottom.transform[5]; double totalDist = bottomEdge - topEdge; double dist = totalDist / (elements.size() - 1); double d = topEdge; for (int i = 1; i < elements.size() -1; i++) { d += dist; AlignElement e = elements.get(i); double my = d; DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],e.transform[4],my}); } break; } case HORIZ_BTW: { Collections.sort(elements, new YComparator()); AlignElement top = elements.get(0); AlignElement bottom = elements.get(elements.size() - 1); double topEdge = top.transform[5] + top.rotatedBounds.getMaxY(); double bottomEdge = bottom.transform[5] + bottom.rotatedBounds.getMinY(); double totalDist = bottomEdge - topEdge; double totalElementSize = 0; for (int i = 1; i < elements.size() -1; i++) { totalElementSize += elements.get(i).rotatedBounds.getHeight(); } double totalAvail = totalDist - totalElementSize; double dist = totalAvail / (elements.size() - 1); double d = topEdge; for (int i = 1; i < elements.size() -1; i++) { d += dist; AlignElement e = elements.get(i); double my = d - e.rotatedBounds.getMinY(); d += e.rotatedBounds.getHeight(); DiagramGraphUtil.changeTransform(graph, e.element, new double[]{e.transform[0],e.transform[1],e.transform[2],e.transform[3],e.transform[4],my}); } break; } } } }); } /** * Rotate specified set of diagram element resources around the center of * mass of the specified element selection. * * @param resources diagram element resources to rotate * @param clockwise true to rotate 90 degrees clockwise, * false to rotate 90 degrees counter-clockwise */ public static void rotate(final Resource resources[], final boolean clockwise) { SimanticsUI.getSession().asyncRequest(new WriteRequest() { @Override public void perform(WriteGraph graph) throws DatabaseException { graph.markUndoPoint(); IDiagram hints = Diagram.spawnNew(DiagramClass.DEFAULT); DiagramResource DIA = DiagramResource.getInstance(graph); List elements = new ArrayList(); List connections = new ArrayList(); for (Resource r : resources) { AlignElement e = create(graph, hints, r); if (e != null) elements.add(e); else if(graph.isInstanceOf(r, DIA.RouteGraphConnection)) connections.add(r); } if (elements.size() < 1) return; // Add comment to change set. CommentMetadata cm = graph.getMetadata(CommentMetadata.class); graph.addMetadata( cm.add("Rotate " + elements.size() + " elements " + (clockwise ? "clockwise" : "counter-clockwise")) ); AffineTransform at = clockwise ? AffineTransform.getQuadrantRotateInstance(1) : AffineTransform.getQuadrantRotateInstance(3); if (elements.size() == 1 && connections.isEmpty()) { for (AlignElement e : elements) { AffineTransform eat = new AffineTransform(e.transform[0], e.transform[1], e.transform[2], e.transform[3], 0, 0); eat.preConcatenate(at); DiagramGraphUtil.changeTransform(graph, e.element, new double[]{eat.getScaleX(),eat.getShearY(),eat.getShearX(),eat.getScaleY(),e.transform[4],e.transform[5]}); } } else { Rectangle2D selectionBounds = null; for (AlignElement e : elements) { if (selectionBounds != null) { selectionBounds.add(e.transform[4], e.transform[5]); } else { selectionBounds = new Rectangle2D.Double(e.transform[4], e.transform[5], 0, 0); } } double cx = selectionBounds.getCenterX(); double cy = selectionBounds.getCenterY(); for (AlignElement e : elements) { double x = e.transform[4]; double y = e.transform[5]; double dx = x - cx; double dy = y - cy; Point2D r = at.transform(new Point2D.Double(dx, dy), null); double mx = r.getX() + cx; double my = r.getY() + cy; AffineTransform eat = new AffineTransform(e.transform[0], e.transform[1], e.transform[2], e.transform[3], 0, 0); eat.preConcatenate(at); DiagramGraphUtil.changeTransform(graph, e.element, new double[]{eat.getScaleX(),eat.getShearY(),eat.getShearX(),eat.getScaleY(),mx,my}); } if(!connections.isEmpty()) { Command rotateConnection = Commands.get(graph, "Simantics/Diagram/rotateConnection"); Resource model = graph.syncRequest(new IndexRoot(connections.get(0))); for(Resource r : connections) rotateConnection.execute(graph, model, r, cx, cy, clockwise); } } } }); } /** * Flip specified set of diagram element resources around either the x or * y-axis specified by the mass center of the selection bounding box. * Each element is considered to weigh an equal amount. * * @param resources diagram element resources to flip * @param xAxis true to flip around x-axis, false * for y-axis */ public static void flip(final Resource resources[], final boolean xAxis) { SimanticsUI.getSession().asyncRequest(new WriteRequest() { @Override public void perform(WriteGraph graph) throws DatabaseException { graph.markUndoPoint(); IDiagram hints = Diagram.spawnNew(DiagramClass.DEFAULT); DiagramResource DIA = DiagramResource.getInstance(graph); List elements = new ArrayList(); List connections = new ArrayList(); for (Resource r : resources) { AlignElement e = create(graph, hints, r); if (e != null) elements.add(e); else if(graph.isInstanceOf(r, DIA.RouteGraphConnection)) connections.add(r); } if (elements.size() < 1) return; // Add comment to change set. CommentMetadata cm = graph.getMetadata(CommentMetadata.class); graph.addMetadata( cm.add("Flip " + elements.size() + " elements " + (xAxis ? "vertically" : "horizontally")) ); if (elements.size() == 1 && connections.isEmpty()) { for (AlignElement e : elements) { if (xAxis) { AffineTransform at = new AffineTransform(e.transform); AffineTransform at2 = AffineTransform.getScaleInstance(1, -1); at.preConcatenate(at2); DiagramGraphUtil.changeTransform(graph, e.element, new double[]{at.getScaleX(),at.getShearY(),at.getShearX(),at.getScaleY(),e.transform[4],e.transform[5]}); } else { AffineTransform at = new AffineTransform(e.transform); AffineTransform at2 = AffineTransform.getScaleInstance(-1, 1); at.preConcatenate(at2); DiagramGraphUtil.changeTransform(graph, e.element, new double[]{at.getScaleX(),at.getShearY(),at.getShearX(),at.getScaleY(),e.transform[4],e.transform[5]}); } } } else { Rectangle2D selectionBounds = null; for (AlignElement e : elements) { if (selectionBounds != null) { selectionBounds.add(e.transform[4], e.transform[5]); } else { selectionBounds = new Rectangle2D.Double(e.transform[4], e.transform[5], 0, 0); } } for (AlignElement e : elements) { if (xAxis) { double y = e.transform[5]; double cy = selectionBounds.getCenterY(); double my = cy + cy - y; AffineTransform at = new AffineTransform(e.transform); AffineTransform at2 = AffineTransform.getScaleInstance(1, -1); at.preConcatenate(at2); DiagramGraphUtil.changeTransform(graph, e.element, new double[]{at.getScaleX(),at.getShearY(),at.getShearX(),at.getScaleY(),e.transform[4],my}); } else { double x = e.transform[4]; double cx = selectionBounds.getCenterX(); double mx = cx + cx - x; AffineTransform at = new AffineTransform(e.transform); AffineTransform at2 = AffineTransform.getScaleInstance(-1, 1); at.preConcatenate(at2); DiagramGraphUtil.changeTransform(graph, e.element, new double[]{at.getScaleX(),at.getShearY(),at.getShearX(),at.getScaleY(),mx,e.transform[5]}); } } if(!connections.isEmpty()) { Command flipConnection = Commands.get(graph, "Simantics/Diagram/flipConnection"); Resource model = graph.syncRequest(new IndexRoot(connections.get(0))); for(Resource r : connections) flipConnection.execute(graph, model, r, xAxis, xAxis ? selectionBounds.getCenterY() : selectionBounds.getCenterX()); } } } }); } private static AlignElement create(ReadGraph graph, IDiagram hints, Resource r) throws ManyObjectsForFunctionalRelationException, NoSingleResultException, ServiceException, DatabaseException { DiagramResource dr = DiagramResource.getInstance(graph); if (graph.isInstanceOf(r, dr.Element) && !graph.isInstanceOf(r, dr.Connection) /*&& !graph.isInstanceOf(r, dr.Monitor)*/) { double transform[] = graph.getPossibleRelatedValue(r, dr.HasTransform); ElementClass ec = graph.syncRequest(DiagramRequests.getElementClass(graph.getSingleType(r, dr.Element), hints)); IElement e = Element.spawnNew(ec); Rectangle2D bounds = ElementUtils.getElementBounds(e); if (transform != null && bounds != null) { return new AlignElement(r, transform, bounds); } } return null; } private static class AlignElement { public Resource element; public double[] transform; public Rectangle2D bounds; public Rectangle2D rotatedBounds; // public Rectangle2D transformedBounds; public AlignElement(Resource element, double[] transform, Rectangle2D bounds) { this.element = element; this.transform = transform; this.bounds = bounds; // this.transformedBounds = getBounds(); this.rotatedBounds = getRotatedBounds(); } // public Rectangle2D getBounds() { // AffineTransform at = new AffineTransform(transform[0], transform[1], transform[2], transform[3], transform[4], transform[5]); // return GeometryUtils.transformShape(bounds, at).getBounds2D(); // } public Rectangle2D getRotatedBounds() { AffineTransform at = new AffineTransform(transform[0], transform[1], transform[2], transform[3], 0.0, 0.0); return GeometryUtils.transformShape(bounds, at).getBounds2D(); } public double getDeterminant() { return transform[0] * transform[3] - transform[1] * transform[2]; } } private static class XComparator implements Comparator { @Override public int compare(AlignElement o1, AlignElement o2) { if (o1.transform[4] < o2.transform[4]) return -1; if (o1.transform[4] > o2.transform[4]) return 1; return 0; } } private static class YComparator implements Comparator { @Override public int compare(AlignElement o1, AlignElement o2) { if (o1.transform[5] < o2.transform[5]) return -1; if (o1.transform[5] > o2.transform[5]) return 1; return 0; } } /** * Set 2D affine transforms for the listed diagram element resources. * * @param elements diagram element resources to set transforms for * @param transforms transforms for each element */ public static Write setTransformRequest(final Collection elements) { return new WriteRequest() { @Override public void perform(WriteGraph graph) throws DatabaseException { for (TransformedObject element : elements) DiagramGraphUtil.changeTransform(graph, element.element, element.transform); } }; } /** * Set 2D affine transforms for the listed diagram element resources. * * @param undoContext the database undo context to use for the returned * request * @param elements diagram element resources to set transforms for * @param transforms transforms for each element * @param preConcatenate true to pre-concatenate the * transforms, false to concatenate */ public static Write concatenateTransformRequest( UndoContext undoContext, final Collection elements, final boolean preConcatenate) { return new WriteRequest() { @Override public void perform(WriteGraph graph) throws DatabaseException { for (TransformedObject element : elements) { AffineTransform at = DiagramGraphUtil.getTransform(graph, element.element); if (preConcatenate) at.preConcatenate(element.transform); else at.concatenate(element.transform); DiagramGraphUtil.setTransform(graph, element.element, at); } } }; } public static class TransformedObject { public final Resource element; public final AffineTransform transform; public TransformedObject(Resource element) { this.element = element; this.transform = new AffineTransform(); } public TransformedObject(Resource element, AffineTransform transform) { this.element = element; this.transform = transform; } } }