... | ... |
@@ -14,6 +14,7 @@ |
14 | 14 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
15 | 15 |
# |
16 | 16 |
import math |
17 |
+import operator |
|
17 | 18 |
|
18 | 19 |
import gi |
19 | 20 |
gi.require_version('Gtk', '3.0') |
... | ... |
@@ -26,17 +27,36 @@ from gi.repository import Pango |
26 | 27 |
from gi.repository import PangoCairo |
27 | 28 |
import cairo |
28 | 29 |
|
30 |
+_inf = float('inf') |
|
31 |
+_get_bounding = operator.attrgetter('bounding') |
|
32 |
+ |
|
29 | 33 |
|
30 | 34 |
class Shape: |
31 | 35 |
"""Abstract base class for all the drawing shapes.""" |
36 |
+ bounding = (-_inf, -_inf, _inf, _inf) |
|
32 | 37 |
|
33 | 38 |
def __init__(self): |
34 | 39 |
pass |
35 | 40 |
|
36 |
- def draw(self, cr, highlight=False): |
|
37 |
- """Draw this shape with the given cairo context""" |
|
41 |
+ def _intersects(self, bounding): |
|
42 |
+ x0, y0, x1, y1 = bounding |
|
43 |
+ x2, y2, x3, y3 = self.bounding |
|
44 |
+ return x2 <= x1 and x0 <= x3 and y2 <= y1 and y0 <= y3 |
|
45 |
+ |
|
46 |
+ def _fully_in(self, bounding): |
|
47 |
+ x0, y0, x1, y1 = bounding |
|
48 |
+ x2, y2, x3, y3 = self.bounding |
|
49 |
+ return x0 <= x2 and x3 <= x1 and y0 <= y2 and y3 <= y1 |
|
50 |
+ |
|
51 |
+ def _draw(self, cr, highlight, bounding): |
|
52 |
+ """Actual draw implementation""" |
|
38 | 53 |
raise NotImplementedError |
39 | 54 |
|
55 |
+ def draw(self, cr, highlight=False, bounding=None): |
|
56 |
+ """Draw this shape with the given cairo context""" |
|
57 |
+ if bounding is None or self._intersects(bounding): |
|
58 |
+ self._draw(cr, highlight, bounding) |
|
59 |
+ |
|
40 | 60 |
def select_pen(self, highlight): |
41 | 61 |
if highlight: |
42 | 62 |
if not hasattr(self, 'highlight_pen'): |
... | ... |
@@ -48,6 +68,26 @@ class Shape: |
48 | 68 |
def search_text(self, regexp): |
49 | 69 |
return False |
50 | 70 |
|
71 |
+ @staticmethod |
|
72 |
+ def _bounds_from_points(points): |
|
73 |
+ x0, y0 = points[0] |
|
74 |
+ x1, y1 = x0, y0 |
|
75 |
+ for i in range(1, len(points)): |
|
76 |
+ x, y = points[i] |
|
77 |
+ x0, x1 = min(x0, x), max(x1, x) |
|
78 |
+ y0, y1 = min(y0, y), max(y1, y) |
|
79 |
+ return x0, y0, x1, y1 |
|
80 |
+ |
|
81 |
+ @staticmethod |
|
82 |
+ def _envelope_bounds(*args): |
|
83 |
+ xa = ya = _inf |
|
84 |
+ xb = yb = -_inf |
|
85 |
+ for bounds in args: |
|
86 |
+ for x0, y0, x1, y1 in bounds: |
|
87 |
+ xa, xb = min(xa, x0), max(xb, x1) |
|
88 |
+ ya, yb = min(ya, y0), max(yb, y1) |
|
89 |
+ return xa, ya, xb, yb |
|
90 |
+ |
|
51 | 91 |
|
52 | 92 |
class TextShape(Shape): |
53 | 93 |
|
... | ... |
@@ -62,7 +102,7 @@ class TextShape(Shape): |
62 | 102 |
self.w = w # width |
63 | 103 |
self.t = t # text |
64 | 104 |
|
65 |
- def draw(self, cr, highlight=False): |
|
105 |
+ def _draw(self, cr, highlight, bounding): |
|
66 | 106 |
|
67 | 107 |
try: |
68 | 108 |
layout = self.layout |
... | ... |
@@ -139,34 +179,22 @@ class TextShape(Shape): |
139 | 179 |
else: |
140 | 180 |
f = 1.0 |
141 | 181 |
|
142 |
- if self.j == self.LEFT: |
|
143 |
- x = self.x |
|
144 |
- elif self.j == self.CENTER: |
|
145 |
- x = self.x - 0.5*width |
|
146 |
- elif self.j == self.RIGHT: |
|
147 |
- x = self.x - width |
|
148 |
- else: |
|
149 |
- assert 0 |
|
150 |
- |
|
151 | 182 |
y = self.y - height + descent |
152 | 183 |
|
153 |
- cr.move_to(x, y) |
|
184 |
+ if bounding is None or (y <= bounding[3] and bounding[1] <= y + height): |
|
185 |
+ x = self.x - 0.5 * (1 + self.j) * width |
|
186 |
+ cr.move_to(x, y) |
|
154 | 187 |
|
155 |
- cr.save() |
|
156 |
- cr.scale(f, f) |
|
157 |
- cr.set_source_rgba(*self.select_pen(highlight).color) |
|
158 |
- PangoCairo.show_layout(cr, layout) |
|
159 |
- cr.restore() |
|
188 |
+ cr.save() |
|
189 |
+ cr.scale(f, f) |
|
190 |
+ cr.set_source_rgba(*self.select_pen(highlight).color) |
|
191 |
+ PangoCairo.show_layout(cr, layout) |
|
192 |
+ cr.restore() |
|
160 | 193 |
|
161 | 194 |
if 0: # DEBUG |
162 | 195 |
# show where dot thinks the text should appear |
163 | 196 |
cr.set_source_rgba(1, 0, 0, .9) |
164 |
- if self.j == self.LEFT: |
|
165 |
- x = self.x |
|
166 |
- elif self.j == self.CENTER: |
|
167 |
- x = self.x - 0.5*self.w |
|
168 |
- elif self.j == self.RIGHT: |
|
169 |
- x = self.x - self.w |
|
197 |
+ x = self.x - 0.5 * (1 + self.j) * width |
|
170 | 198 |
cr.move_to(x, self.y) |
171 | 199 |
cr.line_to(x+self.w, self.y) |
172 | 200 |
cr.stroke() |
... | ... |
@@ -174,6 +202,11 @@ class TextShape(Shape): |
174 | 202 |
def search_text(self, regexp): |
175 | 203 |
return regexp.search(self.t) is not None |
176 | 204 |
|
205 |
+ @property |
|
206 |
+ def bounding(self): |
|
207 |
+ x, w, j = self.x, self.w, self.j |
|
208 |
+ return x - 0.5 * (1 + j) * w, -_inf, x + 0.5 * (1 - j) * w, _inf |
|
209 |
+ |
|
177 | 210 |
|
178 | 211 |
class ImageShape(Shape): |
179 | 212 |
|
... | ... |
@@ -186,7 +219,7 @@ class ImageShape(Shape): |
186 | 219 |
self.h = h |
187 | 220 |
self.path = path |
188 | 221 |
|
189 |
- def draw(self, cr, highlight=False): |
|
222 |
+ def _draw(self, cr, highlight, bounding): |
|
190 | 223 |
pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.path) |
191 | 224 |
sx = float(self.w)/float(pixbuf.get_width()) |
192 | 225 |
sy = float(self.h)/float(pixbuf.get_height()) |
... | ... |
@@ -197,6 +230,11 @@ class ImageShape(Shape): |
197 | 230 |
cr.paint() |
198 | 231 |
cr.restore() |
199 | 232 |
|
233 |
+ @property |
|
234 |
+ def bounding(self): |
|
235 |
+ x0, y0 = self.x0, self.y0 |
|
236 |
+ return x0, y0 - self.h, x0 + self.w, y0 |
|
237 |
+ |
|
200 | 238 |
|
201 | 239 |
class EllipseShape(Shape): |
202 | 240 |
|
... | ... |
@@ -209,7 +247,7 @@ class EllipseShape(Shape): |
209 | 247 |
self.h = h |
210 | 248 |
self.filled = filled |
211 | 249 |
|
212 |
- def draw(self, cr, highlight=False): |
|
250 |
+ def _draw(self, cr, highlight, bounding): |
|
213 | 251 |
cr.save() |
214 | 252 |
cr.translate(self.x0, self.y0) |
215 | 253 |
cr.scale(self.w, self.h) |
... | ... |
@@ -226,6 +264,13 @@ class EllipseShape(Shape): |
226 | 264 |
cr.set_source_rgba(*pen.color) |
227 | 265 |
cr.stroke() |
228 | 266 |
|
267 |
+ @property |
|
268 |
+ def bounding(self): |
|
269 |
+ x0, y0, w, h = self.x0, self.y0, self.w, self.h |
|
270 |
+ bt = 0 if self.filled else self.pen.linewidth / 2. |
|
271 |
+ w, h = w + bt, h + bt |
|
272 |
+ return x0 - w, y0 - h, x0 + w, y0 + h |
|
273 |
+ |
|
229 | 274 |
|
230 | 275 |
class PolygonShape(Shape): |
231 | 276 |
|
... | ... |
@@ -235,7 +280,11 @@ class PolygonShape(Shape): |
235 | 280 |
self.points = points |
236 | 281 |
self.filled = filled |
237 | 282 |
|
238 |
- def draw(self, cr, highlight=False): |
|
283 |
+ x0, y0, x1, y1 = Shape._bounds_from_points(self.points) |
|
284 |
+ bt = 0 if self.filled else self.pen.linewidth / 2. |
|
285 |
+ self.bounding = x0 - bt, y0 - bt, x1 + bt, y1 + bt |
|
286 |
+ |
|
287 |
+ def _draw(self, cr, highlight, bounding): |
|
239 | 288 |
x0, y0 = self.points[-1] |
240 | 289 |
cr.move_to(x0, y0) |
241 | 290 |
for x, y in self.points: |
... | ... |
@@ -260,7 +309,11 @@ class LineShape(Shape): |
260 | 309 |
self.pen = pen.copy() |
261 | 310 |
self.points = points |
262 | 311 |
|
263 |
- def draw(self, cr, highlight=False): |
|
312 |
+ x0, y0, x1, y1 = Shape._bounds_from_points(self.points) |
|
313 |
+ bt = self.pen.linewidth / 2. |
|
314 |
+ self.bounding = x0 - bt, y0 - bt, x1 + bt, y1 + bt |
|
315 |
+ |
|
316 |
+ def _draw(self, cr, highlight, bounding): |
|
264 | 317 |
x0, y0 = self.points[0] |
265 | 318 |
cr.move_to(x0, y0) |
266 | 319 |
for x1, y1 in self.points[1:]: |
... | ... |
@@ -280,13 +333,69 @@ class BezierShape(Shape): |
280 | 333 |
self.points = points |
281 | 334 |
self.filled = filled |
282 | 335 |
|
283 |
- def draw(self, cr, highlight=False): |
|
336 |
+ x0, y0 = self.points[0] |
|
337 |
+ xa = xb = x0 |
|
338 |
+ ya = yb = y0 |
|
339 |
+ for i in range(1, len(self.points), 3): |
|
340 |
+ (x1, y1), (x2, y2), (x3, y3) = self.points[i:i+3] |
|
341 |
+ for t in self._cubic_bernstein_extrema(x0, x1, x2, x3): |
|
342 |
+ if 0 < t < 1: # We're dealing only with Bezier curves |
|
343 |
+ v = self._cubic_bernstein(x0, x1, x2, x3, t) |
|
344 |
+ xa, xb = min(xa, v), max(xb, v) |
|
345 |
+ xa, xb = min(xa, x3), max(xb, x3) # t=0 / t=1 |
|
346 |
+ for t in self._cubic_bernstein_extrema(y0, y1, y2, y3): |
|
347 |
+ if 0 < t < 1: # We're dealing only with Bezier curves |
|
348 |
+ v = self._cubic_bernstein(y0, y1, y2, y3, t) |
|
349 |
+ ya, yb = min(ya, v), max(yb, v) |
|
350 |
+ ya, yb = min(ya, y3), max(yb, y3) # t=0 / t=1 |
|
351 |
+ x0, y0 = x3, y3 |
|
352 |
+ |
|
353 |
+ bt = 0 if self.filled else self.pen.linewidth / 2. |
|
354 |
+ self.bounding = xa - bt, ya - bt, xb + bt, yb + bt |
|
355 |
+ |
|
356 |
+ @staticmethod |
|
357 |
+ def _cubic_bernstein_extrema(p0, p1, p2, p3): |
|
358 |
+ """ |
|
359 |
+ Find extremas of a function of real domain defined by evaluating |
|
360 |
+ a cubic bernstein polynomial of given bernstein coefficients. |
|
361 |
+ """ |
|
362 |
+ # compute coefficients of derivative |
|
363 |
+ a = 3.*(p3-p0+3.*(p1-p2)) |
|
364 |
+ b = 6.*(p0+p2-2.*p1) |
|
365 |
+ c = 3.*(p1-p0) |
|
366 |
+ |
|
367 |
+ if a == 0: |
|
368 |
+ if b == 0: |
|
369 |
+ return () # constant |
|
370 |
+ return (-c / b,) # linear |
|
371 |
+ |
|
372 |
+ # quadratic |
|
373 |
+ # compute discriminant |
|
374 |
+ d = b*b - 4.*a*c |
|
375 |
+ if d < 0: |
|
376 |
+ return () |
|
377 |
+ |
|
378 |
+ k = -2. * a |
|
379 |
+ if d == 0: |
|
380 |
+ return (b / k,) |
|
381 |
+ |
|
382 |
+ r = math.sqrt(d) |
|
383 |
+ return ((b + r) / k, (b - r) / k) |
|
384 |
+ |
|
385 |
+ @staticmethod |
|
386 |
+ def _cubic_bernstein(p0, p1, p2, p3, t): |
|
387 |
+ """ |
|
388 |
+ Evaluate polynomial of given bernstein coefficients |
|
389 |
+ using de Casteljau's algorithm. |
|
390 |
+ """ |
|
391 |
+ u = 1 - t |
|
392 |
+ return p0*(u**3) + 3*t*u*(p1*u + p2*t) + p3*(t**3) |
|
393 |
+ |
|
394 |
+ def _draw(self, cr, highlight, bounding): |
|
284 | 395 |
x0, y0 = self.points[0] |
285 | 396 |
cr.move_to(x0, y0) |
286 | 397 |
for i in range(1, len(self.points), 3): |
287 |
- x1, y1 = self.points[i] |
|
288 |
- x2, y2 = self.points[i + 1] |
|
289 |
- x3, y3 = self.points[i + 2] |
|
398 |
+ (x1, y1), (x2, y2), (x3, y3) = self.points[i:i+3] |
|
290 | 399 |
cr.curve_to(x1, y1, x2, y2, x3, y3) |
291 | 400 |
pen = self.select_pen(highlight) |
292 | 401 |
if self.filled: |
... | ... |
@@ -305,10 +414,14 @@ class CompoundShape(Shape): |
305 | 414 |
def __init__(self, shapes): |
306 | 415 |
Shape.__init__(self) |
307 | 416 |
self.shapes = shapes |
417 |
+ self.bounding = Shape._envelope_bounds(map(_get_bounding, self.shapes)) |
|
308 | 418 |
|
309 |
- def draw(self, cr, highlight=False): |
|
419 |
+ def _draw(self, cr, highlight, bounding): |
|
420 |
+ if bounding is not None and self._fully_in(bounding): |
|
421 |
+ bounding = None |
|
310 | 422 |
for shape in self.shapes: |
311 |
- shape.draw(cr, highlight=highlight) |
|
423 |
+ if bounding is None or shape._intersects(bounding): |
|
424 |
+ shape._draw(cr, highlight, bounding) |
|
312 | 425 |
|
313 | 426 |
def search_text(self, regexp): |
314 | 427 |
for shape in self.shapes: |
... | ... |
@@ -440,10 +553,21 @@ class Graph(Shape): |
440 | 553 |
self.nodes = nodes |
441 | 554 |
self.edges = edges |
442 | 555 |
|
556 |
+ self.bounding = Shape._envelope_bounds( |
|
557 |
+ map(_get_bounding, self.shapes), |
|
558 |
+ map(_get_bounding, self.nodes), |
|
559 |
+ map(_get_bounding, self.edges)) |
|
560 |
+ |
|
443 | 561 |
def get_size(self): |
444 | 562 |
return self.width, self.height |
445 | 563 |
|
446 |
- def draw(self, cr, highlight_items=None): |
|
564 |
+ def draw(self, cr, highlight_items=None, bounding=None): |
|
565 |
+ if bounding is not None: |
|
566 |
+ if not self._intersects(bounding): |
|
567 |
+ return |
|
568 |
+ if self._fully_in(bounding): |
|
569 |
+ bounding = None |
|
570 |
+ |
|
447 | 571 |
if highlight_items is None: |
448 | 572 |
highlight_items = () |
449 | 573 |
cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) |
... | ... |
@@ -452,13 +576,16 @@ class Graph(Shape): |
452 | 576 |
cr.set_line_join(cairo.LINE_JOIN_MITER) |
453 | 577 |
|
454 | 578 |
for shape in self.shapes: |
455 |
- shape.draw(cr) |
|
579 |
+ if bounding is None or shape._intersects(bounding): |
|
580 |
+ shape._draw(cr, highlight=False, bounding=bounding) |
|
456 | 581 |
for edge in self.edges: |
457 |
- should_highlight = any(e in highlight_items |
|
458 |
- for e in (edge, edge.src, edge.dst)) |
|
459 |
- edge.draw(cr, highlight=should_highlight) |
|
582 |
+ if bounding is None or edge._intersects(bounding): |
|
583 |
+ should_highlight = any(e in highlight_items |
|
584 |
+ for e in (edge, edge.src, edge.dst)) |
|
585 |
+ edge._draw(cr, highlight=should_highlight, bounding=bounding) |
|
460 | 586 |
for node in self.nodes: |
461 |
- node.draw(cr, highlight=(node in highlight_items)) |
|
587 |
+ if bounding is None or node._intersects(bounding): |
|
588 |
+ node._draw(cr, highlight=(node in highlight_items), bounding=bounding) |
|
462 | 589 |
|
463 | 590 |
def get_element(self, x, y): |
464 | 591 |
for node in self.nodes: |
... | ... |
@@ -169,17 +169,26 @@ class DotWidget(Gtk.DrawingArea): |
169 | 169 |
self.reload() |
170 | 170 |
return True |
171 | 171 |
|
172 |
+ def _draw_graph(self, cr, rect): |
|
173 |
+ w, h = float(rect.width), float(rect.height) |
|
174 |
+ cx, cy = 0.5 * w, 0.5 * h |
|
175 |
+ x, y, ratio = self.x, self.y, self.zoom_ratio |
|
176 |
+ x0, y0 = x - cx / ratio, y - cy / ratio |
|
177 |
+ x1, y1 = x0 + w / ratio, y0 + h / ratio |
|
178 |
+ bounding = (x0, y0, x1, y1) |
|
179 |
+ |
|
180 |
+ cr.translate(cx, cy) |
|
181 |
+ cr.scale(ratio, ratio) |
|
182 |
+ cr.translate(-x, -y) |
|
183 |
+ self.graph.draw(cr, highlight_items=self.highlight, bounding=bounding) |
|
184 |
+ |
|
172 | 185 |
def on_draw(self, widget, cr): |
173 | 186 |
rect = self.get_allocation() |
174 | 187 |
Gtk.render_background(self.get_style_context(), cr, 0, 0, |
175 | 188 |
rect.width, rect.height) |
176 | 189 |
|
177 | 190 |
cr.save() |
178 |
- cr.translate(0.5*rect.width, 0.5*rect.height) |
|
179 |
- cr.scale(self.zoom_ratio, self.zoom_ratio) |
|
180 |
- cr.translate(-self.x, -self.y) |
|
181 |
- |
|
182 |
- self.graph.draw(cr, highlight_items=self.highlight) |
|
191 |
+ self._draw_graph(cr, rect) |
|
183 | 192 |
cr.restore() |
184 | 193 |
|
185 | 194 |
self.drag_action.draw(cr) |
... | ... |
@@ -342,13 +351,8 @@ class DotWidget(Gtk.DrawingArea): |
342 | 351 |
|
343 | 352 |
def draw_page(self, operation, context, page_nr): |
344 | 353 |
cr = context.get_cairo_context() |
345 |
- |
|
346 | 354 |
rect = self.get_allocation() |
347 |
- cr.translate(0.5*rect.width, 0.5*rect.height) |
|
348 |
- cr.scale(self.zoom_ratio, self.zoom_ratio) |
|
349 |
- cr.translate(-self.x, -self.y) |
|
350 |
- |
|
351 |
- self.graph.draw(cr, highlight_items=self.highlight) |
|
355 |
+ self._draw_graph(cr, rect) |
|
352 | 356 |
|
353 | 357 |
def get_drag_action(self, event): |
354 | 358 |
state = event.state |