Move port selection into ConnectionSink

This commit is contained in:
Arjan Molenaar 2021-01-10 16:46:25 +01:00
parent ff5ea4ffc3
commit 4e56050358
No known key found for this signature in database
GPG Key ID: BF977B918996CB13
7 changed files with 89 additions and 97 deletions

View File

@ -4,5 +4,5 @@ Aspects form intermediate items between tools and items.
""" """
from gaphas.aspect.connector import ConnectionSink, Connector 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 from gaphas.aspect.move import Move

View File

@ -1,18 +1,28 @@
from functools import singledispatch from functools import singledispatch
from typing import Callable, Optional from typing import Callable, Optional, Tuple
from typing_extensions import Protocol from typing_extensions import Protocol
from gaphas.connections import Connections from gaphas.connections import Connections
from gaphas.connector import Handle, Port from gaphas.connector import Handle, Port
from gaphas.item import Item, matrix_i2i from gaphas.item import Item, matrix_i2i
from gaphas.solver import Constraint
from gaphas.types import Pos, SupportsFloatPos
class ConnectionSinkType(Protocol): class ConnectionSinkType(Protocol):
item: Item 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 item = self.item
matrix = matrix_i2i(item, sink.item) matrix = matrix_i2i(item, sink.item)
pos = matrix.transform_point(*handle.pos) pos = matrix.transform_point(*handle.pos)
gluepos, dist = sink.port.glue(pos) gluepos, dist = sink.glue(pos)
matrix.invert() if gluepos:
handle.pos = matrix.transform_point(*gluepos) matrix.invert()
handle.pos = matrix.transform_point(*gluepos)
def connect(self, sink: ConnectionSinkType) -> None: def connect(self, sink: ConnectionSinkType) -> None:
"""Connect the handle to a sink (item, port). """Connect the handle to a sink (item, port).
@ -74,7 +85,7 @@ class ItemConnector:
handle = self.handle handle = self.handle
item = self.item item = self.item
constraint = sink.port.constraint(item, handle, sink.item) constraint = sink.constraint(item, handle)
self.connections.connect_item( self.connections.connect_item(
item, handle, sink.item, sink.port, constraint, callback=callback item, handle, sink.item, sink.port, constraint, callback=callback
@ -95,9 +106,31 @@ class ItemConnectionSink:
connectable item or port. connectable item or port.
""" """
def __init__(self, item: Item, port: Port): def __init__(self, item: Item, distance: float = 10) -> None:
self.item = item 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) ConnectionSink = singledispatch(ItemConnectionSink)

View File

@ -1,14 +1,17 @@
import logging
from functools import singledispatch from functools import singledispatch
from typing import Optional, Sequence, Tuple, Union from typing import Optional, Sequence
from gi.repository import Gdk from gi.repository import Gdk
from gaphas.aspect.connector import ConnectionSink, ConnectionSinkType, Connector 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.item import Element, Item
from gaphas.types import Pos from gaphas.types import Pos
from gaphas.view import GtkView from gaphas.view import GtkView
log = logging.getLogger(__name__)
class ItemHandleMove: class ItemHandleMove:
"""Move a handle (role is applied to the handle)""" """Move a handle (role is applied to the handle)"""
@ -65,17 +68,24 @@ class ItemHandleMove:
if not handle.connectable: if not handle.connectable:
return None return None
connectable, port, glue_pos = port_at_point( connectable = item_at_point(view, pos, distance=distance, exclude=(item,))
view, pos, distance=distance, exclude=(item,) if not connectable:
) return None
# check if item and found item can be connected on closest port sink = ConnectionSink(connectable)
if connectable and port and glue_pos:
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 model = self.view.model
assert model assert model
connections = model.connections connections = model.connections
connector = Connector(self.item, self.handle, connections) connector = Connector(self.item, self.handle, connections)
sink = ConnectionSink(connectable, port)
if connector.allow(sink): if connector.allow(sink):
# transform coordinates from view space to the item # transform coordinates from view space to the item
@ -149,58 +159,31 @@ class ElementHandleMove(ItemHandleMove):
self.view.get_window().set_cursor(self.cursor) 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, view: GtkView,
vpos: Pos, pos: Pos,
distance: float = 10, distance: float = 0.5,
exclude: Sequence[Item] = (), exclude: Sequence[Item] = (),
) -> Union[Tuple[Item, Port, Tuple[float, float]], Tuple[None, None, None]]: ) -> Optional[Item]:
"""Find item with port closest to specified position. """Return the topmost item located at ``pos`` (x, y).
List of items to be ignored can be specified with `exclude` Parameters:
parameter. - view: a view
- pos: Position, a tuple ``(x, y)`` in view coordinates
Tuple is returned - selected: if False returns first non-selected item
- 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.
""" """
v2i = view.get_matrix_v2i item: Item
vx, vy = vpos for item in reversed(list(view.get_items_in_rectangle((pos[0], pos[1], 1, 1)))):
if item in exclude:
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:
continue continue
for p in i.ports():
if not p.connectable:
continue
ix, iy = v2i(i).transform_point(vx, vy) v2i = view.get_matrix_v2i(item)
pg, d = p.glue((ix, iy)) ix, iy = v2i.transform_point(*pos)
item_distance = item.point(ix, iy)
if d >= max_dist: if item_distance is None:
continue log.warning("Item distance is None for %s", item)
continue
max_dist = d if item_distance < distance:
item = i return item
port = p return None
# 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]

