--- /dev/null
+/*******************************************************************************
+ * Copyright (c) 2020 Association for Decentralized Information Management in
+ * Industry THTH ry.
+ * 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:
+ * Semantum Oy - initial API and implementation
+ *******************************************************************************/
+package org.simantics.diagram.connection.rendering;
+
+import java.awt.geom.Arc2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.PathIterator;
+import java.awt.geom.Point2D;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ConnectionCrossings implements PathModifier {
+ public enum Type {
+ NONE,
+ GAP,
+ ARC,
+ SQUARE
+ }
+
+ private List<Segment> segments = new ArrayList<>();
+ private double width;
+ private Type type;
+
+ public void setWidth(double gapWidth) {
+ this.width = gapWidth;
+ }
+
+ public double getWidth() {
+ return width;
+ }
+
+ public void setType(Type type) {
+ this.type = type;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ static class Segment {
+ public double x1, y1, x2, y2;
+
+ public Segment(double x1, double y1, double x2, double y2) {
+ this.x1 = x1;
+ this.y1 = y1;
+ this.x2 = x2;
+ this.y2 = y2;
+ }
+ };
+
+ public void reset() {
+ segments.clear();
+ }
+
+ static Double lineLineIntersection(Segment l1, Segment l2) {
+ double epsilon = 0.001;
+
+ double d = (l1.x1 - l1.x2) * (l2.y1 - l2.y2) - (l1.y1 - l1.y2) * (l2.x1 - l2.x2);
+ if (d == 0.0) return null;
+ double s = ((l1.x1 - l2.x1) * (l2.y1 - l2.y2) - (l1.y1 - l2.y1) * (l2.x1 - l2.x2)) / d;
+ if ((s > epsilon) && (s < 1 - epsilon)) {
+ double t = -((l1.x1 - l1.x2) * (l1.y1 - l2.y1) - (l1.y1 - l1.y2) * (l1.x1 - l2.x1)) / d;
+ if ((t > epsilon) && (t < 1 - epsilon)) {
+ return t;
+ }
+ }
+ return null;
+ }
+
+ public Path2D modify(Path2D path) {
+ Path2D.Double path2 = new Path2D.Double();
+ PathIterator iter = path.getPathIterator(null);
+
+ while (!iter.isDone()) {
+
+ double c[] = new double[6];
+ int i = iter.currentSegment(c);
+ switch (i) {
+ case PathIterator.SEG_MOVETO:
+ path2.moveTo(c[0], c[1]);
+ break;
+ case PathIterator.SEG_LINETO:
+ Segment l = new Segment(path2.getCurrentPoint().getX(), path2.getCurrentPoint().getY(), c[0], c[1]);
+
+ List<Double> gaps = new ArrayList<>();
+ for (Segment old : segments) {
+ Double t = lineLineIntersection(old, l);
+ if (t != null) {
+ gaps.add(t);
+ }
+ }
+
+ if (gaps.isEmpty()) {
+ path2.lineTo(c[0], c[1]);
+ } else {
+ Collections.sort(gaps);
+ double dx = l.x2 - l.x1;
+ double dy = l.y2 - l.y1;
+
+ double pos = 0.0;
+ double len = Math.sqrt(dx*dx + dy*dy);
+
+ boolean finish = true;
+ Point2D prevGapEnd = null;
+ for (Double gapCenter : gaps) {
+ double pos2 = gapCenter - width / 2 / len;
+ double pos3 = gapCenter + width / 2 / len;
+ if (pos2 > pos) {
+ handleGap(path2, prevGapEnd);
+ prevGapEnd = null;
+ path2.lineTo(l.x1 + pos2 * dx, l.y1 + pos2 * dy);
+ }
+ if (pos3 < 1.0) {
+ double x = l.x1 + pos3 * dx;
+ double y = l.y1 + pos3 * dy;
+ prevGapEnd = new Point2D.Double(x, y);
+ } else {
+ finish = false;
+ }
+ pos = pos3;
+ }
+
+ if (finish) {
+ handleGap(path2, prevGapEnd);
+ path2.lineTo(l.x2, l.y2);
+ } else {
+ prevGapEnd = new Point2D.Double(l.x2, l.y2);
+ handleGap(path2, prevGapEnd);
+ }
+ }
+ segments.add(l);
+
+ break;
+ case PathIterator.SEG_QUADTO:
+ // TODO: implement gaps
+ path2.quadTo(c[0], c[1], c[2], c[3]);
+ break;
+ case PathIterator.SEG_CUBICTO:
+ // TODO: implement gaps
+ path2.curveTo(c[0], c[1], c[2], c[3], c[4], c[5]);
+ break;
+ case PathIterator.SEG_CLOSE:
+ // TODO: implement gaps
+ path2.closePath();
+ break;
+ default:
+ throw new RuntimeException("Unexpected segment type " + i);
+ }
+ iter.next();
+ }
+ return path2;
+ }
+
+ private void handleGap(Path2D path, Point2D prevGapEnd) {
+ if (prevGapEnd != null) {
+ switch (type) {
+ case ARC:
+ arcTo(path, prevGapEnd.getX(), prevGapEnd.getY());
+ break;
+ case SQUARE:
+ squareTo(path, prevGapEnd.getX(), prevGapEnd.getY(), width);
+ break;
+ case GAP:
+ path.moveTo(prevGapEnd.getX(), prevGapEnd.getY());
+ break;
+ case NONE:
+ break;
+ }
+ }
+ }
+
+ private static void arcTo(Path2D path, double x2, double y2) {
+ Arc2D arc = new Arc2D.Double();
+ double x1 = path.getCurrentPoint().getX();
+ double y1 = path.getCurrentPoint().getY();
+ double dx = x2 - x1;
+ double dy = y2 - y1;
+ double r = Math.sqrt(dx * dx + dy * dy) / 2;
+ double angle = Math.atan2(dx, dy) * 180 / Math.PI + 90;
+ double span = (angle > 225 || angle < 45) ? 180 : -180;
+ arc.setArcByCenter((x1 + x2) / 2, (y1 + y2) / 2, r, angle, span, Arc2D.OPEN);
+ path.append(arc, true);
+ }
+
+ private static void squareTo(Path2D path, double x2, double y2, double width) {
+ double x1 = path.getCurrentPoint().getX();
+ double y1 = path.getCurrentPoint().getY();
+ double dx = x2 - x1;
+ double dy = y2 - y1;
+ double l = Math.sqrt(dx * dx + dy* dy);
+ if (l > 0) {
+ double nx = -dy / l;
+ double ny = dx / l;
+ if (nx - ny < 0) {
+ nx = -nx;
+ ny = -ny;
+ }
+ path.lineTo(x1 + nx * width / 2, y1 + ny * width / 2);
+ path.lineTo(x2 + nx * width / 2, y2 + ny * width / 2);
+ path.lineTo(x2, y2);
+ }
+ }
+
+}
--- /dev/null
+/*******************************************************************************
+ * Copyright (c) 2020 Association for Decentralized Information Management in
+ * Industry THTH ry.
+ * 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:
+ * Semantum Oy - initial API and implementation
+ *******************************************************************************/
+package org.simantics.diagram.connection.rendering;
+
+import java.awt.RenderingHints.Key;
+
+public final class ConnectionRenderingHints {
+
+ public static final Key KEY_PATH_MODIFIER = new Key(0) {
+ @Override
+ public boolean isCompatibleValue(Object val) {
+ return val == null || val instanceof PathModifier;
+ }
+ };
+}
\ No newline at end of file
--- /dev/null
+/*******************************************************************************
+ * Copyright (c) 2020 Association for Decentralized Information Management in
+ * Industry THTH ry.
+ * 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:
+ * Semantum Oy - initial API and implementation
+ *******************************************************************************/
+package org.simantics.diagram.connection.rendering;
+
+import java.awt.geom.Path2D;
+
+public interface PathModifier {
+ public Path2D modify(Path2D source);
+}
path.reset();
rg.getPath2D(path);
+ PathModifier pm = (PathModifier) g.getRenderingHint(ConnectionRenderingHints.KEY_PATH_MODIFIER);
+ if (pm != null) {
+ path = pm.modify(path);
+ }
style.drawPath(g, path, false);
branchPoints.clear();
DIA.Diagram <T DIA.Composite
>-- DIA.Diagram.IOTableRenaming --> DIA.IOTableRename <T L0.DependsOn
@L0.optionalProperty DIA.HasModCount
+ @L0.assert DIA.ConnectionCrossingStyle ""
+ @L0.assert DIA.ConnectionCrossingStyle.Width 0.0
+ @L0.assert DIA.ConnectionCrossingStyle.HasType DIA.ConnectionCrossingStyle.Type.None
DIA.HasModCount <R L0.HasProperty : L0.FunctionalRelation
L0.HasLabel "Modification Counter"
DIA.ProfileEntryContribution <T L0.Entity
--> DIA.ProfileEntryContribution.HasEntry --> DIA.ProfileEntry <R L0.IsRelatedTo
-
\ No newline at end of file
+
+DIA.ConnectionCrossingStyle <R L0.HasProperty : L0.FunctionalRelation : SEL.GenericParameterType
+ L0.HasLabel "Connection Crossing Style"
+ SEL.HasDisplayValue ""
+ L0.readOnly true
+ SEL.canBeLifted false
+
+DIA.ConnectionCrossingStyle.Width <R L0.HasProperty : L0.FunctionalRelation : SEL.GenericParameterType
+ L0.HasLabel "Width"
+ L0.HasDescription "Width of connection crossings."
+ L0.HasDomain DIA.Diagram
+ L0.HasRange L0.Double
+ SEL.IsShownUnder DIA.ConnectionCrossingStyle
+
+DIA.ConnectionCrossingStyle.HasType <R L0.HasProperty : L0.FunctionalRelation : SEL.GenericParameterType
+ L0.HasLabel "Type"
+ L0.HasDescription "Type of connection crossings."
+ L0.HasDomain DIA.Diagram
+ L0.HasRange DIA.ConnectionCrossingStyle.Type
+ SEL.IsShownUnder DIA.ConnectionCrossingStyle
+
+DIA.ConnectionCrossingStyle.Type <T L0.Value
+ @L0.tag L0.Enumeration
+
+DIA.ConnectionCrossingStyle.Type.Arc : DIA.ConnectionCrossingStyle.Type
+DIA.ConnectionCrossingStyle.Type.Square : DIA.ConnectionCrossingStyle.Type
+DIA.ConnectionCrossingStyle.Type.Gap : DIA.ConnectionCrossingStyle.Type
+DIA.ConnectionCrossingStyle.Type.None : DIA.ConnectionCrossingStyle.Type
\ No newline at end of file
--- /dev/null
+/*******************************************************************************
+ * Copyright (c) 2020 Association for Decentralized Information Management in
+ * Industry THTH ry.
+ * 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:
+ * Semantum Oy - initial API and implementation
+ *******************************************************************************/
+package org.simantics.diagram.participant;
+
+import org.simantics.Simantics;
+import org.simantics.db.ReadGraph;
+import org.simantics.db.Resource;
+import org.simantics.db.common.request.UnaryRead;
+import org.simantics.db.exception.DatabaseException;
+import org.simantics.db.procedure.Listener;
+import org.simantics.diagram.connection.rendering.ConnectionCrossings;
+import org.simantics.diagram.stubs.DiagramResource;
+import org.simantics.g2d.canvas.ICanvasContext;
+import org.simantics.g2d.canvas.SGDesignation;
+import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;
+import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit;
+import org.simantics.g2d.diagram.participant.AbstractDiagramParticipant;
+import org.simantics.scenegraph.g2d.G2DParentNode;
+import org.simantics.scenegraph.g2d.nodes.ConnectionCrossingsNode;
+import org.simantics.utils.datastructures.Pair;
+import org.simantics.utils.ui.ErrorLogger;
+
+public class ConnectionCrossingsParticipant extends AbstractDiagramParticipant {
+ private static final String CONNECTION_CROSSINGS_NODE_KEY = "connection-crossings";
+
+ private ConnectionCrossingsNode ccNode;
+ private final ConnectionCrossings crossings = new ConnectionCrossings();
+ private ConnectionCrossingStyleListener listener;
+ private Resource diagram;
+
+ public ConnectionCrossingsParticipant(Resource diagram) {
+ this.diagram = diagram;
+ }
+ public ConnectionCrossingsParticipant(double width, ConnectionCrossings.Type type) {
+ crossings.setWidth(width);
+ crossings.setType(type);
+ }
+
+ @SGInit(designation = SGDesignation.CONTROL)
+ public void initSG(G2DParentNode parent) {
+ ccNode = parent.addNode(CONNECTION_CROSSINGS_NODE_KEY, ConnectionCrossingsNode.class);
+ ccNode.setCrossings(crossings);
+ ccNode.setZIndex(Integer.MIN_VALUE / 4);
+ }
+
+ @SGCleanup
+ public void cleanupSG() {
+ if (ccNode != null) {
+ ccNode.remove();
+ ccNode = null;
+ }
+ }
+
+ @Override
+ public void addedToContext(ICanvasContext ctx) {
+ super.addedToContext(ctx);
+
+ if (diagram != null) {
+ listener = new ConnectionCrossingStyleListener(ctx);
+ Simantics.getSession().async(new UnaryRead<Resource, Pair<Double, ConnectionCrossings.Type>>(diagram){
+
+ @Override
+ public Pair<Double, ConnectionCrossings.Type> perform(ReadGraph graph) throws DatabaseException {
+ DiagramResource DIA = DiagramResource.getInstance(graph);
+ Double gap = graph.getPossibleRelatedValue(diagram, DIA.ConnectionCrossingStyle_Width);
+ Resource typeRes = graph.getPossibleObject(diagram, DIA.ConnectionCrossingStyle_HasType);
+ ConnectionCrossings.Type type;
+ if (DIA.ConnectionCrossingStyle_Type_Gap.equals(typeRes)) {
+ type = ConnectionCrossings.Type.GAP;
+ } else if (DIA.ConnectionCrossingStyle_Type_Arc.equals(typeRes)) {
+ type = ConnectionCrossings.Type.ARC;
+ } else if (DIA.ConnectionCrossingStyle_Type_Square.equals(typeRes)) {
+ type = ConnectionCrossings.Type.SQUARE;
+ } else {
+ type = ConnectionCrossings.Type.NONE;
+ }
+ return new Pair<>(gap, type);
+ }
+
+ }, listener);
+ }
+ }
+
+ @Override
+ public void removedFromContext(ICanvasContext ctx) {
+ if (listener != null) {
+ listener.dispose();
+ listener = null;
+ }
+ super.removedFromContext(ctx);
+ }
+
+ class ConnectionCrossingStyleListener implements Listener<Pair<Double, ConnectionCrossings.Type>> {
+ ICanvasContext context;
+ public ConnectionCrossingStyleListener(ICanvasContext context) {
+ this.context = context;
+ }
+ @Override
+ public void execute(final Pair<Double, ConnectionCrossings.Type> result) {
+ context.getThreadAccess().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ ICanvasContext ctx = context;
+ if (ctx == null)
+ return;
+ if (ctx.isDisposed())
+ return;
+ crossings.setWidth(result.first != null ? result.first : 0.0);
+ crossings.setType(result.second);
+ ccNode.repaint();
+ }
+ });
+ }
+ public void dispose() {
+ context = null;
+ }
+ @Override
+ public boolean isDisposed() {
+ return context == null || context.isDisposed();
+ }
+ @Override
+ public void exception(Throwable t) {
+ ErrorLogger.defaultLogError(t);
+ }
+ }
+
+}
import org.simantics.diagram.handler.ExpandSelectionHandler;
import org.simantics.diagram.handler.SimpleElementTransformHandler;
import org.simantics.diagram.layer.ILayersViewPage;
+import org.simantics.diagram.participant.ConnectionCrossingsParticipant;
import org.simantics.diagram.participant.ContextUtil;
import org.simantics.diagram.participant.PointerInteractor2;
import org.simantics.diagram.participant.SGFocusParticipant;
//ctx.add(new ZoomTransitionParticipant(TransitionFunction.SIGMOID));
//ctx.add(new TooltipParticipant());
ctx.add(new TerminalTooltipParticipant());
+ ctx.add(new ConnectionCrossingsParticipant(getInputResource()));
}
protected void addPainterParticipants(ICanvasContext ctx) {
import org.simantics.diagram.handler.DefaultCopyPasteStrategy;
import org.simantics.diagram.handler.DeleteHandler;
import org.simantics.diagram.handler.SimpleElementTransformHandler;
+import org.simantics.diagram.participant.ConnectionCrossingsParticipant;
import org.simantics.diagram.query.DiagramRequests;
import org.simantics.diagram.runtime.RuntimeDiagramManager;
import org.simantics.diagram.stubs.DiagramResource;
ctx.add( new Selection() );
ctx.add( new DiagramParticipant() );
ctx.add( new ElementPainter(true) );
+ ctx.add( new ConnectionCrossingsParticipant(resource));
//ctx.add( new ElementHeartbeater() );
ctx.add( new ZOrderHandler() );
--- /dev/null
+/*******************************************************************************
+ * Copyright (c) 2020 Association for Decentralized Information Management in
+ * Industry THTH ry.
+ * 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:
+ * Semantum Oy - initial API and implementation
+ *******************************************************************************/
+package org.simantics.scenegraph.g2d.nodes;
+
+import java.awt.Graphics2D;
+import java.awt.geom.Rectangle2D;
+
+import org.simantics.diagram.connection.rendering.ConnectionCrossings;
+import org.simantics.diagram.connection.rendering.ConnectionRenderingHints;
+import org.simantics.scenegraph.g2d.G2DNode;
+
+public class ConnectionCrossingsNode extends G2DNode {
+
+ private static final long serialVersionUID = -696142275610396889L;
+
+ private ConnectionCrossings crossings;
+
+ @Override
+ public void render(Graphics2D g) {
+ crossings.reset();
+ if (crossings.getWidth() > 0 && crossings.getType() != ConnectionCrossings.Type.NONE) {
+ g.setRenderingHint(ConnectionRenderingHints.KEY_PATH_MODIFIER, crossings);
+ } else {
+ g.setRenderingHint(ConnectionRenderingHints.KEY_PATH_MODIFIER, null);
+ }
+ }
+
+ public void setCrossings(ConnectionCrossings crossings) {
+ this.crossings = crossings;
+ }
+
+ @Override
+ public Rectangle2D getBoundsInLocal() {
+ return null;
+ }
+
+ public ConnectionCrossings getConnectionCrossings() {
+ return crossings;
+ }
+}