Skip to content

Commit

Permalink
Merge pull request #413 from gaphor/negative-distance
Browse files Browse the repository at this point in the history
Negative distance
  • Loading branch information
amolenaar authored Mar 23, 2022
2 parents a11d22e + 04f2269 commit a20f60f
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 27 deletions.
21 changes: 16 additions & 5 deletions gaphas/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,26 +291,37 @@ def distance_point_point_fast(point1: Point, point2: Point = (0.0, 0.0)) -> floa
return abs(dx) + abs(dy)


def distance_rectangle_point(rect: Rect | Rectangle, point: Point) -> float:
def distance_rectangle_border_point(rect: Rect | Rectangle, point: Point) -> float:
"""Return the distance (fast) from a rectangle ``(x, y, width,height)`` to
a ``point``."""
dx = dy = 0.0
px, py = point
rx, ry, rw, rh = rect # typing: ignore[misc]
rx1 = rx + rw
ry1 = ry + rh

if rx < px < rx1 and ry < py < ry1:
return -min(px - rx, rx1 - px, py - ry, ry1 - py)

if px < rx:
dx = rx - px
elif px > rx + rw:
dx = px - (rx + rw)
elif px > rx1:
dx = px - rx1

if py < ry:
dy = ry - py
elif py > ry + rh:
dy = py - (ry + rh)
elif py > ry1:
dy = py - ry1

return abs(dx) + abs(dy)


def distance_rectangle_point(rect: Rect | Rectangle, point: Point) -> float:
"""Return the distance (fast) from a rectangle ``(x, y, width,height)`` to
a ``point``."""
return max(0, distance_rectangle_border_point(rect, point))


def point_on_rectangle(
rect: Rect | Rectangle, point: Point, border: bool = False
) -> Point:
Expand Down
57 changes: 42 additions & 15 deletions gaphas/handlemove.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
from functools import singledispatch
from typing import Iterable, Optional, Sequence
from operator import itemgetter
from typing import Iterable, Optional, Sequence, Tuple

from gi.repository import Gdk, Gtk

Expand Down Expand Up @@ -30,8 +31,7 @@ def start_move(self, pos: Pos) -> None:
self.last_x, self.last_y = pos
model = self.view.model
assert model
cinfo = model.connections.get_connection(self.handle)
if cinfo:
if cinfo := model.connections.get_connection(self.handle):
self.handle.glued = True
model.connections.solver.remove_constraint(cinfo.constraint)

Expand Down Expand Up @@ -101,11 +101,7 @@ def connect(self, pos: Pos) -> None:
connections = model.connections
connector = Connector(self.item, handle, connections)

# find connectable item and its port
sink = self.glue(pos)

# no new connectable item, then diconnect and exit
if sink:
if sink := self.glue(pos):
connector.connect(sink)
else:
cinfo = connections.get_connection(handle)
Expand Down Expand Up @@ -153,13 +149,12 @@ def reset_cursor(self) -> None:
) if Gtk.get_major_version() == 3 else self.view.set_cursor(self.cursor)


# Maybe make this an iterator? so extra checks can be done on the item
def item_at_point(
def item_distance(
view: GtkView,
pos: Pos,
distance: float = 0.5,
exclude: Sequence[Item] = (),
) -> Iterable[Item]:
) -> Iterable[Tuple[float, Item]]:
"""Return the topmost item located at ``pos`` (x, y).
Parameters:
Expand All @@ -176,9 +171,41 @@ def item_at_point(

v2i = view.get_matrix_v2i(item)
ix, iy = v2i.transform_point(vx, vy)
item_distance = item.point(ix, iy)
if item_distance is None:
d = item.point(ix, iy)
if d is None:
log.warning("Item distance is None for %s", item)
continue
if item_distance < distance:
yield item
if d < distance:
yield d, item


def order_items(distance_items, key=itemgetter(0)):
inside = []
outside = []
for e in distance_items:
if key(e) > 0:
outside.append(e)
else:
inside.append(e)

inside.sort(key=key, reverse=True)
outside.sort(key=key)
return inside + outside


def item_at_point(
view: GtkView,
pos: Pos,
distance: float = 0.5,
exclude: Sequence[Item] = (),
) -> Iterable[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
"""
return (
item for _d, item in order_items(item_distance(view, pos, distance, exclude))
)
10 changes: 5 additions & 5 deletions gaphas/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from cairo import Context as CairoContext

