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.handlemove import HandleMove
from gaphas.aspect.handlemove import HandleMove, item_at_point
from gaphas.aspect.move import Move

View File

@ -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)

View File

@ -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

View File

@ -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]]:

View File

@ -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)

View File

@ -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):

View File

@ -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)