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

Add toggle to automatically connect new vertex to edge underneath it #337

Merged
merged 13 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
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
42 changes: 42 additions & 0 deletions zxlive/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,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
56 changes: 47 additions & 9 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,16 @@
from zxlive.sfx import SFXEnum

from .base_panel import BasePanel, ToolbarSection
from .commands import (AddEdge, AddNode, AddWNode, ChangeEdgeColor,
from .commands import (AddEdge, 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 .graphscene import EditGraphScene
from .settings import display_setting
from .vitem import BLACK
from . import animations


class ShapeType(Enum):
Expand Down Expand Up @@ -67,14 +68,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 +100,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 @@ -144,9 +149,33 @@ 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.
"""
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)

Expand Down Expand Up @@ -289,7 +318,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 @@ -313,7 +342,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.setText("Vertex-edge snap")
RazinShaikh marked this conversation as resolved.
Show resolved Hide resolved
snap.setToolTip("Snap newly added vertex to the edge beneath it (f)")
snap.setShortcut("f")
snap.clicked.connect(lambda: parent._snap_vertex_edge_clicked())
yield ToolbarSection(snap)


def create_list_widget(parent: EditorBasePanel,
Expand Down
87 changes: 84 additions & 3 deletions zxlive/eitem.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@

from __future__ import annotations
from math import sqrt
from typing import Optional, Any, TYPE_CHECKING
from typing import Optional, Any, TYPE_CHECKING, Union
from enum import Enum

from PySide6.QtCore import QPointF
from PySide6.QtCore import QPointF, QVariantAnimation, QAbstractAnimation
from PySide6.QtWidgets import QGraphicsEllipseItem, QGraphicsPathItem, QGraphicsItem, \
QGraphicsSceneMouseEvent, QStyleOptionGraphicsItem, QWidget, QStyle
from PySide6.QtGui import QPen, QPainter, QColor, QPainterPath
Expand All @@ -35,6 +36,13 @@
class EItem(QGraphicsPathItem):
"""A QGraphicsItem representing an edge"""

# Set of animations that are currently running on this vertex
active_animations: set[EItemAnimation]

class Properties(Enum):
"""Properties of an EItem that can be animated."""
Thickness = 1

def __init__(self, graph_scene: GraphScene, e: ET, s_item: VItem, t_item: VItem, curve_distance: float = 0) -> None:
super().__init__()
self.setZValue(EITEM_Z)
Expand All @@ -46,6 +54,7 @@ def __init__(self, graph_scene: GraphScene, e: ET, s_item: VItem, t_item: VItem,
self.s_item = s_item
self.t_item = t_item
self.curve_distance = curve_distance
self.active_animations = set()
s_item.adj_items.add(self)
t_item.adj_items.add(self)
self.selection_node = QGraphicsEllipseItem(-0.1 * SCALE, -0.1 * SCALE, 0.2 * SCALE, 0.2 * SCALE)
Expand All @@ -58,21 +67,26 @@ def __init__(self, graph_scene: GraphScene, e: ET, s_item: VItem, t_item: VItem,
self.is_mouse_pressed = False
self.is_dragging = False
self._old_pos: Optional[QPointF] = None
self.thickness: float = 3

self.refresh()

@property
def g(self) -> GraphT:
return self.graph_scene.g

@property
def is_animated(self) -> bool:
return len(self.active_animations) > 0

def refresh(self) -> None:
"""Call whenever source or target moves or edge data changes"""

self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable,
self.g.edge_type(self.e) != EdgeType.W_IO)
# set color/style according to edge type
pen = QPen()
pen.setWidthF(3)
pen.setWidthF(self.thickness)
if self.g.edge_type(self.e) == EdgeType.HADAMARD:
pen.setColor(QColor(HAD_EDGE_BLUE))
pen.setDashPattern([4.0, 2.0])
Expand Down Expand Up @@ -197,3 +211,70 @@ def compute_perpendicular_direction(source_pos: QPointF, target_pos: QPointF) ->
direction = direction / norm
perpendicular = QPointF(-direction.y(), direction.x())
return perpendicular


class EItemAnimation(QVariantAnimation):
RazinShaikh marked this conversation as resolved.
Show resolved Hide resolved
"""Animator for edge graphics items.

This animator lets the edge know that its being animated which stops any
interaction with the user. Furthermore, this animator
ensures that it's not garbage collected until the animation is finished, so there is
no need to hold onto a reference of this class."""

_it: Optional[EItem]
prop: EItem.Properties
refresh: bool # Whether the item is refreshed at each frame

e: Optional[ET]

def __init__(self, item: Union[EItem, ET], property: EItem.Properties,
scene: Optional[GraphScene] = None, refresh: bool = False) -> None:
super().__init__()
self.e = None
self._it = None
self.scene: Optional[GraphScene] = None
#if refresh and property != VItem.Properties.Position:
jvdwetering marked this conversation as resolved.
Show resolved Hide resolved
# raise ValueError("Only position animations require refresh")
if isinstance(item, EItem):
self._it = item
elif scene is None:
raise ValueError("Scene is required to obtain EItem from edge ET")
else:
self.e = item
self.scene = scene
self.prop = property
self.refresh = refresh
self.stateChanged.connect(self._on_state_changed)

@property
def it(self) -> EItem:
if self._it is None and self.scene is not None and self.e is not None:
self._it = self.scene.edge_map[self.e]
assert self._it is not None
return self._it

def _on_state_changed(self, state: QAbstractAnimation.State) -> None:
if state == QAbstractAnimation.State.Running and self not in self.it.active_animations:
# Stop all animations that target the same property
for anim in self.it.active_animations.copy():
if anim.prop == self.prop:
anim.stop()
self.it.active_animations.add(self)
elif state == QAbstractAnimation.State.Stopped:
self.it.active_animations.remove(self)
elif state == QAbstractAnimation.State.Paused:
# TODO: Once we use pausing, we should decide what to do here.
# Note that we cannot just remove ourselves from the set since the garbage
# collector will eat us in that case. We'll probably need something like
# `it.paused_animations`
pass

def updateCurrentValue(self, value: Any) -> None:
if self.state() != QAbstractAnimation.State.Running:
return

if self.prop == EItem.Properties.Thickness:
self.it.thickness = value

if self.refresh:
self.it.refresh()
8 changes: 6 additions & 2 deletions zxlive/graphscene.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ def update_graph(self, new: GraphT, select_new: bool = False) -> None:
e_item = self.edge_map[e][edge_idx]
if e_item.selection_node:
self.removeItem(e_item.selection_node)
for anim in e_item.active_animations.copy():
anim.stop()
self.removeItem(e_item)
self.edge_map[e].pop(edge_idx)
s, t = self.g.edge_st(e)
Expand Down Expand Up @@ -231,7 +233,7 @@ class EditGraphScene(GraphScene):
# Signals to handle addition of vertices and edges.
# Note that we have to set the argument types to `object`,
# otherwise it doesn't work for some reason...
vertex_added = Signal(object, object) # Actual types: float, float
vertex_added = Signal(object, object, object) # Actual types: float, float, list[EItem]
edge_added = Signal(object, object) # Actual types: VT, VT

# Currently selected edge type for preview when dragging
Expand Down Expand Up @@ -291,7 +293,9 @@ def mouseReleaseEvent(self, e: QGraphicsSceneMouseEvent) -> None:

def add_vertex(self, e: QGraphicsSceneMouseEvent) -> None:
p = e.scenePos()
self.vertex_added.emit(*pos_from_view(p.x(), p.y()))
# edges under current mouse position
edges: list[EItem] = [e for e in self.items(p, deviceTransform=QTransform()) if isinstance(e,EItem)]
RazinShaikh marked this conversation as resolved.
Show resolved Hide resolved
self.vertex_added.emit(*pos_from_view(p.x(), p.y()), edges)

def add_edge(self, e: QGraphicsSceneMouseEvent) -> None:
assert self._drag is not None
Expand Down
3 changes: 2 additions & 1 deletion zxlive/rewrite_action.py
jvdwetering marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def from_rewrite_data(cls, d: RewriteData) -> RewriteAction:
picture_path = 'custom'
elif 'picture' in d:
picture_path = d['picture']
else:
else:
picture_path = None
return cls(
name=d['text'],
Expand Down Expand Up @@ -107,6 +107,7 @@ def update_active(self, g: GraphT, verts: list[VT], edges: list[ET]) -> None:
)

@property

def tooltip(self) -> str:
if self.picture_path is None:
return self.tooltip_str
Expand Down
Loading