1113 行
38 KiB
Python
1113 行
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")
|