Move port selection into ConnectionSink
This commit is contained in:
parent
ff5ea4ffc3
commit
4e56050358
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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]
|
|
||||||
|
@ -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]]:
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user