2
A custom graphic renderer for the '.plain' files produced by dot.
6
from __future__ import generators
10
from pygame.locals import *
13
FONT = os.path.join(autopath.this_dir, 'cyrvetic.ttf')
14
FIXEDFONT = os.path.join(autopath.this_dir, 'VeraMoBd.ttf')
17
'white': (255,255,255),
21
'yellow': (255,255,0),
23
re_nonword=re.compile(r'([^0-9a-zA-Z_.]+)')
25
def combine(color1, color2, alpha):
29
return (int(r1 * alpha + r2 * beta),
30
int(g1 * alpha + g2 * beta),
31
int(b1 * alpha + b2 * beta))
34
def highlight_color(color):
35
if color == (0, 0, 0): # black becomes magenta
37
elif color == (255, 255, 255): # white becomes yellow
39
intensity = sum(color)
40
if intensity > 191 * 3:
41
return combine(color, (128, 192, 0), 0.2)
43
return combine(color, (255, 255, 0), 0.2)
45
def getcolor(name, default):
48
elif name.startswith('#') and len(name) == 7:
49
rval = COLOR[name] = (int(name[1:3],16), int(name[3:5],16), int(name[5:7],16))
58
def __init__(self, filename):
59
# parse the layout file (.plain format)
60
lines = open(str(filename), 'r').readlines()
61
for i in range(len(lines)-2, -1, -1):
62
if lines[i].endswith('\\\n'): # line ending in '\'
63
lines[i] = lines[i][:-2] + lines[i+1]
65
header = splitline(lines.pop(0))
66
# XXX very simple-minded way to give a somewhat better error message
67
if header[0] == '<body':
68
raise Exception("the dot on codespeak has very likely crashed")
69
assert header[0] == 'graph'
70
self.scale = float(header[1])
71
self.boundingbox = float(header[2]), float(header[3])
75
line = splitline(line)
78
self.nodes[n.name] = n
80
self.edges.append(Edge(self.nodes, *line[1:]))
85
def get_display(self):
86
from pypy.translator.tool.pygame.graphdisplay import GraphDisplay
87
return GraphDisplay(self)
90
self.get_display().run()
95
# async interaction helpers
97
def display_async_quit():
98
pygame.event.post(pygame.event.Event(QUIT))
100
def display_async_cmd(**kwds):
101
pygame.event.post(pygame.event.Event(USEREVENT, **kwds))
105
def wait_for_events():
107
EventQueue.append(pygame.event.wait())
108
EventQueue.extend(pygame.event.get())
110
def wait_for_async_cmd():
111
# wait until another thread pushes a USEREVENT in the queue
114
e = EventQueue.pop(0)
115
if e.type in (USEREVENT, QUIT): # discard all other events
117
EventQueue.insert(0, e) # re-insert the event for further processing
121
def __init__(self, name, x, y, w, h, label, style, shape, color, fillcolor):
131
self.fillcolor = fillcolor
132
self.highlight = False
134
def sethighlight(self, which):
135
self.highlight = bool(which)
140
def __init__(self, nodes, tail, head, cnt, *rest):
141
self.tail = nodes[tail]
142
self.head = nodes[head]
144
self.points = [(float(rest[i]), float(rest[i+1]))
145
for i in range(0, cnt*2, 2)]
148
self.label, xl, yl = rest[:3]
152
self.style, self.color = rest
153
self.highlight = False
154
self.cachedbezierpoints = None
155
self.cachedarrowhead = None
156
self.cachedlimits = None
158
def sethighlight(self, which):
159
self.highlight = bool(which)
162
result = self.cachedlimits
164
points = self.bezierpoints()
165
xs = [point[0] for point in points]
166
ys = [point[1] for point in points]
167
self.cachedlimits = result = (min(xs), max(ys), max(xs), min(ys))
170
def bezierpoints(self):
171
result = self.cachedbezierpoints
175
for i in range(0, len(pts)-3, 3):
176
result += beziercurve(pts[i], pts[i+1], pts[i+2], pts[i+3])
177
self.cachedbezierpoints = result
181
result = self.cachedarrowhead
183
bottom_up = self.points[0][1] > self.points[-1][1]
184
if (self.tail.y > self.head.y) != bottom_up: # reversed edge
193
x0, y0 = self.points[head]
194
x1, y1 = self.points[head+n*dir]
201
f = 0.12 / math.sqrt(vx*vx + vy*vy)
204
result = [(x0 + 0.9*vx, y0 + 0.9*vy),
205
(x0 + 0.4*vy, y0 - 0.4*vx),
206
(x0 - 0.4*vy, y0 + 0.4*vx)]
208
except (ZeroDivisionError, ValueError):
210
self.cachedarrowhead = result
213
def beziercurve((x0,y0), (x1,y1), (x2,y2), (x3,y3), resolution=8):
215
f = 1.0/(resolution-1)
216
append = result.append
217
for i in range(resolution):
219
t0 = (1-t)*(1-t)*(1-t)
220
t1 = t *(1-t)*(1-t) * 3.0
221
t2 = t * t *(1-t) * 3.0
223
append((x0*t0 + x1*t1 + x2*t2 + x3*t3,
224
y0*t0 + y1*t1 + y2*t2 + y3*t3))
227
def segmentdistance((x0,y0), (x1,y1), (x,y)):
228
"Distance between the point (x,y) and the segment (x0,y0)-(x1,y1)."
232
l = math.hypot(vx, vy)
235
dlong = vx*(x-x0) + vy*(y-y0)
236
except (ZeroDivisionError, ValueError):
239
return math.hypot(x-x0, y-y0)
241
return math.hypot(x-x1, y-y1)
243
return abs(vy*(x-x0) - vx*(y-y0))
245
def splitline(line, re_word = re.compile(r'[^\s"]\S*|["]["]|["].*?[^\\]["]')):
247
for word in re_word.findall(line):
248
if word.startswith('"'):
260
def __init__(self, screen, graphlayout, scale=75):
261
self.graphlayout = graphlayout
266
self.highlightwords = graphlayout.links
267
self.highlight_word = None
268
self.visiblenodes = []
269
self.visibleedges = []
271
def wordcolor(self, word):
272
info = self.highlightwords[word]
273
if isinstance(info, tuple) and len(info) >= 2:
279
if word == self.highlight_word:
280
return ((255,255,80), color)
284
def setscale(self, scale):
285
scale = max(min(scale, self.SCALEMAX), self.SCALEMIN)
286
self.scale = float(scale)
287
w, h = self.graphlayout.boundingbox
288
self.margin = int(self.MARGIN * scale)
289
self.width = int(w * scale) + (2 * self.margin)
290
self.height = int(h * scale) + (2 * self.margin)
292
size = int(15 * (scale-10) / 75)
293
self.font = self.getfont(size)
295
def getfont(self, size):
296
if size in self.FONTCACHE:
297
return self.FONTCACHE[size]
299
self.FONTCACHE[size] = None
302
if self.graphlayout.fixedfont:
306
font = self.FONTCACHE[size] = pygame.font.Font(filename, size)
309
def setoffset(self, offsetx, offsety):
310
"Set the (x,y) origin of the rectangle where the graph will be rendered."
311
self.ofsx = offsetx - self.margin
312
self.ofsy = offsety - self.margin
314
def shiftoffset(self, dx, dy):
319
w, h = self.screen.get_size()
320
return self.revmap(w//2, h//2)
322
def setcenter(self, x, y):
323
w, h = self.screen.get_size()
324
x, y = self.map(x, y)
325
self.shiftoffset(x-w//2, y-h//2)
327
def shiftscale(self, factor, fix=None):
329
fixx, fixy = self.screen.get_size()
334
x, y = self.revmap(fixx, fixy)
335
self.setscale(self.scale * factor)
336
newx, newy = self.map(x, y)
337
self.shiftoffset(newx - fixx, newy - fixy)
339
def reoffset(self, swidth, sheight):
340
offsetx = noffsetx = self.ofsx
341
offsety = noffsety = self.ofsy
345
# if it fits, center it, otherwise clamp
347
noffsetx = (width - swidth) // 2
349
noffsetx = min(max(0, offsetx), width - swidth)
351
if height <= sheight:
352
noffsety = (height - sheight) // 2
354
noffsety = min(max(0, offsety), height - sheight)
359
def getboundingbox(self):
360
"Get the rectangle where the graph will be rendered."
361
return (-self.ofsx, -self.ofsy, self.width, self.height)
363
def visible(self, x1, y1, x2, y2):
364
"""Is any part of the box visible (i.e. within the bounding box)?
366
We have to perform clipping ourselves because with big graphs the
367
coordinates may sometimes become longs and cause OverflowErrors
370
w, h = self.screen.get_size()
371
return x1 < w and x2 > 0 and y1 < h and y2 > 0
373
def computevisible(self):
374
del self.visiblenodes[:]
375
del self.visibleedges[:]
376
w, h = self.screen.get_size()
377
for node in self.graphlayout.nodes.values():
378
x, y = self.map(node.x, node.y)
379
nw2 = int(node.w * self.scale)//2
380
nh2 = int(node.h * self.scale)//2
381
if x-nw2 < w and x+nw2 > 0 and y-nh2 < h and y+nh2 > 0:
382
self.visiblenodes.append(node)
383
for edge in self.graphlayout.edges:
384
x1, y1, x2, y2 = edge.limits()
385
x1, y1 = self.map(x1, y1)
386
if x1 < w and y1 < h:
387
x2, y2 = self.map(x2, y2)
388
if x2 > 0 and y2 > 0:
389
self.visibleedges.append(edge)
392
return (int(x*self.scale) - (self.ofsx - self.margin),
393
int((self.bboxh-y)*self.scale) - (self.ofsy - self.margin))
395
def revmap(self, px, py):
396
return ((px + (self.ofsx - self.margin)) / self.scale,
397
self.bboxh - (py + (self.ofsy - self.margin)) / self.scale)
399
def draw_node_commands(self, node):
400
xcenter, ycenter = self.map(node.x, node.y)
401
boxwidth = int(node.w * self.scale)
402
boxheight = int(node.h * self.scale)
403
fgcolor = getcolor(node.color, (0,0,0))
404
bgcolor = getcolor(node.fillcolor, (255,255,255))
406
fgcolor = highlight_color(fgcolor)
407
bgcolor = highlight_color(bgcolor)
410
lines = text.replace('\\l','\\l\n').replace('\r','\r\n').split('\n')
411
# ignore a final newline
419
if self.font is None:
421
raw_line = lines[0].replace('\\l','').replace('\r','')
423
for size in (12, 10, 8, 6, 4):
424
font = self.getfont(size)
425
img = TextSnippet(self, raw_line, (0, 0, 0), bgcolor, font=font)
426
w, h = img.get_size()
427
if (w >= boxwidth or h >= boxheight):
431
def cmd(img=img, y=hmax, w=w):
432
img.draw(xcenter-w//2, ytop+y)
438
raw_line = line.replace('\\l','').replace('\r','') or ' '
439
img = TextSnippet(self, raw_line, (0, 0, 0), bgcolor)
440
w, h = img.get_size()
443
if line.endswith('\\l'):
444
def cmd(img=img, y=hmax):
445
img.draw(xleft, ytop+y)
446
elif line.endswith('\r'):
447
def cmd(img=img, y=hmax, w=w):
448
img.draw(xright-w, ytop+y)
450
def cmd(img=img, y=hmax, w=w):
451
img.draw(xcenter-w//2, ytop+y)
456
# we know the bounding box only now; setting these variables will
457
# have an effect on the values seen inside the cmd() functions above
458
xleft = xcenter - wmax//2
459
xright = xcenter + wmax//2
460
ytop = ycenter - hmax//2
461
x = xcenter-boxwidth//2
462
y = ycenter-boxheight//2
464
if node.shape == 'box':
465
rect = (x-1, y-1, boxwidth+2, boxheight+2)
467
self.screen.fill(bgcolor, rect)
468
bkgndcommands.append(cmd)
470
pygame.draw.rect(self.screen, fgcolor, rect, 1)
472
elif node.shape == 'ellipse':
473
rect = (x-1, y-1, boxwidth+2, boxheight+2)
475
pygame.draw.ellipse(self.screen, bgcolor, rect, 0)
476
bkgndcommands.append(cmd)
478
pygame.draw.ellipse(self.screen, fgcolor, rect, 1)
480
elif node.shape == 'octagon':
481
step = 1-math.sqrt(2)/2
482
points = [(int(x+boxwidth*fx), int(y+boxheight*fy))
483
for fx, fy in [(step,0), (1-step,0),
484
(1,step), (1,1-step),
485
(1-step,1), (step,1),
486
(0,1-step), (0,step)]]
488
pygame.draw.polygon(self.screen, bgcolor, points, 0)
489
bkgndcommands.append(cmd)
491
pygame.draw.polygon(self.screen, fgcolor, points, 1)
493
return bkgndcommands, commands
495
def draw_commands(self):
498
for node in self.visiblenodes:
499
cmd1, cmd2 = self.draw_node_commands(node)
505
for edge in self.visibleedges:
507
fgcolor = getcolor(edge.color, (0,0,0))
509
fgcolor = highlight_color(fgcolor)
510
points = [self.map(*xy) for xy in edge.bezierpoints()]
512
def drawedgebody(points=points, fgcolor=fgcolor):
513
pygame.draw.lines(self.screen, fgcolor, False, points)
514
edgebodycmd.append(drawedgebody)
516
points = [self.map(*xy) for xy in edge.arrowhead()]
518
def drawedgehead(points=points, fgcolor=fgcolor):
519
pygame.draw.polygon(self.screen, fgcolor, points, 0)
520
edgeheadcmd.append(drawedgehead)
523
x, y = self.map(edge.xl, edge.yl)
524
img = TextSnippet(self, edge.label, (0, 0, 0))
525
w, h = img.get_size()
526
if self.visible(x-w//2, y-h//2, x+w//2, y+h//2):
527
def drawedgelabel(img=img, x1=x-w//2, y1=y-h//2):
529
edgeheadcmd.append(drawedgelabel)
531
return edgebodycmd + nodebkgndcmd + edgeheadcmd + nodecmd
534
self.computevisible()
536
bbox = self.getboundingbox()
537
ox, oy, width, height = bbox
538
dpy_width, dpy_height = self.screen.get_size()
539
# some versions of the SDL misinterpret widely out-of-range values,
547
if width > dpy_width:
549
if height > dpy_height:
551
self.screen.fill((224, 255, 224), (ox, oy, width, height))
553
# gray off-bkgnd areas
554
gray = (128, 128, 128)
556
self.screen.fill(gray, (0, 0, ox, dpy_height))
558
self.screen.fill(gray, (0, 0, dpy_width, oy))
559
w = dpy_width - (ox + width)
561
self.screen.fill(gray, (dpy_width-w, 0, w, dpy_height))
562
h = dpy_height - (oy + height)
564
self.screen.fill(gray, (0, dpy_height-h, dpy_width, h))
566
# draw the graph and record the position of texts
567
del self.textzones[:]
568
for cmd in self.draw_commands():
571
def findall(self, searchstr):
572
"""Return an iterator for all nodes and edges that contain a searchstr.
574
for item in self.graphlayout.nodes.itervalues():
575
if item.label and searchstr in item.label:
577
for item in self.graphlayout.edges:
578
if item.label and searchstr in item.label:
581
def at_position(self, (x, y)):
582
"""Figure out the word under the cursor."""
583
for rx, ry, rw, rh, word in self.textzones:
584
if rx <= x < rx+rw and ry <= y < ry+rh:
588
def node_at_position(self, (x, y)):
589
"""Return the Node under the cursor."""
590
x, y = self.revmap(x, y)
591
for node in self.visiblenodes:
592
if 2.0*abs(x-node.x) <= node.w and 2.0*abs(y-node.y) <= node.h:
596
def edge_at_position(self, (x, y), distmax=14):
597
"""Return the Edge near the cursor."""
598
# XXX this function is very CPU-intensive and makes the display kinda sluggish
599
distmax /= self.scale
600
xy = self.revmap(x, y)
602
for edge in self.visibleedges:
603
pts = edge.bezierpoints()
604
for i in range(1, len(pts)):
605
d = segmentdistance(pts[i-1], pts[i], xy)
614
def __init__(self, renderer, text, fgcolor, bgcolor=None, font=None):
615
self.renderer = renderer
623
for word in re_nonword.split(text):
626
if word in renderer.highlightwords:
627
fg, bg = renderer.wordcolor(word)
630
fg, bg = fgcolor, bgcolor
631
parts.append((word, fg, bg))
632
# consolidate sequences of words with the same color
633
for i in range(len(parts)-2, -1, -1):
634
if parts[i][1:] == parts[i+1][1:]:
635
word, fg, bg = parts[i]
636
parts[i] = word + parts[i+1][0], fg, bg
638
# delete None backgrounds
639
for i in range(len(parts)):
640
if parts[i][2] is None:
641
parts[i] = parts[i][:2]
644
while i < len(parts):
649
img = font.render(word, False, *part[1:])
650
except pygame.error, e:
651
# Try *with* anti-aliasing to work around a bug in SDL
652
img = font.render(word, True, *part[1:])
654
del parts[i] # Text has zero width
656
self.imgs.append(img)
661
sizes = [img.get_size() for img in self.imgs]
662
return sum([w for w,h in sizes]), max([h for w,h in sizes])
666
def draw(self, x, y):
667
for part, img in zip(self.parts, self.imgs):
669
self.renderer.screen.blit(img, (x, y))
670
w, h = img.get_size()
671
self.renderer.textzones.append((x, y, w, h, word))