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