rpms/ghost-diagrams/devel ghost-diagrams-0.8.py, NONE, 1.1 ghost-diagrams.spec, NONE, 1.1

Ignacio Vazquez-Abrams (ivazquez) fedora-extras-commits at redhat.com
Fri Feb 1 19:48:59 UTC 2008


Author: ivazquez

Update of /cvs/pkgs/rpms/ghost-diagrams/devel
In directory cvs-int.fedora.redhat.com:/tmp/cvs-serv7691/devel

Added Files:
	ghost-diagrams-0.8.py ghost-diagrams.spec 
Log Message:
Initial import


--- NEW FILE ghost-diagrams-0.8.py ---
#!/usr/bin/env python

#    Copyright (C) 2004 Paul Harrison
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

"""  Ghost Diagrams
     
     This program takes sets of tiles that connect together in certain
     ways, and looks for the patterns these tiles imply. The patterns
     are often surprising. 
     

     This software is currently somewhat alpha.
     
  Tile set specification:
  
     A tile set specification is a list of 6- or 4-character strings,
     eg 'B Aa  ', 'b  Aa '
     
     Each character represents a tile edge. Letters (abcd, ABCD)
     match with their opposite case. Numbers match with themselves.
     
     A number of extra paramters can also be supplied:
     
         border : True/False : draw tile borders or not
         thickness : the thickness of the border
         width : minimum width of diagram
         height : minimum height of diagram
         colors : a list of colors 
            [ background color, edge color, tile 1 color, tile 2 color... ]
         grid : True/False : draw a grid
         labels : True/False : draw labels for each tile under diagram
            
     eg 'B Aa  ', 'b  Aa ', width=1000, height=1000, thickness=0.5, colors=['#000','#000','#fff','#f00']
     
  Change log:
     
     0.1 -- initial release
     0.2 -- don't segfault on empty tiles
     0.3 -- random keeps trying till it finds something that will grow
            optimization (options_cache)
     0.4 -- assembly algorithm tweaks
            random tile set tweaks
     0.5 -- Patch by Jeff Epler
             - allow window resizing
             - new connection types (33,44,cC,dD)
             - DNA tile set 
            widget to set size of tiles
            no repeated tiles in random
            improvements to assembler
     0.6 -- Use Bezier curves
            Parameters to set width, height, thickness, color
            Save images
     0.7 -- Allow square tiles
            Smarter assembler
            Animate assembly
     0.8 -- Knotwork
            Don't fill all of memory
            Use psyco if available


  TODO: don't backtrack areas outside current locus
        (difficulty: accidentally creating disconnected islands)
        
  TODO: (blue sky) 3D, third dimension == time
  TODO: allowances: 3 of this, 2 of that, etc.
"""

try:
    import psyco
    psyco.profile()
except:
    pass

__version__ = '0.8'

import sys, os, random, gtk, pango, gobject, string, math, sets

class Point:
    def __init__(self, x,y):
        self.x = x
        self.y = y
    
    def __add__(self, other): return Point(self.x+other.x,self.y+other.y)
    def __sub__(self, other): return Point(self.x-other.x,self.y-other.y)
    def __mul__(self, factor): return Point(self.x*factor, self.y*factor)
    def length(self): return (self.y*self.y + self.x*self.x) ** 0.5
    def int_xy(self): return int(self.x+0.5), int(self.y+0.5)
    def left90(self): return Point(-self.y, self.x)


# ========================================================================
# Constants

# Hexagonal connection pattern:
#
#     o o
#   o * o
#   o o
#
# (all points remain on a square grid, but a regular hexagon pattern 
#  can be formed by a simple linear transformation)

connections_6 = [ (-1, 0, 3), (-1, 1, 4), (0, 1, 5), (1, 0, 0), (1, -1, 1), (0, -1, 2) ]
# [ (y, x, index of reverse connection) ]
x_mapper_6 = Point(1.0, 0.0)
y_mapper_6 = Point(0.5, 0.75**0.5)

connections_4 = [ (-1,0,2), (0,1,3), (1,0,0), (0,-1,1) ]
x_mapper_4 = Point(1.0, 0.0)
y_mapper_4 = Point(0.0, 1.0)



# What edge type connects with what?
# (a tile is represented as a string of 6 characters representing the 6 edges)
compatabilities = { 
    ' ':' ', 
    'A':'a', 'a':'A', 'B':'b', 'b':'B', 'c':'C', 'C':'c', 'd':'D', 'D':'d',
    '1':'1', '2':'2', '3':'3', '4':'4',  '-':'-'
}


# Amount of oversampling in drawing
factor = 3


