Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multigraph self loop support #260

Merged
merged 6 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions zxlive/eitem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down Expand Up @@ -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
Expand Down
48 changes: 20 additions & 28 deletions zxlive/graphscene.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand All @@ -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"""
Expand All @@ -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:
Expand Down Expand Up @@ -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
Loading