How to make proper connections in CASE tools

From TASTE
Jump to: navigation, search

Introduction

This short article aims at helping CASE tool developers to handle properly the update of connections between boxes when they are moved.

Most tools do it wrong and the results is that users have to spend a lot of time placing arrows at the right place.

Author: Maxime Perrotin

Expected output

We aim at allowing all kinds of connections - straight lignes or multiple angles, and we want that when moving a block, the overall diagram respects the connection shape given by the user.

This is the example for which we are going to provide the algorithm:

PerfectConnectionsMove Opengeode.gif

Algorithm

The main idea is that the shapes have to keep their proportions when the block moves away or gets closer.

When creating a connection made of multiple points, we must not only store the coordinates of the points themselves, but also their proportional distance to the starting point. When the block is moved, the proportion can be applied back. There are a few special cases to keep right angles on the first or last connection point in place.

When the connection is created

The following Python snippet shows what to store and how. It is based on PySide2, but the same would apply with Qt/C++ or any other GUI library.

   @middle_points.setter
   def middle_points(self, points_scene_coord):
       # compute the distance between the start and end points
       dist_x = abs(self.end_point.x() - self.start_point.x())
       dist_y = abs(self.end_point.y() - self.start_point.y())
       # Compute the distance ratio
       self._ratios = []
       for point in points_scene_coord:
           pCoord = self.parent.mapFromScene(point)
           len_x = abs(pCoord.x() - self.start_point.x())
           len_y = abs(pCoord.y() - self.start_point.y())
           fact_x = 1 if dist_x == 0 else len_x / dist_x
           fact_y = 1 if dist_y == 0 else len_y / dist_y
           if pCoord.y() == self.start_point.y():
               fact_y = -fact_y
           if pCoord.x() == self.start_point.x():
               fact_x = -fact_x
           self._ratios.append((fact_x, fact_y))
       self._middle_points = points_scene_coord

When the destination block is moved

The connection's parent is the source block. You have to pay attention to use the right coordinate system.

   @Slot(float, float)
   def child_moved(self, delta_x, delta_y):
        
       # Move the end point according to the mouse movement
       self._end_point.setX(self._end_point.x() - delta_x)
       self._end_point.setY(self._end_point.y() - delta_y)

       # Get the current position of each middle point
       middle_points = list(self.middle_points)

       # Start from a new list of middle points to compute new coordinates
       self._middle_points = []

       for ratio, point in zip(self._ratios, middle_points):
           # Point is in local coord, as start and end points
           sp = self.start_point
           fact_x, fact_y = ratio
           if point.x() == sp.x() and fact_x < 0:
               new_x = point.x()
           elif 0 <= fact_x < 1:
               new_x = point.x() - (delta_x * fact_x)
           else:
               new_x = point.x() - delta_x
           if point.y() == sp.y() and fact_y < 0:
               new_y = point.y()
           elif 0 <= fact_y < 1:
               new_y = point.y() - (delta_y * fact_y)
           else:
               new_y = point.y() - delta_y

           self._middle_points.append(
                   self.parent.mapToScene(QPointF(new_x, new_y)))

       self.reshape()
       self.update() # force a repaint

When the parent block is moved

Apply a similar move..