From 4e56050358c2b995630ef069e6668f78778067e1 Mon Sep 17 00:00:00 2001 From: Arjan Molenaar Date: Sun, 10 Jan 2021 16:46:25 +0100 Subject: [PATCH] Move port selection into ConnectionSink --- gaphas/aspect/__init__.py | 2 +- gaphas/aspect/connector.py | 51 +++++++++++++++---- gaphas/aspect/handlemove.py | 97 +++++++++++++++---------------------- gaphas/tool/itemtool.py | 26 +--------- tests/test_canvas.py | 4 +- tests/test_tool_item.py | 2 +- tests/test_undo.py | 4 +- 7 files changed, 89 insertions(+), 97 deletions(-) diff --git a/gaphas/aspect/__init__.py b/gaphas/aspect/__init__.py index 8b0dfbc..5ccca51 100644 --- a/gaphas/aspect/__init__.py +++ b/gaphas/aspect/__init__.py @@ -4,5 +4,5 @@ Aspects form intermediate items between tools and items. """ from gaphas.aspect.connector import ConnectionSink, Connector -from gaphas.aspect.handlemove import HandleMove +from gaphas.aspect.handlemove import HandleMove, item_at_point from gaphas.aspect.move import Move diff --git a/gaphas/aspect/connector.py b/gaphas/aspect/connector.py index 490cfac..b8be5b5 100644 --- a/gaphas/aspect/connector.py +++ b/gaphas/aspect/connector.py @@ -1,18 +1,28 @@ from functools import singledispatch -from typing import Callable, Optional +from typing import Callable, Optional, Tuple from typing_extensions import Protocol from gaphas.connections import Connections from gaphas.connector import Handle, Port from gaphas.item import Item, matrix_i2i +from gaphas.solver import Constraint +from gaphas.types import Pos, SupportsFloatPos class ConnectionSinkType(Protocol): item: Item - port: Port + port: Optional[Port] - def __init__(self, item: Item, port: Port): + def __init__(self, item: Item, distance: float = 10): + ... + + def glue( + self, pos: SupportsFloatPos, secondary_pos: Optional[SupportsFloatPos] = None + ) -> Tuple[Optional[Pos], float]: + ... + + def constraint(self, item: Item, handle: Handle) -> Constraint: ... @@ -35,9 +45,10 @@ class ItemConnector: item = self.item matrix = matrix_i2i(item, sink.item) pos = matrix.transform_point(*handle.pos) - gluepos, dist = sink.port.glue(pos) - matrix.invert() - handle.pos = matrix.transform_point(*gluepos) + gluepos, dist = sink.glue(pos) + if gluepos: + matrix.invert() + handle.pos = matrix.transform_point(*gluepos) def connect(self, sink: ConnectionSinkType) -> None: """Connect the handle to a sink (item, port). @@ -74,7 +85,7 @@ class ItemConnector: handle = self.handle item = self.item - constraint = sink.port.constraint(item, handle, sink.item) + constraint = sink.constraint(item, handle) self.connections.connect_item( item, handle, sink.item, sink.port, constraint, callback=callback @@ -95,9 +106,31 @@ class ItemConnectionSink: connectable item or port. """ - def __init__(self, item: Item, port: Port): + def __init__(self, item: Item, distance: float = 10) -> None: self.item = item - self.port = port + self.distance = distance + self.port: Optional[Port] = None + + def glue( + self, pos: SupportsFloatPos, secondary_pos: Optional[SupportsFloatPos] = None + ) -> Tuple[Optional[Pos], float]: + max_dist = self.distance + glue_pos = None + for p in self.item.ports(): + if not p.connectable: + continue + + g, d = p.glue(pos) + + if d < max_dist: + max_dist = d + self.port = p + glue_pos = g + return glue_pos, max_dist if glue_pos else 1e4 + + def constraint(self, item: Item, handle: Handle) -> Constraint: + assert self.port, "constraint() can only be called after glue()" + return self.port.constraint(item, handle, self.item) ConnectionSink = singledispatch(ItemConnectionSink) diff --git a/gaphas/aspect/handlemove.py b/gaphas/aspect/handlemove.py index 7d6f682..612bbad 100644 --- a/gaphas/aspect/handlemove.py +++ b/gaphas/aspect/handlemove.py @@ -1,14 +1,17 @@ +import logging from functools import singledispatch -from typing import Optional, Sequence, Tuple, Union +from typing import Optional, Sequence from gi.repository import Gdk from gaphas.aspect.connector import ConnectionSink, ConnectionSinkType, Connector -from gaphas.connector import Handle, Port +from gaphas.connector import Handle from gaphas.item import Element, Item from gaphas.types import Pos from gaphas.view import GtkView +log = logging.getLogger(__name__) + class ItemHandleMove: """Move a handle (role is applied to the handle)""" @@ -65,17 +68,24 @@ class ItemHandleMove: if not handle.connectable: return None - connectable, port, glue_pos = port_at_point( - view, pos, distance=distance, exclude=(item,) - ) + connectable = item_at_point(view, pos, distance=distance, exclude=(item,)) + if not connectable: + return None - # check if item and found item can be connected on closest port - if connectable and port and glue_pos: + sink = ConnectionSink(connectable) + + ix, iy = view.get_matrix_v2i(connectable).transform_point(*pos) + iglue_pos, _ = sink.glue((ix, iy)) + if not iglue_pos: + return None + + glue_pos = view.get_matrix_i2v(connectable).transform_point(*iglue_pos) + + if glue_pos: model = self.view.model assert model connections = model.connections connector = Connector(self.item, self.handle, connections) - sink = ConnectionSink(connectable, port) if connector.allow(sink): # transform coordinates from view space to the item @@ -149,58 +159,31 @@ class ElementHandleMove(ItemHandleMove): self.view.get_window().set_cursor(self.cursor) -def port_at_point( +# Maybe make this an iterator? so extra checks can be done on the item +def item_at_point( view: GtkView, - vpos: Pos, - distance: float = 10, + pos: Pos, + distance: float = 0.5, exclude: Sequence[Item] = (), -) -> Union[Tuple[Item, Port, Tuple[float, float]], Tuple[None, None, None]]: - """Find item with port closest to specified position. +) -> Optional[Item]: + """Return the topmost item located at ``pos`` (x, y). - List of items to be ignored can be specified with `exclude` - parameter. - - Tuple is returned - - - found item - - closest, connectable port - - closest point on found port (in view coordinates) - - :Parameters: - vpos - Position specified in view coordinates. - distance - Max distance from point to a port (default 10) - exclude - Set of items to ignore. + Parameters: + - view: a view + - pos: Position, a tuple ``(x, y)`` in view coordinates + - selected: if False returns first non-selected item """ - v2i = view.get_matrix_v2i - vx, vy = vpos - - max_dist = distance - port = None - glue_pos = None - item = None - - rect = (vx - distance, vy - distance, distance * 2, distance * 2) - for i in reversed(list(view.get_items_in_rectangle(rect))): - if i in exclude: + item: Item + for item in reversed(list(view.get_items_in_rectangle((pos[0], pos[1], 1, 1)))): + if item in exclude: continue - for p in i.ports(): - if not p.connectable: - continue - ix, iy = v2i(i).transform_point(vx, vy) - pg, d = p.glue((ix, iy)) - - if d >= max_dist: - continue - - max_dist = d - item = i - port = p - - # transform coordinates from connectable item space to view space - i2v = view.get_matrix_i2v(i).transform_point - glue_pos = i2v(*pg) - return item, port, glue_pos # type: ignore[return-value] + v2i = view.get_matrix_v2i(item) + ix, iy = v2i.transform_point(*pos) + item_distance = item.point(ix, iy) + if item_distance is None: + log.warning("Item distance is None for %s", item) + continue + if item_distance < distance: + return item + return None diff --git a/gaphas/tool/itemtool.py b/gaphas/tool/itemtool.py index 739c537..808edac 100644 --- a/gaphas/tool/itemtool.py +++ b/gaphas/tool/itemtool.py @@ -4,7 +4,7 @@ from typing import Optional, Tuple, Union from gi.repository import Gdk, Gtk from typing_extensions import Protocol -from gaphas.aspect import HandleMove, Move +from gaphas.aspect import HandleMove, Move, item_at_point from gaphas.canvas import ancestors from gaphas.connector import Handle from gaphas.geometry import distance_point_point_fast @@ -116,30 +116,6 @@ def on_drag_end(gesture, offset_x, offset_y, drag_state): drag_state.moving = set() -def item_at_point(view: GtkView, pos: Pos, selected: bool = True) -> Optional[Item]: - """Return the topmost item located at ``pos`` (x, y). - - Parameters: - - view: a view - - pos: Position, a tuple ``(x, y)`` in view coordinates - - selected: if False returns first non-selected item - """ - item: Item - for item in reversed(list(view.get_items_in_rectangle((pos[0], pos[1], 1, 1)))): - if not selected and item in view.selection.selected_items: - continue # skip selected items - - v2i = view.get_matrix_v2i(item) - ix, iy = v2i.transform_point(*pos) - item_distance = item.point(ix, iy) - if item_distance is None: - log.warning("Item distance is None for %s", item) - continue - if item_distance < 0.5: - return item - return None - - def handle_at_point( view: GtkView, pos: Pos, distance: int = 6 ) -> Union[Tuple[Item, Handle], Tuple[None, None]]: diff --git a/tests/test_canvas.py b/tests/test_canvas.py index 3661def..cb6f8d2 100644 --- a/tests/test_canvas.py +++ b/tests/test_canvas.py @@ -177,14 +177,14 @@ def test_remove_connected_item(): number_cons2 = len(canvas.solver.constraints) conn = Connector(l1, l1.handles()[0], canvas.connections) - sink = ConnectionSink(b1, b1.ports()[0]) + sink = ConnectionSink(b1) conn.connect(sink) assert canvas.connections.get_connection(l1.handles()[0]) conn = Connector(l1, l1.handles()[1], canvas.connections) - sink = ConnectionSink(b2, b2.ports()[0]) + sink = ConnectionSink(b2) conn.connect(sink) diff --git a/tests/test_tool_item.py b/tests/test_tool_item.py index e41e5dd..4297e1e 100644 --- a/tests/test_tool_item.py +++ b/tests/test_tool_item.py @@ -78,7 +78,7 @@ def test_get_unselected_item_at_point(view, box): view.selection.select_items(box) assert item_at_point(view, (10, 10)) is box - assert item_at_point(view, (10, 10), selected=False) is None + assert item_at_point(view, (10, 10), exclude=(box,)) is None def test_get_handle_at_point(view, canvas, connections): diff --git a/tests/test_undo.py b/tests/test_undo.py index ff7f44e..e0e8e42 100644 --- a/tests/test_undo.py +++ b/tests/test_undo.py @@ -18,11 +18,11 @@ def test_undo_on_delete_element(revert_undo, undo_fixture): canvas.add(line) - sink = ConnectionSink(b1, b1.ports()[0]) + sink = ConnectionSink(b1) connector = Connector(line, line.handles()[0], canvas.connections) connector.connect(sink) - sink = ConnectionSink(b2, b2.ports()[0]) + sink = ConnectionSink(b2) connector = Connector(line, line.handles()[-1], canvas.connections) connector.connect(sink)