diff --git a/zxlive/eitem.py b/zxlive/eitem.py index bb19b340..85abb47f 100644 --- a/zxlive/eitem.py +++ b/zxlive/eitem.py @@ -78,12 +78,21 @@ def refresh(self) -> None: self.setPen(QPen(pen)) path = QPainterPath() - control_point = calculate_control_point(self.s_item.pos(), self.t_item.pos(), self.curve_distance) - path.moveTo(self.s_item.pos()) - path.quadTo(control_point, self.t_item.pos()) + if self.s_item == self.t_item: # self-loop + cd = self.curve_distance + cd = cd + 0.5 if cd >= 0 else cd - 0.5 + s_pos = self.s_item.pos() + path.moveTo(s_pos) + path.cubicTo(s_pos + QPointF(1, -1) * cd * SCALE, + s_pos + QPointF(-1, -1) * cd * SCALE, + s_pos) + curve_midpoint = s_pos + QPointF(0, -0.75) * cd * SCALE + else: + control_point = calculate_control_point(self.s_item.pos(), self.t_item.pos(), self.curve_distance) + path.moveTo(self.s_item.pos()) + path.quadTo(control_point, self.t_item.pos()) + curve_midpoint = self.s_item.pos() * 0.25 + control_point * 0.5 + self.t_item.pos() * 0.25 self.setPath(path) - - curve_midpoint = self.s_item.pos() * 0.25 + control_point * 0.5 + self.t_item.pos() * 0.25 self.selection_node.setPos(curve_midpoint.x(), curve_midpoint.y()) self.selection_node.setVisible(self.isSelected()) @@ -142,10 +151,11 @@ def refresh(self) -> None: path.lineTo(self.mouse_pos) self.setPath(path) -def calculate_control_point(source_pos, target_pos, curve_distance): +def calculate_control_point(source_pos: QPointF, target_pos: QPointF, curve_distance: float): """Calculate the control point for the curve""" direction = target_pos - source_pos - direction /= sqrt(direction.x()**2 + direction.y()**2) # Normalize the direction + norm = sqrt(direction.x()**2 + direction.y()**2) + direction = direction / norm perpendicular = QPointF(-direction.y(), direction.x()) midpoint = (source_pos + target_pos) / 2 offset = perpendicular * curve_distance * SCALE diff --git a/zxlive/graphscene.py b/zxlive/graphscene.py index 0b0cfc7b..bd11dd91 100644 --- a/zxlive/graphscene.py +++ b/zxlive/graphscene.py @@ -52,7 +52,7 @@ def __init__(self) -> None: self.setSceneRect(0, 0, 2*OFFSET_X, 2*OFFSET_Y) self.setBackgroundBrush(QBrush(QColor(255, 255, 255))) self.vertex_map: dict[VT, VItem] = {} - self.edge_map: dict[tuple[ET, int], EItem] = {} + self.edge_map: dict[ET, dict[int, EItem]] = {} @property def selected_vertices(self) -> Iterator[VT]: @@ -100,28 +100,22 @@ def update_graph(self, new: GraphT, select_new: bool = False) -> None: diff = GraphDiff(self.g, new) - removed_edges = set(diff.removed_edges) - for v in diff.removed_verts: v_item = self.vertex_map[v] if v_item.phase_item: self.removeItem(v_item.phase_item) for anim in v_item.active_animations.copy(): anim.stop() - for s, t in self.g.incident_edges(v): - for e in self.g.edges(s, t): - removed_edges.add(e) - selected_vertices.discard(v) self.removeItem(v_item) - for e in removed_edges: - edge_key = (e, self.g.graph[e[0]][e[1]].get_edge_count(e[2]) - 1) - e_item = self.edge_map[edge_key] + for e in diff.removed_edges: + edge_idx = len(self.edge_map[e]) - 1 + e_item = self.edge_map[e][edge_idx] if e_item.selection_node: self.removeItem(e_item.selection_node) self.removeItem(e_item) - self.edge_map.pop(edge_key) + self.edge_map[e].pop(edge_idx) s, t = self.g.edge_st(e) self.update_edge_curves(s, t) @@ -143,10 +137,12 @@ def update_graph(self, new: GraphT, select_new: bool = False) -> None: for e, typ in diff.new_edges: s, t = self.g.edge_st(e) - cur_edge_type_idx = self.g.graph[s][t].get_edge_count(typ) - 1 e = (s,t,typ) + if e not in self.edge_map: + self.edge_map[e] = {} + idx = len(self.edge_map[e]) e_item = EItem(self, e, self.vertex_map[s], self.vertex_map[t]) - self.edge_map[(e, cur_edge_type_idx)] = e_item + self.edge_map[e][idx] = e_item self.update_edge_curves(s, t) self.addItem(e_item) self.addItem(e_item.selection_node) @@ -171,28 +167,27 @@ def update_graph(self, new: GraphT, select_new: bool = False) -> None: v_item.set_vitem_rotation() for e in diff.changed_edge_types: - for i in range(self.g.graph[e[0]][e[1]].get_edge_count(e[2])): - self.edge_map[(e, i)].refresh() + for i in self.edge_map[e]: + self.edge_map[e][i].refresh() self.select_vertices(selected_vertices) def update_edge_curves(self, s, t): edges = [] for e in set(self.g.edges(s, t)): - for i in range(self.g.graph[s][t].get_edge_count(e[2])): - edge_key = (e, i) - if edge_key in self.edge_map: - edges.append(edge_key) + for i in self.edge_map[e]: + edges.append(self.edge_map[e][i]) midpoint_index = 0.5 * (len(edges) - 1) for n, edge in enumerate(edges): - self.edge_map[edge].curve_distance = (n - midpoint_index) * 0.5 - self.edge_map[edge].refresh() + edge.curve_distance = (n - midpoint_index) * 0.5 + edge.refresh() def update_colors(self) -> None: for v in self.vertex_map.values(): v.refresh() for e in self.edge_map.values(): - e.refresh() + for ei in e.values(): + ei.refresh() def add_items(self) -> None: """Add QGraphicsItem's for all vertices and edges in the graph""" @@ -206,12 +201,12 @@ def add_items(self) -> None: self.edge_map = {} for e in set(self.g.edges()): s, t = self.g.edge_st(e) + self.edge_map[e] = {} for i in range(self.g.graph[s][t].get_edge_count(e[2])): ei = EItem(self, e, self.vertex_map[s], self.vertex_map[t]) self.addItem(ei) self.addItem(ei.selection_node) - edge_key = (e, i) - self.edge_map[edge_key] = ei + self.edge_map[e][i] = ei self.update_edge_curves(s, t) def select_all(self) -> None: @@ -292,9 +287,6 @@ def add_edge(self, e: QGraphicsSceneMouseEvent) -> None: assert self._drag is not None self.removeItem(self._drag) for it in self.items(e.scenePos(), deviceTransform=QTransform()): - # TODO: Think about if we want to allow self loops here? - # For example, if had edge is selected this would mean that - # right clicking adds pi to the phase... - if isinstance(it, VItem) and it != self._drag.start: + if isinstance(it, VItem): self.edge_added.emit(self._drag.start.v, it.v) self._drag = None