# Some cool tile sets people have found
catalogue = [
    "'  33Aa', ' 33 Aa'",
    "'  33Aa/#000', ' 33 Aa/#000', colors=['#fff','#fff'], border=0, grid=0",
    "'ab A  ', 'B  C  ', 'B  c  ', 'B  D  ', 'B  d  '",
    "'d D 4 ', 'd  D  ', '44    '",
    "'AaAa  '",
    "'aA    ', 'AaAa  '",
    "'   bB ', 'bAaB  ', 'aAaA  '",    
    "'B Aa  ', 'b  Aa '",
    "'44    ', '11  4 '",
    "'3 3 3 ', '33    '",
    "'1 1 1 ', '2  12 '",
    "' a    ', 'a AA  '",
    "' AAaa ', 'a  A  '",
    "' a A  ', 'Aaa  A'",
    "'a  a  ', ' aAA A'",
    "' a A a', '    A '",
    "'  AA A', 'a a a '",
    "'a  aa ', '    AA'",
    "'A A a ', 'a a   '",
    "'A A a ', 'a  a  '",
    "' a   4', 'a4 44A', '4A    '",
    "'a2  A ', ' a2 A2'",
    "'a 2a2 ', ' A   A', '     2'",
    "'141   ', '4  4  ', '1 1   '",
    "' 22 22', '22    '",
    "' Aaa  ', 'A1A   ', 'a 1AAa'",
    "'aA a2 ', '2A    ', '  2  A'",
    "'  bB1 ', ' b  B '",
    "'BbB 1 ', '     b'",
    "'b  b b', '  BbB '",
    "'aA1   ', '   AA ', 'a  2  '",
    "' a  4 ', ' 4 4  ', '  A441'",
    "'212111', ' 1 2  '",
    "'22222a', '22 A22'",
    "'2 222 ', '2   B2', '  b  2'",
    "' 21221', '   221', '   2 2'",
    "' a a a', '   A A'",
    "' Dd cA', '   d D', '   a C'",
    "'  CCCc', ' 3Ca A', '  3  c', '     c'",
    "' C dDc', '  CC C', '   ccC'",
    "' Aa Cc', '     c', '     C'",
    "' CcDdC', '  cC c', '     C'",
    "'  CcCc', ' CcC c', '   c C'",
    "'A 1 1 ','a1   B','b  1  '",
    "'aa aa /#fff', 'AA    /#fff', 'A  A  /#fff', grid=0, border=0, thickness=0.3",
    #"'bb bb ', 'BB    ', 'B  B  '",
    "' 44B4D', ' dbB4b', ' 44D d', '    44'",
    "'  d3 3', '   D D'",
    "'  cc c', ' C C c'",
    "'AaAaaa', '  1 Aa', '     A'",
    "'d D 3 ', 'dD    ', '3     '",
    "'a 1 A ', 'a  A  '",
    "'cCCcCC', 'cccC  ', 'c C  C'",
    "'A44444', 'a4   4', '4 4   '",
    "'acaACA', 'acbBCB', 'bcaBCB', 'bcbACA'",
    "'A  ab ', 'B ab  ', 'A  a  ', 'B  b  ', 'ABd  D'", #Tree
    "'d AD  ', ' a  A ', 'a   A ', 'aa A  '", #Counter (?)
    "'bBbBBB', 'bb    ', 'b   B '",
    "'a AA A', 'a a   '",
    "'cC a A', 'a A   '",
    "'bbB  B', 'b BBB ', 'bb    '",
    "'cCc C ', 'cC c C'",
    "'d4 Dd ', 'd D   ', 'DD    '",
    "' 111'",
    "'abA ', 'B C ', 'B c ', 'B D ', 'B d '",
    "'4A4a', '  a4', ' A B', '  Ab'",
    "'acAC', 'adBD', 'bcBD', 'bdAC'",
    "'1111', '   1'",
    "' bbb', '  BB'",
    "'1B1B', 'a A ', ' bA ', 'ab B'",
]


default_colors=['#fff','#000', '#8ff', '#f44', '#aaf','#449', '#ff0088', '#ff4088', '#ff4040', '#ff00ff', '#40c0ff']

default_thickness = 3.0/16


# ========================================================================
# Utility functions

def bezier(a,b,c,d):
    result = [ ]
    n = 12
    for i in xrange(1,n):
        u = float(i) / n
        result.append(
            a * ((1-u)*(1-u)*(1-u)) +
            b * (3*u*(1-u)*(1-u)) +
            c * (3*u*u*(1-u)) +
            d * (u*u*u)
        )
    return result


def normalize(form):
    best = form
    for i in xrange(len(form)-1):
        form = form[1:] + form[0]
        if form > best: best = form
    return best


# =========================================================================

