/******************************************************************************* * 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 * Tamas Miklossy (itemis AG) - Add support for arrowType edge decorations (bug #477980) * *******************************************************************************/ package org.eclipse.gef.dot.internal.ui; import java.util.ArrayList; import java.util.List; import org.eclipse.gef.dot.internal.ui.DotArrowShapeDecorations.IPrimitiveShape; import org.eclipse.gef.fx.anchors.AnchorKey; import org.eclipse.gef.fx.anchors.DynamicAnchor; import org.eclipse.gef.fx.anchors.DynamicAnchor.AnchoredReferencePoint; import org.eclipse.gef.fx.nodes.AbstractInterpolator; import org.eclipse.gef.fx.nodes.Connection; import org.eclipse.gef.fx.nodes.IConnectionInterpolator; 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.CubicCurve; import org.eclipse.gef.geometry.planar.ICurve; import org.eclipse.gef.geometry.planar.Line; import org.eclipse.gef.geometry.planar.Point; import org.eclipse.gef.geometry.planar.PolyBezier; import javafx.scene.Group; import javafx.scene.Node; /** * A {@link DotBSplineInterpolator} is an {@link IConnectionInterpolator * interpolator} that creates a {@link PolyBezier} geometry corresponding to a * single B-spline. It expects that the start, end, and control points of the * {@link Connection} it routes correspond to what can be specified through the * 'pos' attribute of the edges within Graphviz DOT as follows (if multiple * splines are specified through the 'pos' attribute, they have to be * represented through multiple connections). * <p> * The {@link DotBSplineInterpolator} expects that the connection's * {@link Connection#getControlPoints() control points} represent control points * of connected cubic Bézier segments in the form 'p, (p, p, p)+'. In case the * start point equals the first control point, or the end point equals the last * control point, they are ignored when constructing the B-spline. In case this * is not the case, linear segments are added from the start point to the first * control point and from the last control point to the end point, respectively. * * @author anyssen * */ public class DotBSplineInterpolator extends AbstractInterpolator { @Override protected ICurve computeCurve(Connection connection) { // { // System.out.println("Connection:"); // System.out.println(" sp) " + connection.getStartPoint() + " (hint=" // + connection.getStartPointHint() + ")"); // List<Point> controlPoints = connection.getControlPoints(); // for (int i = 0; i < controlPoints.size(); i++) { // System.out.println(String.format(" cp%02d) ", i + 1) // + controlPoints.get(i)); // } // System.out.println(" ep) " + connection.getEndPoint() + " (hint=" // + connection.getEndPointHint() + ")"); // } Point start = connection.getStartPoint(); Point end = connection.getEndPoint(); // return a line in case we have no start or end point or the points do // not correctly specify bezier segments. List<Point> controlPoints = connection.getControlPoints(); int numControlPoints = controlPoints.size(); if (start == null || end == null) { return new Line(0, 0, 0, 0); } else if (numControlPoints < 4) { return new Line(start, end); } // obtain start and end reference points, which have to be used to infer // whether the first and last control point have to be evaluated. Point startReference = connection .getStartAnchor() instanceof DynamicAnchor ? connection.getStartPointHint() : connection.getStartPoint(); // TODO: maybe startReference == null // if (startReference == null) { // startReference = connection.getStartPoint(); // } Point endReference = connection.getEndAnchor() instanceof DynamicAnchor ? connection.getEndPointHint() : connection.getEndPoint(); // TODO: maybe endReference == null // if (endReference == null) { // endReference = connection.getEndPoint(); // } // the first and last control point may be equal to the start and end // anchor reference points, in which case we have to ignore the control // points; else we need to add a line segment from the first control // point to the List<BezierCurve> segments = new ArrayList<>(); Point p0 = controlPoints.get(0); if (!startReference.equals(p0)) { // XXX: Currently, the start point that was computed by the anchor // is overridden by using the startReference here. Once the anchor // computation yields the same value as supplied by dot, we should // be able to use start instead of startReference here. segments.add(new Line(startReference, p0)); } else { p0 = start; } // process segments Point p2 = null; for (int i = 1; i + 2 < numControlPoints; i += 3) { p2 = controlPoints.get(i + 2); if (i + 2 == numControlPoints - 1) { if (endReference.equals(p2)) { p2 = end; } } segments.add(new CubicCurve(p0, controlPoints.get(i), controlPoints.get(i + 1), p2)); // keep track of the last control point of the respective segment // (which is the start point of the next segment) } if (!endReference.equals(p2)) { // XXX: Currently, the end point that was computed by the anchor // is overridden by using the endReference here. Once the anchor // computation yields the same value as supplied by dot, we should // be able to use end instead of endReference here. segments.add(new Line(p2, endReference)); } return new PolyBezier(segments.toArray(new BezierCurve[] {})); } protected Point getProjectionReferencePoint(DynamicAnchor anchor, AnchorKey anchorKey) { return anchor.getComputationParameter(anchorKey, AnchoredReferencePoint.class).get(); } @Override 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(getOffsetForNode(decoration), 0); // apply transform decoration.getTransforms().setAll(Geometry2FX.toFXAffine(transform)); } private double getOffsetForNode(Node decoration) { if (decoration instanceof Group) { List<Node> children = ((Group) decoration).getChildren(); if (!children.isEmpty()) { Node firstChild = children.get(0); if (firstChild instanceof IPrimitiveShape) { double offset = ((IPrimitiveShape) firstChild).getOffset(); return offset; } } } return 0.0; } }