from gaphas.constraint import Constraint, EqualsConstraint, constraint
from gaphas.geometry import distance_line_point, distance_rectangle_point
from gaphas.geometry import distance_line_point, distance_rectangle_border_point
from gaphas.handle import Handle
from gaphas.matrix import Matrix
from gaphas.port import LinePort, Port
Expand Down Expand Up @@ -206,7 +206,7 @@ def point(self, x: float, y: float) -> float:
h = self._handles
x0, y0 = h[NW].pos
x1, y1 = h[SE].pos
return distance_rectangle_point((x0, y0, x1 - x0, y1 - y0), (x, y))
return distance_rectangle_border_point((x0, y0, x1 - x0, y1 - y0), (x, y))

def draw(self, context: DrawContext) -> None:
pass
Expand Down Expand Up @@ -363,10 +363,10 @@ def _update_ports(self) -> None:
used when initializing the line.
"""
assert len(self._handles) >= 2, "Not enough segments"
self._ports = []
handles = self._handles
for h1, h2 in zip(handles[:-1], handles[1:]):
self._ports.append(LinePort(h1.pos, h2.pos))
self._ports = [
LinePort(h1.pos, h2.pos) for h1, h2 in zip(handles[:-1], handles[1:])
]

def opposite(self, handle: Handle) -> Handle:
"""Given the handle of one end of the line, return the other end."""
Expand Down
4 changes: 2 additions & 2 deletions tests/test_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ def test_resize_by_dragging_se_handle(canvas, box, count):
def test_point(box):
box.handles()[SE].pos = (100, 100)

assert box.point(50, 50) == 0
assert box.point(50, 50) == -50


def test_point_with_moved_nw_handle(box):
box.handles()[NW].pos = (-100, -100)

assert box.point(-50, -50) == 0
assert box.point(-50, -50) == -50


def test_point_outside_box(box):
Expand Down
9 changes: 9 additions & 0 deletions tests/test_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from gaphas.geometry import (
Rectangle,
distance_line_point,
distance_rectangle_border_point,
distance_rectangle_point,
intersect_line_line,
point_on_rectangle,
Expand Down Expand Up @@ -72,6 +73,14 @@ def test_distance_with_negative_numbers_in_rectangle():
assert distance_rectangle_point((-50, -100, 100, 100), (-17, -65)) == 0


def test_distance_rectangle_border_point():
assert distance_rectangle_border_point((2, 0, 2, 2), (0, 0)) == 2
assert distance_rectangle_border_point(Rectangle(0, 0, 10, 10), (11, -1)) == 2
assert distance_rectangle_border_point((0, 0, 10, 10), (11, -1)) == 2
assert distance_rectangle_border_point((0, 0, 10, 10), (3, 4)) == -3
assert distance_rectangle_border_point((0, 0, 2, 2), (1, 1)) == -1


def test_point_on_rectangle():
assert point_on_rectangle((2, 2, 2, 2), (0, 0)) == (2, 2)
assert point_on_rectangle((2, 2, 2, 2), (3, 0)) == (3, 2)
Expand Down
25 changes: 25 additions & 0 deletions tests/test_tool_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from gi.repository import Gtk

from gaphas.handlemove import order_items
from gaphas.tool.itemtool import (
DragState,
handle_at_point,
Expand Down Expand Up @@ -85,6 +86,30 @@ def test_get_unselected_item_at_point(view, box):
assert next(item_at_point(view, (10, 10), exclude=(box,)), None) is None # type: ignore[call-overload]


def test_get_item_at_point_overlayed_by_bigger_item(view, canvas, connections):
"""Hover tool only reacts on motion-notify events."""
below = Box(connections)
canvas.add(below)
above = Box(connections)
canvas.add(above)

below.width = 20
below.height = 20
above.matrix.translate(-40, -40)
above.width = 100
above.height = 100
view.request_update((below, above))

assert next(item_at_point(view, (10, 10)), None) is below # type: ignore[call-overload]
assert next(item_at_point(view, (-1, -1)), None) is above # type: ignore[call-overload]


def test_order_by_distance():
m = [(0, ""), (10, ""), (-1, ""), (-3, ""), (5, ""), (4, "")]

assert [e[0] for e in order_items(m)] == [0, -1, -3, 4, 5, 10]


def test_get_handle_at_point(view, canvas, connections):
box = Box(connections)
box.min_width = 20
Expand Down

0 comments on commit a20f60f

Please sign in to comment.