class Config:
    def __init__(self, *forms, **kwargs):                                
        self.colors = kwargs.get("colors", [ ])
        self.colors += default_colors[len(self.colors):]
    
        self.border = kwargs.get("border", True)
        self.thickness = kwargs.get("thickness", default_thickness)
        self.width = kwargs.get("width", -1)
        self.height = kwargs.get("height", -1)
        
        self.grid = kwargs.get("grid", True)
        self.labels = kwargs.get("labels", False)
    
        forms = list(forms)
    
        if len(forms) < 1: raise "error"
        
        for item in forms:
            if type(item) != type(""):
                raise "error"
                
        for i in xrange(len(forms)):
            if "/" in forms[i]:
                forms[i], self.colors[i%(len(self.colors)-2)+2] = forms[i].split("/",1)
                
        if len(forms[0]) == 4:
            self.connections = connections_4
            self.x_mapper = x_mapper_4
            self.y_mapper = y_mapper_4
        else:
            self.connections = connections_6
            self.x_mapper = x_mapper_6
            self.y_mapper = y_mapper_6
            
        for item in forms:
            if len(item) != len(self.connections):
                raise "error"
            for edge in item:
                if edge not in compatabilities:
                    raise "error"
        
        self.forms = forms

# ========================================================================


class Assembler:
    def __init__(self, connections, compatabilities, forms, point_set):
        self.connections = connections    # [(y,x,index of reverse connection)]
        self.compatabilities = compatabilities    # { edge-char -> edge-char }
        self.point_set = point_set   # (y,x) -> True

        self.basic_forms = forms   # ['edge types']
        self.forms = [ ]   # ['edge types']
        self.form_id = [ ]   # [original form number]
        self.rotation = [ ]  # [rotation from original]
        
        for id, form in enumerate(forms):
            current = form
            for i in xrange(len(self.connections)):
                if current not in self.forms:
                    self.forms.append(current)
                    self.form_id.append(id)
                    self.rotation.append(i)
                current = current[1:] + current[0]

        self.tiles = { }   # (y,x) -> form number
        
        self.dirty = { }   # (y,x) -> True   -- Possible sites for adding tiles
        
        self.options_cache = { }   # pattern -> [form_ids]
        
        self.dead_loci = sets.Set([ ]) # [ {(y,x)->form number} ]
        
        self.history = [ ]
                
        self.total_y = 0
        self.total_x = 0
        
        self.changes = { }

    def put(self, y,x, value):
        if (y,x) in self.changes:
            if value == self.changes[(y,x)]:
                del self.changes[(y,x)]
        else:
            self.changes[(y,x)] = self.tiles.get((y,x),None)
        
            
        if (y,x) in self.tiles:
            self.total_y -= y
            self.total_x -= x
            
        if value == None:
            if (y,x) not in self.tiles: return
            del self.tiles[(y,x)]
            self.dirty[(y,x)] = True
        else:
            self.tiles[(y,x)] = value
            self.total_y += y
            self.total_x += x
        
        for oy, ox, opposite in self.connections:
            y1 = y + oy
            x1 = x + ox
            if (y1,x1) not in self.tiles and (y1, x1) in self.point_set:
                self.dirty[(y1,x1)] = True
                    
    def get_pattern(self, y,x):
        result = ''
        for oy, ox, opposite in self.connections:
            y1 = y + oy
            x1 = x + ox
            if self.tiles.has_key((y1,x1)):
                result += self.compatabilities[self.forms[self.tiles[(y1,x1)]][opposite]]
            #elif (y1,x1) not in self.point_set:
            #    result += ' '
            else:
                result += '.'
        
        return result

    def fit_ok(self, pattern,form_number):
        form = self.forms[form_number]
        for i in xrange(len(self.connections)):
            if pattern[i] != '.' and pattern[i] != form[i]:
                return False
               
        return True
    
    def options(self, y,x):
        pattern = self.get_pattern(y,x)
        if pattern in self.options_cache:
            result = self.options_cache[pattern]
        
        result = [ ]
        for i in xrange(len(self.forms)):
            if self.fit_ok(pattern,i):
                result.append(i)
        result = tuple(result)
        
        self.options_cache[pattern] = result
        
        return result
            
    def locus(self, y,x, rotation=0):
        visited = { }
        neighbours = { }
        todo = [ ((y,x), (0,0)) ]
        result = [ ]
        
        min_y = 1<<30
        min_x = 1<<30
        
        while todo:
            current, offset = todo.pop(0)
            if current in visited: continue
            visited[current] = True

            any = False
            new_todo = [ ]
            for i, (oy, ox, opposite) in enumerate(self.connections):
                neighbour = (current[0]+oy, current[1]+ox)
                if neighbour in self.point_set:                
                    if neighbour in self.tiles:
                        any = True
                        neighbours[neighbour] = True
                        min_y = min(min_y, offset[0])
                        min_x = min(min_x, offset[1])
                        result.append( (offset, opposite, 
                                        self.forms[self.tiles[neighbour]][opposite]) )
                    else:
                        temp = self.connections[(i+rotation) % len(self.connections)]
                        new_offset = (offset[0]+temp[0], offset[1]+temp[1])                
                        new_todo.append((neighbour, new_offset))
                        
            if not any and len(self.connections) == 4:
                for oy, ox in ((-1,-1), (-1,1), (1,-1), (1,1)):
                    neighbour = (current[0]+oy, current[1]+ox)
                    if neighbour in self.tiles:
                        any = True
                        break
        
            if any:
                todo.extend(new_todo)
                
        result = [ (yy-min_y,xx-min_x,a,b) for ((yy,xx),a,b) in result ]

        return sets.ImmutableSet(result), visited, neighbours

        
    def filter_options(self, y,x,options):
        result = [ ]
        for i in options:
            self.tiles[(y,x)] = i
            visiteds = [ ]
            
            for oy, ox, oppoiste in self.connections:
                y1 = y+oy
                x1 = x+ox
                
                ok = True
                if (y1,x1) not in self.tiles and (y1,x1) in self.point_set:
                    for visited in visiteds:
                        if (y1,x1) in visited: 
                            ok = False
                            break
                    if ok:
                        locus, visited, _ = self.locus(y1,x1)
                        visiteds.append(visited)
                        if locus is not None and locus in self.dead_loci: 
                            break
            else:
                result.append(i)
            
            del self.tiles[(y,x)]

        return result
                            
    def any_links_to(self, y,x):
        for oy, ox, opposite in self.connections:
            y1 = y + oy
            x1 = x + ox
            if (y1, x1) in self.tiles:
                if self.forms[self.tiles[(y1,x1)]][opposite] != ' ':
                    return True
        return False

    def prune_dead_loci(self):
        for item in list(self.dead_loci):
            if random.randrange(2):
                self.dead_loci.remove(item)

    def iterate(self):
        if not self.tiles:
            self.put(0,0,0)
            self.history.append((0,0))
            return True
            
        mid_y = 0.0
        mid_x = 0.0
        for y, x in self.dirty.keys():
            if (y,x) in self.tiles or not self.any_links_to(y,x): 
                del self.dirty[(y,x)]
                continue
            mid_y += y
            mid_x += x
        
        if not self.dirty:
            return False
        
        mid_y /= len(self.dirty)
        mid_x /= len(self.dirty)
                        
        point_list = [ ]
        for y, x in self.dirty.keys():
            yy = y - mid_y
            xx = x - mid_x
            sorter = ((yy*2)**2+(xx*2+yy)**2)
            point_list.append( (sorter,y,x) )
            
        point_list.sort()
        
        best = None        

        for sorter, y, x in point_list:
            options = self.options(y,x)
            
            if len(options) < 2:
                score = 0
            else:
                score = 1
                                                
            if best == None or score < best_score:
                best = options
                best_score = score
                best_x = x
                best_y = y
                if score == 0: break

        if best == None: return False
        
        best = self.filter_options(best_y,best_x,best)
                
        if len(best) > 0:
            self.put(best_y,best_x,random.choice(best))
            self.history.append((best_y,best_x))
            return True
            
        #otherwise, backtrack:
            
        for i in xrange(len(self.connections)):
            locus, _, relevant = self.locus(best_y,best_x,i)
            if locus is None: break
            self.dead_loci.add(locus)
            if len(locus) > 8: break

        if len(self.dead_loci) > 10000:
            self.prune_dead_loci()                
            
        # Shape of distribution
        autism = 1.0 # 1.0 == normal, >1.0 == autistic (just a theory :-) )
            
        # Overall level
        adhd = 2.0   # Lower == more adhd
            
        n = 1
        while n < len(self.tiles)-1 and random.random() < ( n/(n+autism) )**adhd:
            n += 1
            
        while self.history and (n > 0 or
                                self.locus(best_y,best_x)[0] in self.dead_loci):
            item = self.history.pop(-1)
            self.put(item[0],item[1],None)
            n -= 1
                
        if not self.tiles:
            return False

        return True




