Skip to content

Commit

Permalink
Add toggle to automatically connect new vertex to edge underneath it (#…
Browse files Browse the repository at this point in the history
…337)

* Made previews only load once you first hover over the rewrite rule

* Add button to automatically snap vertices to edges underneath them

* Add animation to edge when adding a snapped vertex

* create a rectangle around mouse position to check for interesection with edges to snap vertex to edge

* Added icon to snap-to-edge tool

* Now newly added edges also snap to all underlying vertices!

* Add animation to affected vertices

* Added exception for W nodes

* Update description of tool

* Comments and whitespace

* Fix mypy errors

---------

Co-authored-by: Razin Shaikh <[email protected]>
  • Loading branch information
jvdwetering and RazinShaikh authored Aug 7, 2024
1 parent f1f15a6 commit 47f9271
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 34 deletions.
15 changes: 15 additions & 0 deletions zxlive/animations.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .common import VT, GraphT, pos_to_view, ANIMATION_DURATION
from .graphscene import GraphScene
from .vitem import VItem, VItemAnimation, VITEM_UNSELECTED_Z, VITEM_SELECTED_Z, get_w_partner_vitem
from .eitem import EItem, EItemAnimation

if TYPE_CHECKING:
from .proof_panel import ProofPanel
Expand Down Expand Up @@ -69,6 +70,13 @@ def _push_now(self, cmd: QUndoCommand, anim_after: Optional[QAbstractAnimation]
anim_after.start()
self.running_anim = anim_after

def set_anim(self, anim: QAbstractAnimation) -> None:
if self.running_anim:
self.running_anim.stop()
self.running_anim = anim
self.running_anim.start()



def scale(it: VItem, target: float, duration: int, ease: QEasingCurve, start: Optional[float] = None) -> VItemAnimation:
anim = VItemAnimation(it, VItem.Properties.Scale)
Expand All @@ -89,6 +97,13 @@ def move(it: VItem, target: QPointF, duration: int, ease: QEasingCurve, start: O
anim.setEasingCurve(ease)
return anim

def edge_thickness(it: EItem, target: float, duration: int, ease: QEasingCurve, start: Optional[float] = None) -> EItemAnimation:
anim = EItemAnimation(it, EItem.Properties.Thickness, refresh=True)
anim.setDuration(duration)
anim.setStartValue(start or it.thickness)
anim.setEndValue(target)
anim.setEasingCurve(ease)
return anim

def morph_graph(start: GraphT, end: GraphT, scene: GraphScene, to_start: Callable[[VT], Optional[VT]],
to_end: Callable[[VT], Optional[VT]], duration: int, ease: QEasingCurve) -> QAbstractAnimation:
Expand Down
60 changes: 59 additions & 1 deletion zxlive/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,48 @@ def redo(self) -> None:
self._added_vert = self.g.add_vertex(self.vty, y,x)
self.update_graph_view()

@dataclass
class AddNodeSnapped(BaseCommand):
"""Adds a new spider positioned on an edge, replacing the original edge"""
x: float
y: float
vty: VertexType
e: ET

added_vert: Optional[VT] = field(default=None, init=False)
s: Optional[VT] = field(default=None, init=False)
t: Optional[VT] = field(default=None, init=False)
_et: Optional[EdgeType] = field(default=None, init=False)

def undo(self) -> None:
assert self.added_vert is not None
assert self.s is not None
assert self.t is not None
assert self._et is not None
self.g.remove_vertex(self.added_vert)
self.g.add_edge(self.g.edge(self.s,self.t), self._et)
self.update_graph_view()

def redo(self) -> None:
y = round(self.y * display_setting.SNAP_DIVISION) / display_setting.SNAP_DIVISION
x = round(self.x * display_setting.SNAP_DIVISION) / display_setting.SNAP_DIVISION
self.added_vert = self.g.add_vertex(self.vty, y,x)
s,t = self.g.edge_st(self.e)
self._et = self.g.edge_type(self.e)
if self._et == EdgeType.SIMPLE:
self.g.add_edge(self.g.edge(s, self.added_vert), EdgeType.SIMPLE)
self.g.add_edge(self.g.edge(t, self.added_vert), EdgeType.SIMPLE)
elif self._et == EdgeType.HADAMARD:
self.g.add_edge(self.g.edge(s, self.added_vert), EdgeType.HADAMARD)
self.g.add_edge(self.g.edge(t, self.added_vert), EdgeType.SIMPLE)
else:
raise ValueError("Can't add spider between vertices connected by edge of type", str(self._et))
self.s = s
self.t = t

self.g.remove_edge(self.e)
self.update_graph_view()

@dataclass
class AddWNode(BaseCommand):
"""Adds a new W node at a given position."""
Expand Down Expand Up @@ -245,7 +287,23 @@ def undo(self) -> None:
self.update_graph_view()

def redo(self) -> None:
self.g.add_edge(((self.u, self.v)), self.ety)
self.g.add_edge((self.u, self.v), self.ety)
self.update_graph_view()

@dataclass
class AddEdges(BaseCommand):
"""Adds multiple edges of the same type to a graph."""
pairs: list[tuple[VT,VT]]
ety: EdgeType

def undo(self) -> None:
for u, v in self.pairs:
self.g.remove_edge((u, v, self.ety))
self.update_graph_view()

def redo(self) -> None:
for u, v in self.pairs:
self.g.add_edge((u, v), self.ety)
self.update_graph_view()


Expand Down
91 changes: 79 additions & 12 deletions zxlive/editor_base_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from enum import Enum
from typing import Callable, Iterator, TypedDict

from PySide6.QtCore import QPoint, QSize, Qt, Signal
from PySide6.QtCore import QPoint, QPointF, QSize, Qt, Signal, QEasingCurve, QParallelAnimationGroup
from PySide6.QtGui import (QAction, QColor, QIcon, QPainter, QPalette, QPen,
QPixmap)
QPixmap, QTransform)
from PySide6.QtWidgets import (QApplication, QComboBox, QFrame, QGridLayout,
QInputDialog, QLabel, QListView, QListWidget,
QListWidgetItem, QScrollArea, QSizePolicy,
Expand All @@ -17,15 +17,17 @@
from zxlive.sfx import SFXEnum

from .base_panel import BasePanel, ToolbarSection
from .commands import (AddEdge, AddNode, AddWNode, ChangeEdgeColor,
from .commands import (BaseCommand, AddEdge, AddEdges, AddNode, AddNodeSnapped, AddWNode, ChangeEdgeColor,
ChangeNodeType, ChangePhase, MoveNode, SetGraph,
UpdateGraph)
from .common import VT, GraphT, ToolType, get_data
from .dialogs import show_error_msg
from .eitem import HAD_EDGE_BLUE
from .eitem import EItem, HAD_EDGE_BLUE, EItemAnimation
from .vitem import VItem, BLACK, VItemAnimation
from .graphscene import EditGraphScene
from .settings import display_setting
from .vitem import BLACK

from . import animations


class ShapeType(Enum):
Expand Down Expand Up @@ -67,14 +69,15 @@ class EditorBasePanel(BasePanel):

_curr_ety: EdgeType
_curr_vty: VertexType
snap_vertex_edge = True

def __init__(self, *actions: QAction) -> None:
super().__init__(*actions)
self._curr_vty = VertexType.Z
self._curr_ety = EdgeType.SIMPLE

def _toolbar_sections(self) -> Iterator[ToolbarSection]:
yield toolbar_select_node_edge(self)
yield from toolbar_select_node_edge(self)
yield ToolbarSection(*self.actions())

def create_side_bar(self) -> None:
Expand All @@ -98,6 +101,9 @@ def update_colors(self) -> None:
def _tool_clicked(self, tool: ToolType) -> None:
self.graph_scene.curr_tool = tool

def _snap_vertex_edge_clicked(self) -> None:
self.snap_vertex_edge = not self.snap_vertex_edge

def _vty_clicked(self, vty: VertexType) -> None:
self._curr_vty = vty

Expand Down Expand Up @@ -143,21 +149,73 @@ def delete_selection(self) -> None:
else UpdateGraph(self.graph_view,new_g)
self.undo_stack.push(cmd)

def add_vert(self, x: float, y: float) -> None:
def add_vert(self, x: float, y: float, edges: list[EItem]) -> None:
"""Add a vertex at point (x,y). `edges` is a list of EItems that are underneath the current position.
We will try to connect the vertex to an edge.
"""
cmd: BaseCommand
if self.snap_vertex_edge and edges and self._curr_vty != VertexType.W_OUTPUT:
# Trying to snap vertex to an edge
for it in edges:
e = it.e
g = self.graph_scene.g
if self.graph_scene.g.edge_type(e) not in (EdgeType.SIMPLE, EdgeType.HADAMARD):
continue
cmd = AddNodeSnapped(self.graph_view, x, y, self._curr_vty, e)
self.play_sound_signal.emit(SFXEnum.THATS_A_SPIDER)
self.undo_stack.push(cmd)
g = cmd.g
group = QParallelAnimationGroup()
for e in [next(g.edges(cmd.s, cmd.added_vert)), next(g.edges(cmd.t, cmd.added_vert))]:
eitem = self.graph_scene.edge_map[e][0]
anim = animations.edge_thickness(eitem,3,400,
QEasingCurve(QEasingCurve.Type.InCubic),start=7)
group.addAnimation(anim)
self.undo_stack.set_anim(group)
return

cmd = AddWNode(self.graph_view, x, y) if self._curr_vty == VertexType.W_OUTPUT \
else AddNode(self.graph_view, x, y, self._curr_vty)
else AddNode(self.graph_view, x, y, self._curr_vty)

self.play_sound_signal.emit(SFXEnum.THATS_A_SPIDER)
self.undo_stack.push(cmd)

def add_edge(self, u: VT, v: VT) -> None:
def add_edge(self, u: VT, v: VT, verts: list[VItem]) -> None:
"""Add an edge between vertices u and v. `verts` is a list of VItems that collide with the edge.
"""
cmd: BaseCommand
graph = self.graph_view.graph_scene.g
if vertex_is_w(graph.type(u)) and get_w_partner(graph, u) == v:
return None
if graph.type(u) == VertexType.W_INPUT and len(graph.neighbors(u)) >= 2 or \
graph.type(v) == VertexType.W_INPUT and len(graph.neighbors(v)) >= 2:
return None
cmd = AddEdge(self.graph_view, u, v, self._curr_ety)
# We will try to connect all the vertices together in order
# First we filter out the vertices that are not compatible with the edge.
verts = [vitem for vitem in verts if not graph.type(vitem.v) == VertexType.W_INPUT] # we will be adding two edges, which is not compatible with W_INPUT
# but first we check if there any vertices that we do want to additionally connect.
if not self.snap_vertex_edge or not verts:
cmd = AddEdge(self.graph_view, u, v, self._curr_ety)
self.undo_stack.push(cmd)
return

ux, uy = graph.row(u), graph.qubit(u)
# Line was drawn from u to v, we want to order vs with the earlier items first.
def dist(vitem: VItem) -> float:
return (graph.row(vitem.v) - ux)**2 + (graph.qubit(vitem.v) - uy)**2 # type: ignore
verts.sort(key=dist)
vs = [vitem.v for vitem in verts]
pairs = [(u, vs[0])]
for i in range(1, len(vs)):
pairs.append((vs[i-1],vs[i]))
pairs.append((vs[-1],v))
cmd = AddEdges(self.graph_view, pairs, self._curr_ety)
self.undo_stack.push(cmd)
group = QParallelAnimationGroup()
for vitem in verts:
anim = animations.scale(vitem,1.0,400,QEasingCurve(QEasingCurve.Type.InCubic),start=1.3)
group.addAnimation(anim)
self.undo_stack.set_anim(group)

def vert_moved(self, vs: list[tuple[VT, float, float]]) -> None:
self.undo_stack.push(MoveNode(self.graph_view, vs))
Expand Down Expand Up @@ -288,7 +346,7 @@ def _text_changed(self, name: str, text: str) -> None:
self.parent_panel.graph.variable_types[name] = True


def toolbar_select_node_edge(parent: EditorBasePanel) -> ToolbarSection:
def toolbar_select_node_edge(parent: EditorBasePanel) -> Iterator[ToolbarSection]:
icon_size = QSize(32, 32)
select = QToolButton(parent) # Selected by default
vertex = QToolButton(parent)
Expand All @@ -312,7 +370,16 @@ def toolbar_select_node_edge(parent: EditorBasePanel) -> ToolbarSection:
select.clicked.connect(lambda: parent._tool_clicked(ToolType.SELECT))
vertex.clicked.connect(lambda: parent._tool_clicked(ToolType.VERTEX))
edge.clicked.connect(lambda: parent._tool_clicked(ToolType.EDGE))
return ToolbarSection(select, vertex, edge, exclusive=True)
yield ToolbarSection(select, vertex, edge, exclusive=True)

snap = QToolButton(parent)
snap.setCheckable(True)
snap.setChecked(True)
snap.setIcon(QIcon(get_data("icons/vertex-snap-to-edge.svg")))
snap.setToolTip("Snap vertices to the edge beneath them when adding vertices or edges (f)")
snap.setShortcut("f")
snap.clicked.connect(lambda: parent._snap_vertex_edge_clicked())
yield ToolbarSection(snap)


def create_list_widget(parent: EditorBasePanel,
Expand Down
Loading

0 comments on commit 47f9271

Please sign in to comment.