View File

@ -4,7 +4,7 @@ from typing import Optional, Tuple, Union
from gi.repository import Gdk, Gtk from gi.repository import Gdk, Gtk
from typing_extensions import Protocol 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.canvas import ancestors
from gaphas.connector import Handle from gaphas.connector import Handle
from gaphas.geometry import distance_point_point_fast 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() 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( def handle_at_point(
view: GtkView, pos: Pos, distance: int = 6 view: GtkView, pos: Pos, distance: int = 6
) -> Union[Tuple[Item, Handle], Tuple[None, None]]: ) -> Union[Tuple[Item, Handle], Tuple[None, None]]:

View File

@ -177,14 +177,14 @@ def test_remove_connected_item():
number_cons2 = len(canvas.solver.constraints) number_cons2 = len(canvas.solver.constraints)
conn = Connector(l1, l1.handles()[0], canvas.connections) conn = Connector(l1, l1.handles()[0], canvas.connections)
sink = ConnectionSink(b1, b1.ports()[0]) sink = ConnectionSink(b1)
conn.connect(sink) conn.connect(sink)
assert canvas.connections.get_connection(l1.handles()[0]) assert canvas.connections.get_connection(l1.handles()[0])
conn = Connector(l1, l1.handles()[1], canvas.connections) conn = Connector(l1, l1.handles()[1], canvas.connections)
sink = ConnectionSink(b2, b2.ports()[0]) sink = ConnectionSink(b2)
conn.connect(sink) conn.connect(sink)

View File

@ -78,7 +78,7 @@ def test_get_unselected_item_at_point(view, box):
view.selection.select_items(box) view.selection.select_items(box)
assert item_at_point(view, (10, 10)) is 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): def test_get_handle_at_point(view, canvas, connections):

View File

@ -18,11 +18,11 @@ def test_undo_on_delete_element(revert_undo, undo_fixture):
canvas.add(line) canvas.add(line)
sink = ConnectionSink(b1, b1.ports()[0]) sink = ConnectionSink(b1)
connector = Connector(line, line.handles()[0], canvas.connections) connector = Connector(line, line.handles()[0], canvas.connections)
connector.connect(sink) connector.connect(sink)
sink = ConnectionSink(b2, b2.ports()[0]) sink = ConnectionSink(b2)
connector = Connector(line, line.handles()[-1], canvas.connections) connector = Connector(line, line.handles()[-1], canvas.connections)
connector.connect(sink) connector.connect(sink)