# ========================================================================


class Interface:
    def __init__(self):
        self.iteration = 0
        self.idle_enabled = False

        self.drawing = gtk.DrawingArea()
        self.drawing.unset_flags(gtk.DOUBLE_BUFFERED)
        #self.drawing.add_events(gtk.gdk.BUTTON_PRESS_MASK)
        self.drawing.connect('expose-event', self.on_expose)
        self.drawing.connect('size-allocate', self.on_size)
        #self.drawing.connect('button-press-event', self.on_click)
        
        scroller = gtk.ScrolledWindow()
        scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        scroller.add_with_viewport(self.drawing)
    
        self.combo = gtk.Combo()
        for item in catalogue:
            item = gtk.ListItem(item)
            item.get_children()[0].modify_font(pango.FontDescription("mono"))
            item.show()
            self.combo.list.append_items([item])
        self.combo.list.connect('select-child', lambda widget, child: self.reset())
        self.combo.entry.modify_font(pango.FontDescription("mono"))
        
        knot_box = gtk.CheckButton("knotwork")
        knot_box.set_active(False)
        self.knot = False
        knot_box.connect("toggled", self.on_knot_changed)
        
        scale_label = gtk.Label(' Size: ')
        scale = gtk.SpinButton()
        scale.set_digits(1)
        scale.set_increments(1.0,1.0)
        scale.set_range(3.0, 50.0)
        scale.set_value(10.0)
        scale.connect('value-changed', self.on_set_scale)

        random_button = gtk.Button(' Random ')
        random_button.connect('clicked', lambda widget: self.random())
        
        reset = gtk.Button(' Restart ')
        reset.connect('clicked', lambda widget: self.reset())

        save = gtk.Button(' Save image... ')
        save.connect('clicked', self.on_save)

        hbox = gtk.HBox(False,5)
        hbox.set_border_width(3)
        hbox.pack_start(knot_box, False, False, 0)
        hbox.pack_end(save, False,False,0)
        hbox.pack_end(reset, False,False,0)
        hbox.pack_end(random_button, False,False,0)
        hbox.pack_end(scale, False,False,0)
        hbox.pack_end(scale_label, False,False,0)

        vbox = gtk.VBox(False,5)
        vbox.pack_start(self.combo, False,False,0)
        vbox.pack_start(hbox, False,False,0)
        vbox.pack_start(scroller, True,True,0)

        self.window = gtk.Window()
        self.window.set_default_size(600,650)
        #self.window.set_default_size(200,200)
        self.window.set_title('Ghost Diagrams')
        self.window.add(vbox)
        self.window.connect('destroy', lambda widget: gtk.main_quit())

        self.drawing.realize()
        
        self.gc = gtk.gdk.GC(self.drawing.window)

        self.pixbuf_ready = False
        self.render_iterator = None
        
        self.randomizing = False
        
        self.set_scale(scale.get_value())
    
    def on_size(self, widget, event):
        self.pixbuf_ready = False
        
        self.width = event.width
        self.height = event.height
        self.pixmap = gtk.gdk.Pixmap(self.drawing.window, self.width*factor,self.height*factor)

        self.pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8,
                                     self.width*factor,self.height*factor)
        self.scaled_pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, 
                                            self.width,self.height)
        self.reset()

    def on_set_scale(self, widget):
        self.set_scale(widget.get_value())
        self.reset()

    def set_scale(self, value):
        self.scale = value
        
    def on_knot_changed(self, widget):
        self.knot = widget.get_active()
        self.reset()
        
    def reset(self):
        try:
            self.config = eval('Config('+self.combo.entry.get_text()+')')
        except:
            import traceback
            traceback.print_exc()
            self.config = Config("    ", grid=False, colors=["#f66"])

        colormap = self.drawing.get_colormap()
        self.colors = [ colormap.alloc_color(item) for item in self.config.colors ]
        
        point_set = { }
        yr = int( self.height/self.scale/4 )
        xr = int( self.width/self.scale/4 )        
        if self.config.labels:
            bound = self.scale * 3
            for y in xrange(-yr,yr):
                for x in xrange(-yr,xr):
                    point = self.pos(x*2,y*2)
                    if point.x > bound and point.x < self.width-bound and \
                       point.y > bound and point.y < self.height-bound-90:
                        point_set[(y,x)] = True
        else:
            bound = self.scale * 3
            for y in xrange(-yr,yr):
                for x in xrange(-yr,xr):
                    point = self.pos(x*2,y*2)
                    if point.x > -bound and point.x < self.width+bound and \
                       point.y > -bound and point.y < self.height+bound:
                        point_set[(y,x)] = True
                    

        self.pixbuf_ready = False
        self.render_iterator = None
        self.randomizing = False
        self.iteration = 0
        self.drawing.queue_draw()
        self.shapes = { }
        self.polys = { }
        self.assembler = Assembler(self.config.connections, compatabilities, 
                                   self.config.forms, point_set)
        if not self.idle_enabled:
            gobject.idle_add(self.idle)
            self.idle_enabled = True
            
        self.drawing.set_size_request(self.config.width, self.config.height)
        self.drawing.queue_draw()

                
    def run(self):
        self.window.show_all()
        gtk.main()


    def idle(self):
        if self.render_iterator:
            try:
                self.render_iterator.next()
            except StopIteration:
                self.render_iterator = None
            return True
        
        if not self.idle_enabled: 
            return False
        
        self.idle_enabled = self.assembler.iterate()

        self.iteration += 1
        
        if self.randomizing and \
           (self.iteration == 100 or not self.idle_enabled):
            self.randomizing = False
            
            forms_present = { }
            for item in self.assembler.tiles.values():
                forms_present[self.assembler.form_id[item]] = 1
            
            if len(self.assembler.tiles) < 10 \
               or len(forms_present) < len(self.assembler.basic_forms):
                self.idle_enabled = False
                self.random(True)
                return False

        if not self.idle_enabled or len(self.assembler.changes) >= 8:
            changes = self.assembler.changes
            self.assembler.changes = { }
            for y,x in changes:
                old = changes[(y,x)]
                if old is not None:
                    self.draw_poly(y,x,old,1, self.drawing.window, True)
            for y,x in changes:
                new = self.assembler.tiles.get((y,x),None)
                if new is not None:
                    self.draw_poly(y,x,new,1, self.drawing.window, False)

        if not self.idle_enabled and not self.assembler.dirty:
            self.render_iterator = self.make_render_iter()

        return True


    def pos(self, x,y, center=True):
        result = (self.config.x_mapper*x + self.config.y_mapper*y) * (self.scale*2)
        if center: 
            return result + Point(self.width/2.0,self.height/2.0)
        else:
            return result
        
    def make_shape(self, form_number):
        if form_number in self.shapes:
            return self.shapes[form_number]
            
        result = [ ]
        connections = { }
        
        for i in xrange(len(self.assembler.connections)):
            yy, xx = self.assembler.connections[i][:2]
            symbol = self.assembler.forms[form_number][i]
            if symbol in ' -': continue
            
            edge = self.pos(xx,yy,0)
            out = edge
            left = out.left90()
        
            if symbol in 'aA1':
                r = 0.4
                #r = 0.6
            elif symbol in 'bB2': 
                r = 0.3
            elif symbol in 'cC3':
                r = 0.225
            else:
                r = 0.15
                        
            if symbol in 'ABCD': 
                poke = 0.3 #r
            elif symbol in 'abcd': 
                poke = -0.3 #-r
            else:
                poke = 0.0

            points = [
                edge + left*-r,
                edge + out*poke,
                edge + left*r,
            ]                        
            
            result.append( (out * (1.0/out.length()), points, 0.5)) #0.625))
            connections[i] = points
            # Note: set constant to ~0.35 for old-style circular look

        if len(result) == 1:
            point = result[0][0]*(self.scale*-0.7)
            result.append( (result[0][0].left90()*-1.0, [point], 0.8) )
            result.append( (result[0][0].left90(), [point], 0.8) )

        poly = [ ]
        for i in xrange(len(result)):
            a = result[i-1][1][-1]
            d = result[i][1][0]
            length = (d-a).length() * ((result[i][2]+result[i-1][2])*0.5)
            b = a - result[i-1][0]*length
            c = d - result[i][0]*length
            poly.extend(bezier(a,b,c,d))                 
            poly.extend(result[i][1])
            
        links = [ ]    
        if self.knot:
            form = self.assembler.forms[form_number]
            items = connections.keys()
            cords = [ ]
            
            if len(items)%2 != 0:
                for item in items[:]:
                    if (item+len(form)/2)   % len(form) not in items and \
                       (item+len(form)/2+1) % len(form) not in items and \
                       (item+len(form)/2-1) % len(form) not in items:
                        items.remove(item)
                        
            if len(items)%2 != 0:
                for i in xrange(len(form)):
                    if form[i] not in ' -' and \
                       form.count(form[i]) == 1 and \
                       (compatabilities[form[i]] == form[i] or \
                        form.count(compatabilities[form[i]])%2 == 0):
                        items.remove(i)
                        
            if len(items)%2 != 0:
                for item in items[:]:
                    if (item+len(form)/2) % len(form) not in items:
                        items.remove(item)
                        
            if len(items)%2 == 0:
                rot = self.assembler.rotation[form_number]
                mod = len(self.assembler.connections)
                items.sort(lambda a,b: cmp((a+rot)%mod,(b+rot)%mod))
                step = len(items)/2
              
                for ii in xrange(len(items)/2):
                    i = items[ii]
                    j = items[ii-step]
                    cords.append((i,j))
                    
            for i,j in cords:
                a = connections[i]
                b = connections[j]
                a_in = (a[-1]-a[0]).left90()
                a_in = a_in*(self.scale*1.25/a_in.length())
                b_in = (b[-1]-b[0]).left90()
                b_in = b_in*(self.scale*1.25/b_in.length())
                a = [(a[0]+a[1])*0.5,a[1],(a[-2]+a[-1])*0.5]
                b = [(b[0]+b[1])*0.5,b[1],(b[-2]+b[-1])*0.5]
                bez1 = bezier(a[-1],a[-1]+a_in,b[0]+b_in,b[0])
                bez2 = bezier(b[-1],b[-1]+b_in,a[0]+a_in,a[0])
                linker = a + bez1 + b + bez2
                links.append((linker,a[-1:]+bez1+b[:1],b[-1:]+bez2+a[:1]))
                
        self.shapes[form_number] = poly, links
        return poly, links
        
    def draw_poly(self, y,x,form_number,factor, drawable, erase=False):
        id = (y,x,form_number,factor)
            
        if id not in self.polys:
            def intify(points): return [ ((middle+point)*factor).int_xy() for point in points ]
        
            middle = self.pos(x*2,y*2)
            
            poly, links = self.make_shape(form_number)        
            poly = intify(poly)
            links = [ (intify(link), intify(line1), intify(line2)) for link,line1,line2 in links ]
            
            self.polys[id] = poly, links
        else:
            poly, links = self.polys[id]

        if len(poly) > 0:
            if erase:
                color = 0
            else:
                color = self.assembler.form_id[form_number] % (len(self.colors)-2) + 2  
                
            self.gc.set_rgb_fg_color(self.colors[color])
            drawable.draw_polygon(self.gc, True, poly)
            
            if self.knot:
                self.gc.set_line_attributes(max(factor,int(self.scale*factor * self.config.thickness)),
                     gtk.gdk.LINE_SOLID, gtk.gdk.CAP_ROUND, gtk.gdk.JOIN_ROUND)
                for link, line1, line2 in links:
                    if not erase: self.gc.set_rgb_fg_color(self.colors[1])
                    drawable.draw_polygon(self.gc, True, link)
                    if not erase: self.gc.set_rgb_fg_color(self.colors[color])
                    #drawable.draw_polygon(self.gc, False, link)
                    drawable.draw_lines(self.gc, line1)
                    drawable.draw_lines(self.gc, line2)
                            #drawable.draw_line(self.gc, *(connections[i][-1]+connections[j][0]))
                            #drawable.draw_line(self.gc, *(connections[j][-1]+connections[i][0]))

            if self.config.border:
                self.gc.set_line_attributes(max(factor,int(self.scale*factor * self.config.thickness)),
                     gtk.gdk.LINE_SOLID, gtk.gdk.CAP_ROUND, gtk.gdk.JOIN_ROUND)
                if not erase: self.gc.set_rgb_fg_color(self.colors[1])
                self.pixmap.draw_polygon(self.gc, False, poly)
                drawable.draw_polygon(self.gc, False, poly)
        
    def make_render_iter(self):
        yield None
        
        if not self.assembler.tiles:
            self.pixbuf_ready = False
            return
        
        self.gc.set_rgb_fg_color(self.colors[0])
        self.pixmap.draw_rectangle(self.gc, True, 0,0,self.width*factor,self.height*factor)
        
        if self.config.labels and False:
            font = pango.FontDescription("mono bold 36")
            self.gc.set_rgb_fg_color(self.colors[1])
        
            for i, form in enumerate(self.assembler.basic_forms):
                layout = self.drawing.create_pango_layout(" "+form.replace(" ","-")+" ")
                layout.set_font_description(font)
                x = (i+1)*(len(form)+3)*30
                y = (self.height-70) *factor
                width, height = layout.get_pixel_size()
                self.pixmap.draw_rectangle(self.gc, True, x-6,y-6, width+12,height+12)
                self.pixmap.draw_layout(self.gc, x,y, layout, self.colors[1], self.colors[i+2])            

        if self.config.grid:
            colormap = self.drawing.get_colormap()
            self.gc.set_rgb_fg_color(colormap.alloc_color("#eee"))
            self.gc.set_line_attributes(factor,
                     gtk.gdk.LINE_SOLID, gtk.gdk.CAP_ROUND, gtk.gdk.JOIN_ROUND)
            f = 4.0 / len(self.config.connections)
            for (y,x) in self.assembler.point_set.keys():
                poly = [ ]
                for i in xrange(len(self.config.connections)):
                    a = self.config.connections[i-1]
                    b = self.config.connections[i]
                    poly.append((self.pos(x*2+(a[0]+b[0])*f,y*2+(a[1]+b[1])*f)*factor).int_xy())
            
                self.pixmap.draw_polygon(self.gc, False, poly)
                yield None

        for (y,x), form_number in self.assembler.tiles.items():
            self.draw_poly(y,x,form_number,factor, self.pixmap)
            yield None
            
        self.pixbuf.get_from_drawable(self.pixmap, self.pixmap.get_colormap(), 
                                      0,0,0,0, self.width*factor,self.height*factor)
                                      
        yield None
            
        self.pixbuf.scale(self.scaled_pixbuf, 0,0,self.width,self.height,
              0,0,1.0/factor,1.0/factor,gtk.gdk.INTERP_BILINEAR)
        
        self.pixbuf_ready = True
        self.drawing.queue_draw()
    
    def on_expose(self, widget, event):
        self.assembler.changes = dict([ (item, None) for item in self.assembler.tiles ])

        if self.pixbuf_ready:
            self.drawing.window.draw_pixbuf(self.gc, self.scaled_pixbuf, 0,0,0,0,-1,-1)
        else:
            self.gc.set_rgb_fg_color(self.colors[0])
            self.drawing.window.draw_rectangle(self.gc, True, 0,0,self.width,self.height)

    def random(self, same_form=False):
        if same_form:
            n = len(self.assembler.basic_forms)
            sides = len(self.assembler.basic_forms[0])
        else:
            n = random.choice([1,1,2,2,2,3,3,3,4])
            if self.knot:
                sides = 6
            else:
                sides = random.choice([4,6])
        
                    
        while True:
            if self.knot:
                edge_counts = [ random.choice(range(2,sides+1,2)) 
                                for i in xrange(n) ]
            else:
                edge_counts = [ random.choice(range(1,sides+1)) 
                                for i in xrange(n) ]
                                
            edge_counts.sort()
            edge_counts.reverse()
            if edge_counts[0] != 1: break
        
        while True:
            try:
                result = [ ]
                previous = '1234' + 'aAbBcCdD' #* 3
                for edge_count in edge_counts:
                    item = [' ']*(sides-edge_count)
                    for j in xrange(edge_count): 
                        selection = random.choice(previous)
                        previous += compatabilities[selection]*6 #12
                        item.append(selection)
                
                    random.shuffle(item)
                    item = normalize(string.join(item,''))
                    if item in result: raise "repeat"
                    result.append(item)
            
                all = string.join(result,'')
                for a, b in compatabilities.items():
                    if a in all and b not in all: raise "repeat"
        
                break
            except "repeat":
                pass
        
        self.combo.entry.set_text(repr(result)[1:-1])
        self.reset()
        self.randomizing = True

    def on_save(self, widget):
        selecter = gtk.FileSelection('Save image')
        selecter.set_filename('diagram.png')
        
        def on_ok(widget):
            filename = selecter.get_filename()
            selecter.destroy()
            
            if self.pixbuf_ready:
                self.scaled_pixbuf.save(filename, 'png')
            else:
                pass
                # TODO: show error
            
        def on_cancel(widget):
            selecter.destroy()
            
        selecter.ok_button.connect('clicked', on_ok)
        selecter.cancel_button.connect('clicked', on_ok)
        
        selecter.show()



