HH_/ghost-diagrams-0.8.py

1113 rivejä
38 KiB
Python

#!/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")