[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