/******************************************************************************* * Copyright (c) 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.nodes; import org.eclipse.gef.fx.utils.Geometry2Shape; import org.eclipse.gef.fx.utils.NodeUtils; import org.eclipse.gef.geometry.convert.fx.FX2Geometry; import org.eclipse.gef.geometry.convert.fx.Geometry2FX; import org.eclipse.gef.geometry.euclidean.Angle; import org.eclipse.gef.geometry.euclidean.Vector; import org.eclipse.gef.geometry.planar.AffineTransform; import org.eclipse.gef.geometry.planar.BezierCurve; import org.eclipse.gef.geometry.planar.ICurve; import org.eclipse.gef.geometry.planar.IGeometry; import org.eclipse.gef.geometry.planar.Point; import javafx.collections.ObservableList; import javafx.geometry.Bounds; import javafx.scene.Node; import javafx.scene.paint.Color; import javafx.scene.shape.Path; import javafx.scene.shape.Rectangle; import javafx.scene.shape.Shape; /** * Abstract base class for {@link IConnectionInterpolator} implementations, * which supports updating the geometry for an {@link IGeometry} curve node, as * well as arranging and clipping the decorations. * * @author anyssen * @author mwienand * */ public abstract class AbstractInterpolator implements IConnectionInterpolator { /** * Arranges the given decoration according to the passed-in values. * * @param decoration * The decoration {@link Node} to arrange. * @param offset * The offset for the decoration visual. * @param direction * The direction of the {@link Connection} at the point where the * decoration is arranged. */ protected void arrangeDecoration(Node decoration, Point offset, Vector direction) { // arrange on start of curve AffineTransform transform = new AffineTransform().translate(offset.x, offset.y); // arrange on curve direction if (!direction.isNull()) { Angle angleCW = new Vector(1, 0).getAngleCW(direction); transform.rotate(angleCW.rad(), 0, 0); } // compensate stroke (ensure decoration 'ends' at curve end). transform.translate(-NodeUtils.getShapeBounds(decoration).getX(), 0); // apply transform decoration.getTransforms().setAll(Geometry2FX.toFXAffine(transform)); } private void arrangeEndDecoration(Node endDecoration, ICurve curve, Point endPoint) { if (endDecoration == null) { return; } // determine curve end point and curve end direction // TODO: check if we can obtain end point as curve.get(1). if (curve == null || endPoint == null) { return; } BezierCurve[] beziers = curve.toBezier(); if (beziers.length == 0) { return; } BezierCurve endDerivative = beziers[beziers.length - 1].getDerivative(); Point slope = endDerivative.get(1); if (slope.equals(0, 0)) { /* * This is the case when beziers[-1] is a degenerated curve where * the last control point equals the end point. As a work around, we * evaluate the derivative at t = 0.99. */ slope = endDerivative.get(0.99); } Vector endDirection = new Vector(slope.getNegated()); arrangeDecoration(endDecoration, endPoint, endDirection); } private void arrangeStartDecoration(Node startDecoration, ICurve curve, Point startPoint) { // TODO: check if we can use curve.get(0) to obtain start point // determine curve start point and curve start direction if (curve == null || startPoint == null) { return; } BezierCurve[] beziers = curve.toBezier(); if (beziers.length == 0) { return; } BezierCurve startDerivative = beziers[0].getDerivative(); Point slope = startDerivative.get(0); if (slope.equals(0, 0)) { /* * This is the case when beziers[0] is a degenerated curve where the * start point equals the first control point. As a work around, we * evaluate the derivative at t = 0.01. */ slope = startDerivative.get(0.01); } Vector curveStartDirection = new Vector(slope); arrangeDecoration(startDecoration, startPoint, curveStartDirection); } /** * Adjusts the curveClip so that the curve node does not paint through the * given decoration. * * @param curveShape * A shape describing the {@link ICurve} geometry, which is used * for clipping. * * @param curveClip * A shape that represents the clip of the curve node, * interpreted in scene coordinates. * @param decoration * The decoration to clip the curve node from. * @return A shape representing the resulting clip, interpreted in scene * coordinates. */ protected Shape clipAtDecoration(Shape curveShape, Shape curveClip, Shape decoration) { // first intersect curve shape with decoration layout bounds, // then subtract the curve shape from the result, and the decoration // from that Path decorationShapeBounds = new Path( Geometry2Shape.toPathElements(NodeUtils .localToScene(decoration, NodeUtils.getShapeBounds(decoration)) .toPath())); decorationShapeBounds.setFill(Color.RED); Shape clip = Shape.intersect(decorationShapeBounds, curveShape); clip = Shape.subtract(clip, decoration); clip = Shape.subtract(curveClip, clip); return clip; } /** * Computes an {@link ICurve} geometry from the {@link Connection}'s points, * which is used to update the {@link Connection#getCurve() curve node}. * * @param connection * The {@link Connection}, for which to compute a new * {@link ICurve} geometry. * @return An {@link ICurve} that represents the to be rendered geometry. */ protected abstract ICurve computeCurve(Connection connection); @Override public void interpolate(Connection connection) { // compute new curve (this can lead to another refreshGeometry() call // which is not executed) ICurve newGeometry = computeCurve(connection); // XXX: we can only deal with geometry nodes so far @SuppressWarnings("unchecked") final GeometryNode<ICurve> curveNode = (GeometryNode<ICurve>) connection .getCurve(); if (curveNode instanceof GeometryNode && !newGeometry.equals(curveNode.getGeometry())) { // TODO: we need to prevent positions are re-calculated as a // result of the changed geometry. -> the static anchors should not // update their positions because of layout bounds changes. // System.out.println("New geometry: " + newGeometry); curveNode.setGeometry(newGeometry); } Node startDecoration = connection.getStartDecoration(); if (startDecoration != null) { arrangeStartDecoration(startDecoration, newGeometry, newGeometry.getP1()); } Node endDecoration = connection.getEndDecoration(); if (endDecoration != null) { arrangeEndDecoration(endDecoration, newGeometry, newGeometry.getP2()); } if (!newGeometry.getBounds().isEmpty() && (startDecoration != null || endDecoration != null)) { // XXX Use scene coordinates, as the clip node does not provide a // parent. // union curve node's children's bounds-in-parent org.eclipse.gef.geometry.planar.Rectangle unionBoundsInCurveNode = new org.eclipse.gef.geometry.planar.Rectangle(); ObservableList<Node> childrenUnmodifiable = curveNode .getChildrenUnmodifiable(); for (Node child : childrenUnmodifiable) { Bounds boundsInParent = child.getBoundsInParent(); org.eclipse.gef.geometry.planar.Rectangle rectangle = FX2Geometry .toRectangle(boundsInParent); unionBoundsInCurveNode.union(rectangle); } // convert unioned bounds to scene coordinates Bounds visualBounds = curveNode.localToScene( Geometry2FX.toFXBounds(unionBoundsInCurveNode)); // create clip Shape clip = new Rectangle(visualBounds.getMinX(), visualBounds.getMinY(), visualBounds.getWidth(), visualBounds.getHeight()); clip.setFill(Color.RED); // can only clip Shape decorations if (startDecoration != null && startDecoration instanceof Shape) { clip = clipAtDecoration(curveNode.getGeometricShape(), clip, (Shape) startDecoration); } // can only clip Shape decorations if (endDecoration != null && endDecoration instanceof Shape) { clip = clipAtDecoration(curveNode.getGeometricShape(), clip, (Shape) endDecoration); } // XXX: All CAG operations deliver result shapes that reflect areas // in scene coordinates. AffineTransform sceneToLocalTx = NodeUtils .getSceneToLocalTx(curveNode); clip.getTransforms().add(Geometry2FX.toFXAffine(sceneToLocalTx)); // set clip curveNode.setClip(clip); } else { curveNode.setClip(null); } } }