/******************************************************************************* * Copyright (c) 2014, 2016 itemis AG and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Alexander Nyßen (itemis AG) - initial API and implementation * Matthias Wienand (itemis AG) - initial API and implementation * *******************************************************************************/ package org.eclipse.gef.fx.utils; import java.awt.geom.NoninvertibleTransformException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.eclipse.gef.fx.nodes.Connection; import org.eclipse.gef.fx.nodes.GeometryNode; import org.eclipse.gef.geometry.convert.fx.FX2Geometry; import org.eclipse.gef.geometry.planar.AffineTransform; import org.eclipse.gef.geometry.planar.ICurve; import org.eclipse.gef.geometry.planar.IGeometry; import org.eclipse.gef.geometry.planar.ITranslatable; import org.eclipse.gef.geometry.planar.Point; import org.eclipse.gef.geometry.planar.Rectangle; import javafx.geometry.Bounds; import javafx.geometry.Point2D; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.shape.Arc; import javafx.scene.shape.Circle; import javafx.scene.shape.CubicCurve; import javafx.scene.shape.Ellipse; import javafx.scene.shape.Line; import javafx.scene.shape.Path; import javafx.scene.shape.Polygon; import javafx.scene.shape.Polyline; import javafx.scene.shape.QuadCurve; import javafx.scene.shape.SVGPath; import javafx.scene.shape.Shape; import javafx.scene.shape.StrokeType; import javafx.scene.text.Text; import javafx.scene.transform.Affine; /** * The {@link NodeUtils} class contains utility methods for working with JavaFX: * <ul> * <li>transforming {@link IGeometry}s from/to different JavaFX coordinate * systems ({@link #localToParent(Node, IGeometry)}, * {@link #localToScene(Node, IGeometry)}, {@link #localToScene(Node, Point)}, * {@link #parentToLocal(Node, IGeometry)}, * {@link #sceneToLocal(Node, IGeometry)})</li> * <li>determining the actual local-to-scene or scene-to-local transform for a * JavaFX {@link Node} ({@link #getLocalToSceneTx(Node)}, * {@link #getSceneToLocalTx(Node)})</li> * <li>perform picking of {@link Node}s at a specific position within the JavaFX * scene graph ({@link #getNodesAt(Node, double, double)})</li> * </ul> * * @author anyssen * @author mwienand * */ public class NodeUtils { /** * Returns <code>true</code> if the given {@link Affine}s are equal. * Otherwise returns <code>false</code>. * * @param a1 * The first operand. * @param a2 * The second operand. * @return <code>true</code> if the given {@link Affine}s are equal, * otherwise <code>false</code>. */ public static boolean equals(Affine a1, Affine a2) { // TODO: verify if Affine#equals() works with Java 8 // Affine does not properly implement equals, so we have to implement // that here return a1.getMxx() == a2.getMxx() && a1.getMxy() == a2.getMxy() && a1.getMxz() == a2.getMxz() && a1.getMyx() == a2.getMyx() && a1.getMyy() == a2.getMyy() && a1.getMyz() == a2.getMyz() && a1.getMzx() == a2.getMzx() && a1.getMzy() == a2.getMzy() && a1.getMzz() == a2.getMzz() && a1.getTx() == a2.getTx() && a1.getTy() == a2.getTy() && a1.getTz() == a2.getTz(); } /** * Returns an {@link IGeometry} that corresponds whose outline represents * the geometric outline of the given {@link Node}, excluding its stroke. * <p> * The {@link IGeometry} is specified within the local coordinate system of * the given {@link Node}. * <p> * The following {@link Node}s are supported: * <ul> * <li>{@link Connection} * <li>{@link GeometryNode} * <li>{@link Arc} * <li>{@link Circle} * <li>{@link CubicCurve} * <li>{@link Ellipse} * <li>{@link Line} * <li>{@link Path} * <li>{@link Polygon} * <li>{@link Polyline} * <li>{@link QuadCurve} * <li>{@link Rectangle} * </ul> * * @param visual * The {@link Node} of which the geometric outline is returned. * @return An {@link IGeometry} that corresponds to the geometric outline of * the given {@link Node}. * @throws IllegalArgumentException * if the given {@link Node} is not supported. */ public static IGeometry getGeometricOutline(Node visual) { if (visual instanceof Connection) { Node curveNode = ((Connection) visual).getCurve(); return localToParent(curveNode, getGeometricOutline(curveNode)); } else if (visual instanceof GeometryNode) { // XXX: The geometry's position is specified relative to the // GeometryNode's layout bounds (which are fixed as (0, 0, width, // height) and includes the layoutX, layoutY (which we have to // compensate here) GeometryNode<?> geometryNode = (GeometryNode<?>) visual; IGeometry geometry = geometryNode.getGeometry(); if (geometry != null) { if (geometry instanceof ITranslatable) { return ((ITranslatable<?>) geometry).getTranslated( -geometryNode.getLayoutX(), -geometryNode.getLayoutY()); } else { return geometry.getTransformed(new AffineTransform() .translate(-geometryNode.getLayoutX(), -geometryNode.getLayoutY())); } } else { // if the geometry node has no geometry (yet), return an empty // geometry return new Rectangle(); } } else if (visual instanceof Shape && !(visual instanceof Text) && !(visual instanceof SVGPath)) { return Shape2Geometry.toGeometry((Shape) visual); } else { throw new IllegalArgumentException( "Cannot determine geometric outline for the given visual <" + visual + ">."); } } /** * Returns an {@link AffineTransform} which represents the transformation * matrix to transform geometries from the local coordinate system of the * given {@link Node} into the coordinate system of the {@link Scene}. * <p> * JavaFX {@link Node} provides a (lazily computed) local-to-scene-transform * property which we could access to get that transform. Unfortunately, this * property is not updated correctly, i.e. its value can differ from the * actual local-to-scene-transform. Therefore, we compute the * local-to-scene-transform for the given node here by concatenating the * local-to-parent-transforms along the hierarchy. * <p> * Note that in situations where you do not need the actual transform, but * instead perform a transformation, you can use the * {@link Node#localToScene(Point2D) Node#localToScene(...)} methods on the * <i>node</i> directly, because it does not make use of the * local-to-scene-transform property, but uses localToParent() internally. * * @param node * The JavaFX {@link Node} for which the local-to-scene * transformation matrix is to be computed. * @return An {@link AffineTransform} representing the local-to-scene * transformation matrix for the given {@link Node}. */ public static AffineTransform getLocalToSceneTx(Node node) { AffineTransform tx = FX2Geometry .toAffineTransform(node.getLocalToParentTransform()); Node tmp = node; while (tmp.getParent() != null) { tmp = tmp.getParent(); tx = FX2Geometry.toAffineTransform(tmp.getLocalToParentTransform()) .concatenate(tx); } return tx; } /** * Computes the nearest common ancestor for two given nodes. * * @param source * The first node. * @param target * The second node. * @return The nearest common ancestor in the scene graph. */ public static Node getNearestCommonAncestor(Node source, Node target) { if (source == target) { return source; } Set<Node> parents = new HashSet<>(); Node m = source; Node n = target; while (m != null || n != null) { if (m != null) { if (parents.contains(m)) { return m; } parents.add(m); if (n != null && parents.contains(n)) { return n; } m = m.getParent(); } if (n != null) { if (parents.contains(n)) { return n; } parents.add(n); if (m != null && parents.contains(m)) { return m; } n = n.getParent(); } } // could not find a common parent return null; } /** * Performs picking on the scene graph beginning at the specified root node * and processing its transitive children. * * @param sceneX * The x-coordinate of the position to pick nodes at, interpreted * in scene coordinate space. * @param sceneY * The y-coordinate of the position to pick nodes at, interpreted * in scene coordinate space. * @param root * The root node at which to start with picking * @return A list of {@link Node}s which contain the the given coordinate. */ public static List<Node> getNodesAt(Node root, double sceneX, double sceneY) { List<Node> picked = new ArrayList<>(); // start with given root node List<Node> nodes = new ArrayList<>(); nodes.add(root); while (!nodes.isEmpty()) { Node current = nodes.remove(0); // transform to local coordinates Point2D pLocal = current.sceneToLocal(sceneX, sceneY); // check if bounds contains (necessary to find children in mouse // transparent regions) if (!current.isMouseTransparent() && current.getBoundsInLocal().contains(pLocal)) { // check precisely if (current.contains(pLocal)) { picked.add(0, current); } // test all children, too if (current instanceof Parent) { nodes.addAll(0, ((Parent) current).getChildrenUnmodifiable()); } } } return picked; } /** * Creates a copy of the given {@link IGeometry} and resizes it to fit the * (corrected) layout-bounds (see {@link #getShapeBounds(Node)}) of the * given {@link Node}. The new, resized {@link IGeometry} is returned. * * @param visual * The visual of which the layout-bounds are used as the basis * for resizing the given {@link IGeometry}. * @param geometry * The {@link IGeometry} that is resized to fit the layout-bounds * of the given {@link Node}. * @return The new, resized {@link IGeometry}. */ public static IGeometry getResizedToShapeBounds(Node visual, IGeometry geometry) { Rectangle geometricBounds = geometry.getBounds(); Rectangle shapeBounds = NodeUtils.getShapeBounds(visual); double dw = shapeBounds.getWidth() - geometricBounds.getWidth(); double dh = shapeBounds.getHeight() - geometricBounds.getHeight(); // geometric bounds match shape bounds, so nothing to do if (dw == 0 && dh == 0) { return geometry; } GeometryNode<IGeometry> geometryNode = new GeometryNode<>(geometry); geometryNode.relocateGeometry(shapeBounds.getX(), shapeBounds.getY()); geometryNode.resizeGeometry(shapeBounds.getWidth(), shapeBounds.getHeight()); return geometryNode.getGeometry(); } /** * Returns the scene-to-local transform for the given {@link Node}. * * @param node * The {@link Node} for which the scene-to-local transform is * returned. * @return The scene-to-local transform for the given {@link Node}. */ public static AffineTransform getSceneToLocalTx(Node node) { try { // XXX: We make use of getLocalToSceneTx(Node) here to // compensate that the Transform provided by FX is updated lazily. // See getLocalToSceneTx(Node) for details. return getLocalToSceneTx(node).invert(); } catch (NoninvertibleTransformException e) { throw new IllegalArgumentException(e); } } /** * Returns the layout-bounds of the given {@link Node}, which might be * adjusted to ensure that it exactly fits the visualization. * * @param node * The {@link Node} to retrieve the (corrected) layout-bounds of. * @return A {@link Rectangle} representing the (corrected) layout-bounds. */ public static Rectangle getShapeBounds(Node node) { Bounds layoutBounds = node.getLayoutBounds(); // XXX: Polygons don't paint exactly to their layout bounds but remain // 0.5 pixels short in case they have a stroke and stroke type is // CENTERED or OUTSIDE (see // https://bugs.openjdk.java.net/browse/JDK-8145499). double offset = 0; if (node instanceof Polygon && ((Polygon) node).getStroke() != null && ((Polygon) node).getStrokeType() != StrokeType.INSIDE) { offset = 0.5; } return FX2Geometry.toRectangle(layoutBounds).shrink(offset, offset, offset, offset); } /** * Creates a geometry whose outline represents the outline of the given * {@link Node}, including its stroke. * <p> * The {@link IGeometry} is specified within the local coordinate system of * the given {@link Node}. * * @param node * The node to infer an outline geometry for. * @return An {@link IGeometry} from which the outline may be retrieved. */ public static IGeometry getShapeOutline(Node node) { try { IGeometry geometry = NodeUtils.getGeometricOutline(node); if (geometry instanceof ICurve) { // XXX: Return as is because fat curves cannot be constructed // yet (see bug #495290 for details). return geometry; } if (geometry != null) { // resize to layout-bounds to include stroke return NodeUtils.getResizedToShapeBounds(node, geometry); } // fall back to layout-bounds return FX2Geometry.toRectangle(node.getLayoutBounds()); } catch (IllegalArgumentException e) { // fall back to layout-bounds return FX2Geometry.toRectangle(node.getLayoutBounds()); } } /** * Returns true if the given {@link Node} is contained within the visual * hierarchy of the given {@link Parent}. * * @param parent * The {@link Parent}, whose hierarchy is to be searched. * @param node * The {@link Node} to test. * @return <code>true</code> if the given node is contained in the visual * hierarchy of the {@link Parent}, <code>false</code> otherwise. */ public static boolean isNested(Parent parent, Node node) { while (node != null) { if (node == parent) { return true; } node = node.getParent(); } return false; } /** * Transforms the given {@link IGeometry} from the local coordinate system * of the given {@link Node} into the coordinate system of the {@link Node} * 's parent. * * @param n * The {@link Node} used to determine the transformation matrix. * @param g * The {@link IGeometry} to transform. * @return The new, transformed {@link IGeometry}. */ public static IGeometry localToParent(Node n, IGeometry g) { AffineTransform localToParentTx = FX2Geometry .toAffineTransform(n.getLocalToParentTransform()); return g.getTransformed(localToParentTx); } /** * Transforms the given {@link Point} from the local coordinate system of * the given {@link Node} into the coordinate system of the {@link Node} 's * parent. * * @param n * The {@link Node} used to determine the transformation matrix. * @param p * The {@link Point} to transform. * @return The new, transformed {@link Point}. */ public static Point localToParent(Node n, Point p) { AffineTransform localToParentTx = FX2Geometry .toAffineTransform(n.getLocalToParentTransform()); return localToParentTx.getTransformed(p); } /** * Transforms the given {@link IGeometry} from the local coordinate system * of the given {@link Node} into scene coordinates. * * @param n * The {@link Node} used to determine the transformation matrix. * @param g * The {@link IGeometry} to transform. * @return The new, transformed {@link IGeometry}. */ public static IGeometry localToScene(Node n, IGeometry g) { AffineTransform localToSceneTx = getLocalToSceneTx(n); return g.getTransformed(localToSceneTx); } /** * Transforms the given {@link Point} from the local coordinate system of * the given {@link Node} into scene coordinates. * * @param n * The {@link Node} used to determine the transformation matrix. * @param p * The {@link IGeometry} to transform. * @return The new, transformed {@link Point}. */ public static Point localToScene(Node n, Point p) { AffineTransform localToSceneTx = getLocalToSceneTx(n); return localToSceneTx.getTransformed(p); } /** * Transforms the given {@link IGeometry} from the parent coordinate system * of the given {@link Node} into the local coordinate system of the * {@link Node}. * * @param n * The {@link Node} used to determine the transformation matrix. * @param g * The {@link IGeometry} to transform. * @return The new, transformed {@link IGeometry}. */ public static IGeometry parentToLocal(Node n, IGeometry g) { // retrieve transform from scene to target parent, by inverting target // parent to scene AffineTransform localToParentTx = FX2Geometry .toAffineTransform(n.getLocalToParentTransform()); AffineTransform parentToLocalTx = null; try { parentToLocalTx = localToParentTx.getCopy().invert(); } catch (NoninvertibleTransformException e) { // TODO: How do we recover from this?! throw new IllegalStateException(e); } return g.getTransformed(parentToLocalTx); } /** * Transforms the given {@link Point} from the parent coordinate system of * the given {@link Node} into the local coordinate system of the * {@link Node}. * * @param n * The {@link Node} used to determine the transformation matrix. * @param p * The {@link Point} to transform. * @return The new, transformed {@link Point}. */ public static Point parentToLocal(Node n, Point p) { // retrieve transform from scene to target parent, by inverting target // parent to scene AffineTransform localToParentTx = FX2Geometry .toAffineTransform(n.getLocalToParentTransform()); AffineTransform parentToLocalTx = null; try { parentToLocalTx = localToParentTx.getCopy().invert(); } catch (NoninvertibleTransformException e) { // TODO: How do we recover from this?! throw new IllegalStateException(e); } return parentToLocalTx.getTransformed(p); } /** * Transforms the given {@link IGeometry} from scene coordinates to the * local coordinate system of the given {@link Node}. * * @param n * The {@link Node} used to determine the transformation matrix. * @param g * The {@link IGeometry} to transform. * @return The new, transformed {@link IGeometry}. */ public static IGeometry sceneToLocal(Node n, IGeometry g) { // retrieve transform from scene to target parent, by inverting target // parent to scene AffineTransform sceneToLocalTx = getSceneToLocalTx(n); return g.getTransformed(sceneToLocalTx); } /** * Transforms the given {@link Point} from scene coordinates to the local * coordinate system of the given {@link Node}. * * @param n * The {@link Node} used to determine the transformation matrix. * @param p * The {@link Point} to transform. * @return The new, transformed {@link Point}. */ public static Point sceneToLocal(Node n, Point p) { // retrieve transform from scene to target parent, by inverting target // parent to scene AffineTransform sceneToLocalTx = getSceneToLocalTx(n); return sceneToLocalTx.getTransformed(p); } /** * Assigns the transformation values of the <i>src</i> {@link Affine} to the * <i>dst</i> {@link Affine}. * * @param dst * The destination {@link Affine}. * @param src * The source {@link Affine}. * @return The destination {@link Affine} for convenience. */ public static Affine setAffine(Affine dst, Affine src) { dst.setMxx(src.getMxx()); dst.setMxy(src.getMxy()); dst.setMxz(src.getMxz()); dst.setMyx(src.getMyx()); dst.setMyy(src.getMyy()); dst.setMyz(src.getMyz()); dst.setMzx(src.getMzx()); dst.setMzy(src.getMzy()); dst.setMzz(src.getMzz()); dst.setTx(src.getTx()); dst.setTy(src.getTy()); dst.setTz(src.getTz()); return dst; } }