How to make proper connections in CASE tools
Contents
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:
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..