if __name__ == '__main__':
    Interface().run()
    sys.exit(0)

    # Just some phd stuff...
    interface = Interface()
    interface.window.show_all()
    
    base = 2
    chars = " 1"
    n = len(connections)
    done = { }
    
    for i in xrange(1, base ** n):
        result = ""
        for j in xrange(n):
            result += chars[(i / (base ** j)) % base]
        if normalize(result) in done or normalize(result.swapcase()) in done: continue
        print result
        done[normalize(result)] = True
        
        interface.combo.entry.set_text("'"+result+"', width=350, height=400")
        interface.reset()
    
        while gtk.events_pending():
            gtk.main_iteration()
            
        if interface.assembler.dirty:
            print "--- failed"
            continue
        
        interface.scaled_pixbuf.save("/tmp/T" + result.replace(" ","-") + ".png", "png")


--- NEW FILE ghost-diagrams.spec ---
Name:           ghost-diagrams
Version:        0.8
Release:        1%{?dist}
Summary:        A program that generates patterns from tiles

Group:          Amusements/Graphics
License:        GPLv2+
URL:            http://logarithmic.net/pfh/ghost-diagrams
Source0:        http://logarithmic.net/pfh-files/ghost-diagrams/%{name}-%{version}.py
BuildRoot:      %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
BuildArch:      noarch

