Browse code

Edge highlight along curve (#82)

- changed Edge to yield Jump everywhere along the curve
- changed Edge and Graph such that edges are highlighted only when directly connected to element under cursor
- fixed issue related to non-Bezier shapes in Edge
- replaced precomputation of points by distance to curve computation
- added requirement numpy to travis configuration
- added method get_smallest_distance to Shape
- moved Edge._get_distance to BezierShape.get_smallest_distance
- changed BezierShape.get_smallest_distance to include endpoints

notEvil authored on 17/08/2020 12:34:02 • GitHub committed on 17/08/2020 12:34:02
Showing 2 changed files

... ...
@@ -9,6 +9,7 @@ addons:
9 9
     - gir1.2-gtk-3.0
10 10
     - python3-gi
11 11
     - python3-gi-cairo
12
+    - python3-numpy
12 13
     - graphviz
13 14
     - xvfb
14 15
     - twine
... ...
@@ -26,6 +26,7 @@ from gi.repository import GdkPixbuf
26 26
 from gi.repository import Pango
27 27
 from gi.repository import PangoCairo
28 28
 import cairo
29
+import numpy
29 30
 
30 31
 _inf = float('inf')
31 32
 _get_bounding = operator.attrgetter('bounding')
... ...
@@ -68,6 +69,9 @@ class Shape:
68 69
     def search_text(self, regexp):
69 70
         return False
70 71
 
72
+    def get_smallest_distance(self, x, y):
73
+        return None
74
+
71 75
     @staticmethod
72 76
     def _bounds_from_points(points):
73 77
         x0, y0 = points[0]
... ...
@@ -408,6 +412,67 @@ class BezierShape(Shape):
408 412
             cr.set_source_rgba(*pen.color)
409 413
             cr.stroke()
410 414
 
415
+    def get_smallest_distance(self, x, y):
416
+        min_squared_distance = float('inf')
417
+
418
+        points_iter = iter(self.points)
419
+        x0, y0 = next(points_iter)
420
+
421
+        while True:
422
+            try:
423
+                x1, y1 = next(points_iter)
424
+
425
+            except StopIteration:
426
+                break
427
+
428
+            x2, y2 = next(points_iter)
429
+            x3, y3 = next(points_iter)
430
+
431
+            _e1 = -5
432
+            _e2 = (x0 - 3 * x1 + 3 * x2 - x3)
433
+            _e3 = (y0 - 3 * y1 + 3 * y2 - y3)
434
+            _e4 = 2 * x1
435
+            _e5 = 2 * y1
436
+            _e6 = x0**2
437
+            _e7 = y0**2
438
+            _e8 = -2
439
+            _e9 = (x0 - _e4 + x2)
440
+            _e10 = (y0 - _e5 + y2)
441
+            _e11 = 2 * x0
442
+            _e12 = 2 * y0
443
+            _e13 = 5 * _e6
444
+            _e14 = 5 * _e7
445
+            _e15 = x1**2
446
+            _e16 = y1**2
447
+            coefficients = [
448
+                (x - x0) * (x0 - x1) + (y - y0) * (y0 - y1),
449
+                _e13 + 3 * _e15 + _e11 * (_e1 * x1 + x2) - 2 * x * _e9 + _e14 + 3 * _e16 + _e12 * (_e1 * y1 + y2) - 2 * y * _e10,
450
+                -10 * _e6 + 9 * x1 * (_e8 * x1 + x2) + x * _e2 + x0 * (30 * x1 - 12 * x2 + x3) - 10 * _e7 + 9 * y1 * (_e8 * y1 + y2) + y * _e3 + y0 * (30 * y1 - 12 * y2 + y3),
451
+                2 * (_e13 + 18 * _e15 + 3 * x2**2 + _e4 * (-9 * x2 + x3) - _e11 * (10 * x1 - 6 * x2 + x3) + _e14 + 3 * (6 * _e16 - 6 * y1 * y2 + y2**2) + _e5 * y3 - _e12 * (10 * y1 - 6 * y2 + y3)),
452
+                _e1 * _e9 * _e2 - 5 * _e10 * _e3,
453
+                _e2**2 + _e3**2
454
+            ]
455
+            coefficients.reverse()
456
+
457
+            for t in numpy.roots(coefficients):
458
+                if 1e-6 < abs(t.imag):
459
+                    continue
460
+
461
+                t = t.real
462
+                if t < 0:
463
+                    t = 0
464
+                elif 1 < t:
465
+                    t = 1
466
+
467
+                squared_distance = ((1 - t)**3 * x0 + 3 * (1 - t)**2 * t * x1 + 3 * (1 - t) * t**2 * x2 + t**3 * x3 - x)**2 + ((1 - t)**3 * y0 + 3 * (1 - t)**2 * t * y1 + 3 * (1 - t) * t**2 * y2 + t**3 * y3 - y)**2
468
+                if squared_distance < min_squared_distance:
469
+                    min_squared_distance = squared_distance
470
+
471
+            x0 = x3
472
+            y0 = y3
473
+
474
+        return math.sqrt(min_squared_distance)
475
+
411 476
 
412 477
 class CompoundShape(Shape):
413 478
 
... ...
@@ -534,10 +599,19 @@ class Edge(Element):
534 599
         return False
535 600
 
536 601
     def get_jump(self, x, y):
537
-        if self.is_inside_begin(x, y):
538
-            return Jump(self, self.dst.x, self.dst.y, highlight=set([self, self.dst]))
539
-        if self.is_inside_end(x, y):
540
-            return Jump(self, self.src.x, self.src.y, highlight=set([self, self.src]))
602
+        for shape in self.shapes:
603
+            x1, y1, x2, y2 = shape.bounding
604
+            if (x1 - self.RADIUS) <= x and x <= (x2 + self.RADIUS) and (y1 - self.RADIUS) <= y and y <= (y2 + self.RADIUS):
605
+                break
606
+
607
+        else:
608
+            return None
609
+
610
+        for shape in self.shapes:
611
+            distance = shape.get_smallest_distance(x, y)
612
+            if distance is not None and distance <= self.RADIUS:
613
+                return Jump(self, self.src.x, self.src.y)
614
+
541 615
         return None
542 616
 
543 617
     def __repr__(self):
... ...
@@ -570,9 +644,17 @@ class Graph(Shape):
570 644
                 shape._draw(cr, highlight=False, bounding=bounding)
571 645
 
572 646
     def _draw_nodes(self, cr, bounding, highlight_items):
647
+        highlight_nodes = []
648
+        for element in highlight_items:
649
+            if isinstance(element, Edge):
650
+                highlight_nodes.append(element.src)
651
+                highlight_nodes.append(element.dst)
652
+            else:
653
+                highlight_nodes.append(element)
654
+
573 655
         for node in self.nodes:
574 656
             if bounding is None or node._intersects(bounding):
575
-                node._draw(cr, highlight=(node in highlight_items), bounding=bounding)
657
+                node._draw(cr, highlight=(node in highlight_nodes), bounding=bounding)
576 658
 
577 659
     def _draw_edges(self, cr, bounding, highlight_items):
578 660
         for edge in self.edges: