[pypy-svn] r5958 - pypy/trunk/src/pypy/translator/tool/pygame
arigo at codespeak.net
arigo at codespeak.net
Sat Aug 14 14:41:58 CEST 2004
Author: arigo
Date: Sat Aug 14 14:41:57 2004
New Revision: 5958
Added:
pypy/trunk/src/pypy/translator/tool/pygame/drawgraph.py
Modified:
pypy/trunk/src/pypy/translator/tool/pygame/graphviewer.py
Log:
Finished a rewrite started some time ago: the dot graphs are now rendered
entierely by a Python script from a description produced by 'dot' in its
so-called 'plain' format, which includes text, coordinates, and bezier control
points for the edges.
This allows much more flexibility. Try graphviewer.py. Variable names are
highlighted; the mouse position can be matched to the text more precisely than
was possible with the previous hack; the rendering's scale can be changed
easily (try dragging the right mouse button).
The goal is to display very large graphs with a reasonable amount of RAM
(before, large graphs were large bitmap images). Also, the zoom feature will
be convenient for that.
Added: pypy/trunk/src/pypy/translator/tool/pygame/drawgraph.py
==============================================================================
--- (empty file)
+++ pypy/trunk/src/pypy/translator/tool/pygame/drawgraph.py Sat Aug 14 14:41:57 2004
@@ -0,0 +1,392 @@
+"""
+A custom graphic renderer for the '.plain' files produced by dot.
+
+"""
+
+from __future__ import generators
+import autopath
+import re, os, math
+import pygame
+from pygame.locals import *
+
+
+FONT = os.path.join(autopath.this_dir, 'cyrvetic.ttf')
+COLOR = {
+ 'black': (0,0,0),
+ 'white': (255,255,255),
+ 'red': (255,0,0),
+ 'green': (0,255,0),
+ }
+re_nonword=re.compile(r'(\W+)')
+
+
+class GraphLayout:
+
+ def __init__(self, filename):
+ # parse the layout file (.plain format)
+ lines = open(filename, 'r').readlines()
+ for i in range(len(lines)-2, -1, -1):
+ if lines[i].endswith('\\\n'): # line ending in '\'
+ lines[i] = lines[i][:-2] + lines[i+1]
+ del lines[i+1]
+ header = splitline(lines.pop(0))
+ assert header[0] == 'graph'
+ self.scale = float(header[1])
+ self.boundingbox = float(header[2]), float(header[3])
+ self.nodes = {}
+ self.edges = []
+ for line in lines:
+ line = splitline(line)
+ if line[0] == 'node':
+ n = Node(*line[1:])
+ self.nodes[n.name] = n
+ if line[0] == 'edge':
+ self.edges.append(Edge(self.nodes, *line[1:]))
+ if line[0] == 'stop':
+ break
+
+class Node:
+ def __init__(self, name, x, y, w, h, label, style, shape, color, fillcolor):
+ self.name = name
+ self.x = float(x)
+ self.y = float(y)
+ self.w = float(w)
+ self.h = float(h)
+ self.label = label
+ self.style = style
+ self.shape = shape
+ self.color = color
+ self.fillcolor = fillcolor
+
+class Edge:
+ label = None
+
+ def __init__(self, nodes, tail, head, cnt, *rest):
+ self.tail = nodes[tail]
+ self.head = nodes[head]
+ cnt = int(cnt)
+ self.points = [(float(rest[i]), float(rest[i+1]))
+ for i in range(0, cnt*2, 2)]
+ rest = rest[cnt*2:]
+ if len(rest) > 2:
+ self.label, xl, yl = rest[:3]
+ self.xl = float(xl)
+ self.yl = float(yl)
+ rest = rest[3:]
+ self.style, self.color = rest
+
+ def bezierpoints(self, resolution=8):
+ result = []
+ pts = self.points
+ for i in range(0, len(pts)-3, 3):
+ result += beziercurve(pts[i], pts[i+1],
+ pts[i+2], pts[i+3], resolution)
+ return result
+
+ def arrowhead(self):
+ bottom_up = self.points[0][1] > self.points[-1][1]
+ if (self.tail.y > self.head.y) != bottom_up: # reversed edge
+ x0, y0 = self.points[0]
+ x1, y1 = self.points[1]
+ else:
+ x0, y0 = self.points[-1]
+ x1, y1 = self.points[-2]
+ vx = x0-x1
+ vy = y0-y1
+ f = 0.12 / math.sqrt(vx*vx + vy*vy)
+ vx *= f
+ vy *= f
+ return [(x0 + 0.9*vx, y0 + 0.9*vy),
+ (x0 + 0.4*vy, y0 - 0.4*vx),
+ (x0 - 0.4*vy, y0 + 0.4*vx)]
+
+def beziercurve((x0,y0), (x1,y1), (x2,y2), (x3,y3), resolution=8):
+ result = []
+ f = 1.0/(resolution-1)
+ for i in range(resolution):
+ t = f*i
+ t0 = (1-t)*(1-t)*(1-t)
+ t1 = t *(1-t)*(1-t) * 3.0
+ t2 = t * t *(1-t) * 3.0
+ t3 = t * t * t
+ result.append((x0*t0 + x1*t1 + x2*t2 + x3*t3,
+ y0*t0 + y1*t1 + y2*t2 + y3*t3))
+ return result
+
+def splitline(line, re_word = re.compile(r'[^\s"]\S*|["]["]|["].*?[^\\]["]')):
+ result = []
+ for word in re_word.findall(line):
+ if word.startswith('"'):
+ word = eval(word)
+ result.append(word)
+ return result
+
+
+class GraphRenderer:
+ MARGIN = 0.2
+ SCALEMIN = 25
+ SCALEMAX = 90
+ FONTCACHE = {}
+
+ def __init__(self, screen, graphlayout, scale=75):
+ self.graphlayout = graphlayout
+ self.setscale(scale)
+ self.setoffset(0, 0)
+ self.screen = screen
+ self.textzones = []
+ self.highlightwords = {}
+
+ def setscale(self, scale):
+ scale = max(min(scale, self.SCALEMAX), self.SCALEMIN)
+ self.scale = float(scale)
+ w, h = self.graphlayout.boundingbox
+ self.margin = int(self.MARGIN*scale)
+ self.width = int((w + 2*self.MARGIN)*scale)
+ self.height = int((h + 2*self.MARGIN)*scale)
+ self.bboxh = h
+ size = int(14 * scale / 75)
+ if size in self.FONTCACHE:
+ self.font = self.FONTCACHE[size]
+ else:
+ self.font = self.FONTCACHE[size] = pygame.font.Font(FONT, size)
+
+ def setoffset(self, offsetx, offsety):
+ "Set the (x,y) origin of the rectangle where the graph will be rendered."
+ self.ofsx = offsetx - self.margin
+ self.ofsy = offsety - self.margin
+
+ def shiftoffset(self, dx, dy):
+ self.ofsx += dx
+ self.ofsy += dy
+
+ def shiftscale(self, factor, fix=None):
+ if fix is None:
+ fixx, fixy = self.screen.get_size()
+ fixx //= 2
+ fixy //= 2
+ else:
+ fixx, fixy = fix
+ x, y = self.revmap(fixx, fixy)
+ self.setscale(self.scale * factor)
+ newx, newy = self.map(x, y)
+ self.shiftoffset(newx - fixx, newy - fixy)
+
+ def getboundingbox(self):
+ "Get the rectangle where the graph will be rendered."
+ offsetx = - self.margin - self.ofsx
+ offsety = - self.margin - self.ofsy
+ return (offsetx, offsety, self.width, self.height)
+
+ def map(self, x, y):
+ return (int(x*self.scale) - self.ofsx,
+ int((self.bboxh-y)*self.scale) - self.ofsy)
+
+ def revmap(self, px, py):
+ return ((px + self.ofsx) / self.scale,
+ self.bboxh - (py + self.ofsy) / self.scale)
+
+ def draw_node_commands(self, node):
+ xcenter, ycenter = self.map(node.x, node.y)
+ boxwidth = int(node.w * self.scale)
+ boxheight = int(node.h * self.scale)
+ fgcolor = COLOR.get(node.color, (0,0,0))
+ bgcolor = COLOR.get(node.fillcolor, (255,255,255))
+
+ text = node.label
+ lines = text.replace('\l','\l\n').replace('\r','\r\n').split('\n')
+ # ignore a final newline
+ if not lines[-1]:
+ del lines[-1]
+ wmax = 0
+ hmax = 0
+ commands = []
+ bkgndcommands = []
+
+ for line in lines:
+ raw_line = line.replace('\l','').replace('\r','') or ' '
+ img = TextSnippet(self, raw_line, (0, 0, 0), bgcolor)
+ w, h = img.get_size()
+ if w>wmax: wmax = w
+ if raw_line.strip():
+ if line.endswith('\l'):
+ def cmd(img=img, y=hmax):
+ img.draw(xleft, ytop+y)
+ elif line.endswith('\r'):
+ def cmd(img=img, y=hmax, w=w):
+ img.draw(xright-w, ytop+y)
+ else:
+ def cmd(img=img, y=hmax, w=w):
+ img.draw(xcenter-w//2, ytop+y)
+ commands.append(cmd)
+ hmax += h
+ #hmax += 8
+
+ # we know the bounding box only now; setting these variables will
+ # have an effect on the values seen inside the cmd() functions above
+ xleft = xcenter - wmax//2
+ xright = xcenter + wmax//2
+ ytop = ycenter - hmax//2
+ x = xcenter-boxwidth//2
+ y = ycenter-boxheight//2
+
+ if node.shape == 'box':
+ rect = (x-1, y-1, boxwidth+2, boxheight+2)
+ def cmd():
+ self.screen.fill(bgcolor, rect)
+ bkgndcommands.append(cmd)
+ def cmd():
+ pygame.draw.rect(self.screen, fgcolor, rect, 1)
+ commands.append(cmd)
+ elif node.shape == 'octagon':
+ step = 1-math.sqrt(2)/2
+ points = [(int(x+boxwidth*fx), int(y+boxheight*fy))
+ for fx, fy in [(step,0), (1-step,0),
+ (1,step), (1,1-step),
+ (1-step,1), (step,1),
+ (0,1-step), (0,step)]]
+ def cmd():
+ pygame.draw.polygon(self.screen, bgcolor, points, 0)
+ bkgndcommands.append(cmd)
+ def cmd():
+ pygame.draw.polygon(self.screen, fgcolor, points, 1)
+ commands.append(cmd)
+ return bkgndcommands, commands
+
+ def draw_commands(self):
+ nodebkgndcmd = []
+ nodecmd = []
+ for node in self.graphlayout.nodes.values():
+ cmd1, cmd2 = self.draw_node_commands(node)
+ nodebkgndcmd += cmd1
+ nodecmd += cmd2
+
+ edgebodycmd = []
+ edgeheadcmd = []
+ for edge in self.graphlayout.edges:
+ fgcolor = COLOR.get(edge.color, (0,0,0))
+ points = [self.map(*xy) for xy in edge.bezierpoints()]
+
+ def drawedgebody(points=points, fgcolor=fgcolor):
+ pygame.draw.lines(self.screen, fgcolor, False, points)
+ edgebodycmd.append(drawedgebody)
+
+ points = [self.map(*xy) for xy in edge.arrowhead()]
+ def drawedgehead(points=points, fgcolor=fgcolor):
+ pygame.draw.polygon(self.screen, fgcolor, points, 0)
+ edgeheadcmd.append(drawedgehead)
+
+ if edge.label:
+ x, y = self.map(edge.xl, edge.yl)
+ img = TextSnippet(self, edge.label, (0, 0, 0))
+ w, h = img.get_size()
+ def drawedgelabel(img=img, x1=x-w//2, y1=y-h//2):
+ img.draw(x1, y1)
+ edgeheadcmd.append(drawedgelabel)
+
+ return edgebodycmd + nodebkgndcmd + edgeheadcmd + nodecmd
+
+ def render(self):
+ bbox = self.getboundingbox()
+ self.screen.fill((224, 255, 224), bbox)
+
+ # gray off-bkgnd areas
+ ox, oy, width, height = bbox
+ dpy_width, dpy_height = self.screen.get_size()
+ gray = (128, 128, 128)
+ if ox > 0:
+ self.screen.fill(gray, (0, 0, ox, dpy_height))
+ if oy > 0:
+ self.screen.fill(gray, (0, 0, dpy_width, oy))
+ w = dpy_width - (ox + width)
+ if w > 0:
+ self.screen.fill(gray, (dpy_width-w, 0, w, dpy_height))
+ h = dpy_height - (oy + height)
+ if h > 0:
+ self.screen.fill(gray, (0, dpy_height-h, dpy_width, h))
+
+ # draw the graph and record the position of texts
+ del self.textzones[:]
+ for cmd in self.draw_commands():
+ cmd()
+
+ def at_position(self, (x, y)):
+ """Figure out the word under the cursor."""
+ for rx, ry, rw, rh, word in self.textzones:
+ if rx <= x < rx+rw and ry <= y < ry+rh:
+ return word
+ return None
+
+class TextSnippet:
+
+ def __init__(self, renderer, text, fgcolor, bgcolor=None):
+ self.renderer = renderer
+ parts = []
+ for word in re_nonword.split(text):
+ if not word:
+ continue
+ if word in renderer.highlightwords:
+ fg, bg = renderer.highlightwords[word]
+ bg = bg or bgcolor
+ else:
+ fg, bg = fgcolor, bgcolor
+ parts.append((word, fg, bg))
+ # consolidate sequences of words with the same color
+ for i in range(len(parts)-2, -1, -1):
+ if parts[i][1:] == parts[i+1][1:]:
+ word, fg, bg = parts[i]
+ parts[i] = word + parts[i+1][0], fg, bg
+ del parts[i+1]
+ # delete None backgrounds
+ for i in range(len(parts)):
+ if parts[i][2] is None:
+ parts[i] = parts[i][:2]
+ # render parts
+ self.imgs = []
+ i = 0
+ while i < len(parts):
+ part = parts[i]
+ word = part[0]
+ antialias = not re_nonword.match(word) # SDL bug with anti-aliasing
+ try:
+ img = renderer.font.render(word, antialias, *part[1:])
+ except pygame.error:
+ del parts[i] # Text has zero width
+ else:
+ self.imgs.append(img)
+ i += 1
+ self.parts = parts
+
+ def get_size(self):
+ if self.imgs:
+ sizes = [img.get_size() for img in self.imgs]
+ return sum([w for w,h in sizes]), max([h for w,h in sizes])
+ else:
+ return 0, 0
+
+ def draw(self, x, y):
+ for part, img in zip(self.parts, self.imgs):
+ word = part[0]
+ self.renderer.screen.blit(img, (x, y))
+ w, h = img.get_size()
+ self.renderer.textzones.append((x, y, w, h, word))
+ x += w
+
+
+try:
+ sum # 2.3 only
+except NameError:
+ def sum(lst):
+ total = 0
+ for item in lst:
+ total += lst
+ return total
+
+
+def build_layout(graphs, name=None):
+ """ Build a GraphLayout from a list of control flow graphs.
+ """
+ from pypy.translator.tool.make_dot import make_dot_graphs
+ name = name or graphs[0].name
+ gs = [(graph.name, graph) for graph in graphs]
+ fn = make_dot_graphs(name, gs, target='plain')
+ return GraphLayout(str(fn))
Modified: pypy/trunk/src/pypy/translator/tool/pygame/graphviewer.py
==============================================================================
--- pypy/trunk/src/pypy/translator/tool/pygame/graphviewer.py (original)
+++ pypy/trunk/src/pypy/translator/tool/pygame/graphviewer.py Sat Aug 14 14:41:57 2004
@@ -3,7 +3,7 @@
import sys, os, re
import pygame
from pygame.locals import *
-from pypy.translator.tool.make_dot import make_dot_graphs
+from drawgraph import GraphRenderer, build_layout
class Display(object):
@@ -17,159 +17,10 @@
self.height = h
self.screen = pygame.display.set_mode((w, h), HWSURFACE|RESIZABLE)
-class GraphViewer(object):
- FONT = os.path.join(autopath.this_dir, 'cyrvetic.ttf')
- xscale = 1
- yscale = 1
- offsetx = 0
- offsety = 0
-
- def __init__(self, xdotfile, pngfile):
- pygame.init()
- g = open(str(pngfile), 'rb')
- try:
- self.bkgnd = pygame.image.load(pngfile)
- except Exception, e:
- print >> sys.stderr, '* Pygame cannot load "%s":' % pngfile
- print >> sys.stderr, '* %s: %s' % (e.__class__.__name__, e)
- print >> sys.stderr, '* Trying with pngtopnm.'
- import os
- g = os.popen("pngtopnm '%s'" % pngfile, 'r')
- w, h, data = decodepixmap(g)
- g.close()
- self.bkgnd = pygame.image.fromstring(data, (w, h), "RGB")
- self.width, self.height = self.bkgnd.get_size()
- self.font = pygame.font.Font(self.FONT, 18)
-
- # compute a list of (rect, originalw, text, name)
- # where text is some text from the graph,
- # rect is its position on the screen,
- # originalw is its real (dot-computed) size on the screen,
- # and name is XXX
- self.positions = []
- g = open(xdotfile, 'rb')
- lines = g.readlines()
- g.close()
- self.parse_xdot_output(lines)
-
- def render(self, dpy):
- ox = -self.offsetx
- oy = -self.offsety
- dpy.screen.blit(self.bkgnd, (ox, oy))
- # gray off-bkgnd areas
- gray = (128, 128, 128)
- if ox > 0:
- dpy.screen.fill(gray, (0, 0, ox, dpy.height))
- if oy > 0:
- dpy.screen.fill(gray, (0, 0, dpy.width, oy))
- w = dpy.width - (ox + self.width)
- if w > 0:
- dpy.screen.fill(gray, (dpy.width-w, 0, w, dpy.height))
- h = dpy.height - (oy + self.height)
- if h > 0:
- dpy.screen.fill(gray, (0, dpy.height-h, dpy.width, h))
-
- def at_position(self, (x, y), re_nonword=re.compile(r'(\W+)')):
- """Compute (word, text, name) where word is the word under the cursor,
- text is the complete line, and name is XXX. All three are None
- if no text is under the cursor."""
- x += self.offsetx
- y += self.offsety
- for (rx,ry,rw,rh), originalw, text, name in self.positions:
- if rx <= x < rx+originalw and ry <= y < ry+rh:
- dx = x - rx
- # scale dx to account for small font mismatches
- dx = int(float(dx) * rw / originalw)
- words = [s for s in re_nonword.split(text) if s]
- segment = ''
- word = ''
- for word in words:
- segment += word
- img = self.font.render(segment, 1, (255, 0, 0))
- w, h = img.get_size()
- if dx < w:
- break
- return word, text, name
- return None, None, None
-
- def getzones(self, re_nonword=re.compile(r'(\W+)')):
- for (rx,ry,rw,rh), originalw, text, name in self.positions:
- words = [s for s in re_nonword.split(text) if s]
- segment = ''
- dx1 = 0
- for word in words:
- segment += word
- img = self.font.render(segment, 1, (255, 0, 0))
- w, h = img.get_size()
- dx2 = int(float(w) * originalw / rw)
- if word.strip():
- yield (rx+dx1, ry, dx2-dx1, rh), word
- dx1 = dx2
-
- def parse_xdot_output(self, lines):
- for i in range(len(lines)):
- if lines[i].endswith('\\\n'):
- lines[i+1] = lines[i][:-2] + lines[i+1]
- lines[i] = ''
- for line in lines:
- self.parse_xdot_line(line)
-
- def parse_xdot_line(self, line,
- re_bb = re.compile(r'\s*graph\s+[[]bb=["]0,0,(\d+),(\d+)["][]]'),
- re_text = re.compile(r"\s*T" + 5*r"\s+(-?\d+)" + r"\s+-"),
- matchtext = ' _ldraw_="'):
- match = re_bb.match(line)
- if match:
- self.xscale = float(self.width-12) / int(match.group(1))
- self.yscale = float(self.height-12) / int(match.group(2))
- return
- p = line.find(matchtext)
- if p < 0:
- return
- p += len(matchtext)
- line = line[p:]
- while 1:
- match = re_text.match(line)
- if not match:
- break
- x = 10+int(float(match.group(1)) * self.xscale)
- y = self.height-2-int(float(match.group(2)) * self.yscale)
- n = int(match.group(5))
- end = len(match.group())
- text = line[end:end+n]
- line = line[end+n:]
- if text:
- img = self.font.render(text, 1, (255, 0, 0))
- w, h = img.get_size()
- align = int(match.group(3))
- if align == 0:
- x -= w//2
- elif align > 0:
- x -= w
- rect = x, y-h, w, h
- originalw = int(float(match.group(4)) * self.xscale)
- self.positions.append((rect, originalw, text, 'XXX'))
-
-
-
-def decodepixmap(f):
- sig = f.readline().strip()
- assert sig == "P6"
- while 1:
- line = f.readline().strip()
- if not line.startswith('#'):
- break
- wh = line.split()
- w, h = map(int, wh)
- sig = f.readline().strip()
- assert sig == "255"
- data = f.read()
- f.close()
- return w, h, data
-
class GraphDisplay(Display):
STATUSBARFONT = os.path.join(autopath.this_dir, 'VeraMoBd.ttf')
+ SCALE = 60
def __init__(self, translator, functions=None):
super(GraphDisplay, self).__init__()
@@ -182,15 +33,13 @@
for var in self.annotator.bindings:
self.variables_by_name[var.name] = var
- graphs = []
functions = functions or self.translator.functions
- for func in functions:
- graph = self.translator.getflowgraph(func)
- graphs.append((graph.name, graph))
- xdotfile = make_dot_graphs(functions[0].__name__, graphs, target='xdot')
- pngfile = make_dot_graphs(functions[0].__name__, graphs, target='png')
- self.viewer = GraphViewer(str(xdotfile), str(pngfile))
- self.viewer.offsetx = (self.viewer.width - self.width) // 2
+ graphs = [self.translator.getflowgraph(func) for func in functions]
+ layout = build_layout(graphs)
+ self.viewer = GraphRenderer(self.screen, layout, self.SCALE)
+ # center horizonally
+ self.viewer.setoffset((self.viewer.width - self.width) // 2, 0)
+ self.sethighlight()
self.statusbarinfo = None
self.must_redraw = True
@@ -229,18 +78,26 @@
y += h
def notifymousepos(self, pos):
- word, text, name = self.viewer.at_position(pos)
+ word = self.viewer.at_position(pos)
if word in self.variables_by_name:
var = self.variables_by_name[word]
s_value = self.annotator.binding(var)
info = '%s: %s' % (var.name, s_value)
self.setstatusbar(info)
+ self.sethighlight(word)
+
+ def sethighlight(self, word=None):
+ self.viewer.highlightwords = {}
+ for name in self.variables_by_name:
+ self.viewer.highlightwords[name] = ((128,0,0), None)
+ if word:
+ self.viewer.highlightwords[word] = ((255,255,80), (128,0,0))
def run(self):
dragging = None
while 1:
if self.must_redraw:
- self.viewer.render(self)
+ self.viewer.render()
if self.statusbarinfo:
self.drawstatusbar()
pygame.display.flip()
@@ -252,8 +109,12 @@
if pygame.event.peek([MOUSEMOTION]):
continue
if dragging:
- self.viewer.offsetx -= (event.pos[0] - dragging[0])
- self.viewer.offsety -= (event.pos[1] - dragging[1])
+ dx = event.pos[0] - dragging[0]
+ dy = event.pos[1] - dragging[1]
+ if event.buttons[2]: # right mouse button
+ self.viewer.shiftscale(1.003 ** dy)
+ else:
+ self.viewer.shiftoffset(-2*dx, -2*dy)
dragging = event.pos
self.must_redraw = True
else:
More information about the Pypy-commit
mailing list