BuildRequires:  desktop-file-utils
Requires:       pygtk2

%description
Ghost Diagrams is a program that takes sets of tiles and tries to find
patterns into which they may be formed. The patterns it finds when given
randomly chosen tiles are often surprising.

%prep

%build

%install
rm -rf $RPM_BUILD_ROOT
install -p -m 0644 -D %{SOURCE0} $RPM_BUILD_ROOT%{_datadir}/%{name}/%{name}.py
sed -i -e 1d $RPM_BUILD_ROOT%{_datadir}/%{name}/%{name}.py
mkdir -p $RPM_BUILD_ROOT%{_bindir}
cat << EOF > $RPM_BUILD_ROOT%{_bindir}/%{name}
#!/bin/sh
exec python %{_datadir}/%{name}/%{name}.py
EOF
mkdir -p $RPM_BUILD_ROOT%{_datadir}/applications
cat << EOF > $RPM_BUILD_ROOT%{_datadir}/applications/%{name}.desktop
[Desktop Entry]
Name=Ghost Diagrams
GenericName=Ghost Diagrams
Comment=Generate patterns from tiles
StartupNotify=true
Exec=%{_bindir}/%{name}
Type=Application
Terminal=false
Categories=Graphics;GTK;
EOF
desktop-file-install --vendor=fedora \
  --delete-original \
  --dir=$RPM_BUILD_ROOT%{_datadir}/applications \
  $RPM_BUILD_ROOT%{_datadir}/applications/%{name}.desktop

%clean
rm -rf $RPM_BUILD_ROOT

%files
%defattr(-,root,root,-)
%attr(0755, root, root) %{_bindir}/%{name}
%{_datadir}/%{name}
%{_datadir}/applications/*%{name}.desktop

%changelog
* Thu Jan 31 2008 Ignacio Vazquez-Abrams <ivazqueznet+rpm at gmail.com> 0.8-1
- Initial RPM release




More information about the fedora-extras-commits mailing list