Ce projet essai, en partant d'une IMAGESOURCE de : - appliquer des effets à l'image - la redimmensionner - changer son mode de couleur - la HAACHEER...
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ghost-diagrams-0.8.py 38KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112
  1. #!/usr/bin/env python
  2. # Copyright (C) 2004 Paul Harrison
  3. # This program is free software; you can redistribute it and/or modify
  4. # it under the terms of the GNU General Public License as published by
  5. # the Free Software Foundation; either version 2 of the License, or
  6. # (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with this program; if not, write to the Free Software
  15. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  16. """ Ghost Diagrams
  17. This program takes sets of tiles that connect together in certain
  18. ways, and looks for the patterns these tiles imply. The patterns
  19. are often surprising.
  20. This software is currently somewhat alpha.
  21. Tile set specification:
  22. A tile set specification is a list of 6- or 4-character strings,
  23. eg 'B Aa ', 'b Aa '
  24. Each character represents a tile edge. Letters (abcd, ABCD)
  25. match with their opposite case. Numbers match with themselves.
  26. A number of extra paramters can also be supplied:
  27. border : True/False : draw tile borders or not
  28. thickness : the thickness of the border
  29. width : minimum width of diagram
  30. height : minimum height of diagram
  31. colors : a list of colors
  32. [ background color, edge color, tile 1 color, tile 2 color... ]
  33. grid : True/False : draw a grid
  34. labels : True/False : draw labels for each tile under diagram
  35. eg 'B Aa ', 'b Aa ', width=1000, height=1000, thickness=0.5, colors=['#000','#000','#fff','#f00']
  36. Change log:
  37. 0.1 -- initial release
  38. 0.2 -- don't segfault on empty tiles
  39. 0.3 -- random keeps trying till it finds something that will grow
  40. optimization (options_cache)
  41. 0.4 -- assembly algorithm tweaks
  42. random tile set tweaks
  43. 0.5 -- Patch by Jeff Epler
  44. - allow window resizing
  45. - new connection types (33,44,cC,dD)
  46. - DNA tile set
  47. widget to set size of tiles
  48. no repeated tiles in random
  49. improvements to assembler
  50. 0.6 -- Use Bezier curves
  51. Parameters to set width, height, thickness, color
  52. Save images
  53. 0.7 -- Allow square tiles
  54. Smarter assembler
  55. Animate assembly
  56. 0.8 -- Knotwork
  57. Don't fill all of memory
  58. Use psyco if available
  59. TODO: don't backtrack areas outside current locus
  60. (difficulty: accidentally creating disconnected islands)
  61. TODO: (blue sky) 3D, third dimension == time
  62. TODO: allowances: 3 of this, 2 of that, etc.
  63. """
  64. try:
  65. import psyco
  66. psyco.profile()
  67. except:
  68. pass
  69. __version__ = '0.8'
  70. import sys, os, random, gtk, pango, gobject, string, math, sets
  71. class Point:
  72. def __init__(self, x,y):
  73. self.x = x
  74. self.y = y
  75. def __add__(self, other): return Point(self.x+other.x,self.y+other.y)
  76. def __sub__(self, other): return Point(self.x-other.x,self.y-other.y)
  77. def __mul__(self, factor): return Point(self.x*factor, self.y*factor)
  78. def length(self): return (self.y*self.y + self.x*self.x) ** 0.5
  79. def int_xy(self): return int(self.x+0.5), int(self.y+0.5)
  80. def left90(self): return Point(-self.y, self.x)
  81. # ========================================================================
  82. # Constants
  83. # Hexagonal connection pattern:
  84. #
  85. # o o
  86. # o * o
  87. # o o
  88. #
  89. # (all points remain on a square grid, but a regular hexagon pattern
  90. # can be formed by a simple linear transformation)
  91. connections_6 = [ (-1, 0, 3), (-1, 1, 4), (0, 1, 5), (1, 0, 0), (1, -1, 1), (0, -1, 2) ]
  92. # [ (y, x, index of reverse connection) ]
  93. x_mapper_6 = Point(1.0, 0.0)
  94. y_mapper_6 = Point(0.5, 0.75**0.5)
  95. connections_4 = [ (-1,0,2), (0,1,3), (1,0,0), (0,-1,1) ]
  96. x_mapper_4 = Point(1.0, 0.0)
  97. y_mapper_4 = Point(0.0, 1.0)
  98. # What edge type connects with what?
  99. # (a tile is represented as a string of 6 characters representing the 6 edges)
  100. compatabilities = {
  101. ' ':' ',
  102. 'A':'a', 'a':'A', 'B':'b', 'b':'B', 'c':'C', 'C':'c', 'd':'D', 'D':'d',
  103. '1':'1', '2':'2', '3':'3', '4':'4', '-':'-'
  104. }
  105. # Amount of oversampling in drawing
  106. factor = 3
  107. # Some cool tile sets people have found
  108. catalogue = [
  109. "' 33Aa', ' 33 Aa'",
  110. "' 33Aa/#000', ' 33 Aa/#000', colors=['#fff','#fff'], border=0, grid=0",
  111. "'ab A ', 'B C ', 'B c ', 'B D ', 'B d '",
  112. "'d D 4 ', 'd D ', '44 '",
  113. "'AaAa '",
  114. "'aA ', 'AaAa '",
  115. "' bB ', 'bAaB ', 'aAaA '",
  116. "'B Aa ', 'b Aa '",
  117. "'44 ', '11 4 '",
  118. "'3 3 3 ', '33 '",
  119. "'1 1 1 ', '2 12 '",
  120. "' a ', 'a AA '",
  121. "' AAaa ', 'a A '",
  122. "' a A ', 'Aaa A'",
  123. "'a a ', ' aAA A'",
  124. "' a A a', ' A '",
  125. "' AA A', 'a a a '",
  126. "'a aa ', ' AA'",
  127. "'A A a ', 'a a '",
  128. "'A A a ', 'a a '",
  129. "' a 4', 'a4 44A', '4A '",
  130. "'a2 A ', ' a2 A2'",
  131. "'a 2a2 ', ' A A', ' 2'",
  132. "'141 ', '4 4 ', '1 1 '",
  133. "' 22 22', '22 '",
  134. "' Aaa ', 'A1A ', 'a 1AAa'",
  135. "'aA a2 ', '2A ', ' 2 A'",
  136. "' bB1 ', ' b B '",
  137. "'BbB 1 ', ' b'",
  138. "'b b b', ' BbB '",
  139. "'aA1 ', ' AA ', 'a 2 '",
  140. "' a 4 ', ' 4 4 ', ' A441'",
  141. "'212111', ' 1 2 '",
  142. "'22222a', '22 A22'",
  143. "'2 222 ', '2 B2', ' b 2'",
  144. "' 21221', ' 221', ' 2 2'",
  145. "' a a a', ' A A'",
  146. "' Dd cA', ' d D', ' a C'",
  147. "' CCCc', ' 3Ca A', ' 3 c', ' c'",
  148. "' C dDc', ' CC C', ' ccC'",
  149. "' Aa Cc', ' c', ' C'",
  150. "' CcDdC', ' cC c', ' C'",
  151. "' CcCc', ' CcC c', ' c C'",
  152. "'A 1 1 ','a1 B','b 1 '",
  153. "'aa aa /#fff', 'AA /#fff', 'A A /#fff', grid=0, border=0, thickness=0.3",
  154. #"'bb bb ', 'BB ', 'B B '",
  155. "' 44B4D', ' dbB4b', ' 44D d', ' 44'",
  156. "' d3 3', ' D D'",
  157. "' cc c', ' C C c'",
  158. "'AaAaaa', ' 1 Aa', ' A'",
  159. "'d D 3 ', 'dD ', '3 '",
  160. "'a 1 A ', 'a A '",
  161. "'cCCcCC', 'cccC ', 'c C C'",
  162. "'A44444', 'a4 4', '4 4 '",
  163. "'acaACA', 'acbBCB', 'bcaBCB', 'bcbACA'",
  164. "'A ab ', 'B ab ', 'A a ', 'B b ', 'ABd D'", #Tree
  165. "'d AD ', ' a A ', 'a A ', 'aa A '", #Counter (?)
  166. "'bBbBBB', 'bb ', 'b B '",
  167. "'a AA A', 'a a '",
  168. "'cC a A', 'a A '",
  169. "'bbB B', 'b BBB ', 'bb '",
  170. "'cCc C ', 'cC c C'",
  171. "'d4 Dd ', 'd D ', 'DD '",
  172. "' 111'",
  173. "'abA ', 'B C ', 'B c ', 'B D ', 'B d '",
  174. "'4A4a', ' a4', ' A B', ' Ab'",
  175. "'acAC', 'adBD', 'bcBD', 'bdAC'",
  176. "'1111', ' 1'",
  177. "' bbb', ' BB'",
  178. "'1B1B', 'a A ', ' bA ', 'ab B'",
  179. ]
  180. default_colors=['#fff','#000', '#8ff', '#f44', '#aaf','#449', '#ff0088', '#ff4088', '#ff4040', '#ff00ff', '#40c0ff']
  181. default_thickness = 3.0/16
  182. # ========================================================================
  183. # Utility functions
  184. def bezier(a,b,c,d):
  185. result = [ ]
  186. n = 12
  187. for i in xrange(1,n):
  188. u = float(i) / n
  189. result.append(
  190. a * ((1-u)*(1-u)*(1-u)) +
  191. b * (3*u*(1-u)*(1-u)) +
  192. c * (3*u*u*(1-u)) +
  193. d * (u*u*u)
  194. )
  195. return result
  196. def normalize(form):
  197. best = form
  198. for i in xrange(len(form)-1):
  199. form = form[1:] + form[0]
  200. if form > best: best = form
  201. return best
  202. # =========================================================================
  203. class Config:
  204. def __init__(self, *forms, **kwargs):
  205. self.colors = kwargs.get("colors", [ ])
  206. self.colors += default_colors[len(self.colors):]
  207. self.border = kwargs.get("border", True)
  208. self.thickness = kwargs.get("thickness", default_thickness)
  209. self.width = kwargs.get("width", -1)
  210. self.height = kwargs.get("height", -1)
  211. self.grid = kwargs.get("grid", True)
  212. self.labels = kwargs.get("labels", False)
  213. forms = list(forms)
  214. if len(forms) < 1: raise "error"
  215. for item in forms:
  216. if type(item) != type(""):
  217. raise "error"
  218. for i in xrange(len(forms)):
  219. if "/" in forms[i]:
  220. forms[i], self.colors[i%(len(self.colors)-2)+2] = forms[i].split("/",1)
  221. if len(forms[0]) == 4:
  222. self.connections = connections_4
  223. self.x_mapper = x_mapper_4
  224. self.y_mapper = y_mapper_4
  225. else:
  226. self.connections = connections_6
  227. self.x_mapper = x_mapper_6
  228. self.y_mapper = y_mapper_6
  229. for item in forms:
  230. if len(item) != len(self.connections):
  231. raise "error"
  232. for edge in item:
  233. if edge not in compatabilities:
  234. raise "error"
  235. self.forms = forms
  236. # ========================================================================
  237. class Assembler:
  238. def __init__(self, connections, compatabilities, forms, point_set):
  239. self.connections = connections # [(y,x,index of reverse connection)]
  240. self.compatabilities = compatabilities # { edge-char -> edge-char }
  241. self.point_set = point_set # (y,x) -> True
  242. self.basic_forms = forms # ['edge types']
  243. self.forms = [ ] # ['edge types']
  244. self.form_id = [ ] # [original form number]
  245. self.rotation = [ ] # [rotation from original]
  246. for id, form in enumerate(forms):
  247. current = form
  248. for i in xrange(len(self.connections)):
  249. if current not in self.forms:
  250. self.forms.append(current)
  251. self.form_id.append(id)
  252. self.rotation.append(i)
  253. current = current[1:] + current[0]
  254. self.tiles = { } # (y,x) -> form number
  255. self.dirty = { } # (y,x) -> True -- Possible sites for adding tiles
  256. self.options_cache = { } # pattern -> [form_ids]
  257. self.dead_loci = sets.Set([ ]) # [ {(y,x)->form number} ]
  258. self.history = [ ]
  259. self.total_y = 0
  260. self.total_x = 0
  261. self.changes = { }
  262. def put(self, y,x, value):
  263. if (y,x) in self.changes:
  264. if value == self.changes[(y,x)]:
  265. del self.changes[(y,x)]
  266. else:
  267. self.changes[(y,x)] = self.tiles.get((y,x),None)
  268. if (y,x) in self.tiles:
  269. self.total_y -= y
  270. self.total_x -= x
  271. if value == None:
  272. if (y,x) not in self.tiles: return
  273. del self.tiles[(y,x)]
  274. self.dirty[(y,x)] = True
  275. else:
  276. self.tiles[(y,x)] = value
  277. self.total_y += y
  278. self.total_x += x
  279. for oy, ox, opposite in self.connections:
  280. y1 = y + oy
  281. x1 = x + ox
  282. if (y1,x1) not in self.tiles and (y1, x1) in self.point_set:
  283. self.dirty[(y1,x1)] = True
  284. def get_pattern(self, y,x):
  285. result = ''
  286. for oy, ox, opposite in self.connections:
  287. y1 = y + oy
  288. x1 = x + ox
  289. if self.tiles.has_key((y1,x1)):
  290. result += self.compatabilities[self.forms[self.tiles[(y1,x1)]][opposite]]
  291. #elif (y1,x1) not in self.point_set:
  292. # result += ' '
  293. else:
  294. result += '.'
  295. return result
  296. def fit_ok(self, pattern,form_number):
  297. form = self.forms[form_number]
  298. for i in xrange(len(self.connections)):
  299. if pattern[i] != '.' and pattern[i] != form[i]:
  300. return False
  301. return True
  302. def options(self, y,x):
  303. pattern = self.get_pattern(y,x)
  304. if pattern in self.options_cache:
  305. result = self.options_cache[pattern]
  306. result = [ ]
  307. for i in xrange(len(self.forms)):
  308. if self.fit_ok(pattern,i):
  309. result.append(i)
  310. result = tuple(result)
  311. self.options_cache[pattern] = result
  312. return result
  313. def locus(self, y,x, rotation=0):
  314. visited = { }
  315. neighbours = { }
  316. todo = [ ((y,x), (0,0)) ]
  317. result = [ ]
  318. min_y = 1<<30
  319. min_x = 1<<30
  320. while todo:
  321. current, offset = todo.pop(0)
  322. if current in visited: continue
  323. visited[current] = True
  324. any = False
  325. new_todo = [ ]
  326. for i, (oy, ox, opposite) in enumerate(self.connections):
  327. neighbour = (current[0]+oy, current[1]+ox)
  328. if neighbour in self.point_set:
  329. if neighbour in self.tiles:
  330. any = True
  331. neighbours[neighbour] = True
  332. min_y = min(min_y, offset[0])
  333. min_x = min(min_x, offset[1])
  334. result.append( (offset, opposite,
  335. self.forms[self.tiles[neighbour]][opposite]) )
  336. else:
  337. temp = self.connections[(i+rotation) % len(self.connections)]
  338. new_offset = (offset[0]+temp[0], offset[1]+temp[1])
  339. new_todo.append((neighbour, new_offset))
  340. if not any and len(self.connections) == 4:
  341. for oy, ox in ((-1,-1), (-1,1), (1,-1), (1,1)):
  342. neighbour = (current[0]+oy, current[1]+ox)
  343. if neighbour in self.tiles:
  344. any = True
  345. break
  346. if any:
  347. todo.extend(new_todo)
  348. result = [ (yy-min_y,xx-min_x,a,b) for ((yy,xx),a,b) in result ]
  349. return sets.ImmutableSet(result), visited, neighbours
  350. def filter_options(self, y,x,options):
  351. result = [ ]
  352. for i in options:
  353. self.tiles[(y,x)] = i
  354. visiteds = [ ]
  355. for oy, ox, oppoiste in self.connections:
  356. y1 = y+oy
  357. x1 = x+ox
  358. ok = True
  359. if (y1,x1) not in self.tiles and (y1,x1) in self.point_set:
  360. for visited in visiteds:
  361. if (y1,x1) in visited:
  362. ok = False
  363. break
  364. if ok:
  365. locus, visited, _ = self.locus(y1,x1)
  366. visiteds.append(visited)
  367. if locus is not None and locus in self.dead_loci:
  368. break
  369. else:
  370. result.append(i)
  371. del self.tiles[(y,x)]
  372. return result
  373. def any_links_to(self, y,x):
  374. for oy, ox, opposite in self.connections:
  375. y1 = y + oy
  376. x1 = x + ox
  377. if (y1, x1) in self.tiles:
  378. if self.forms[self.tiles[(y1,x1)]][opposite] != ' ':
  379. return True
  380. return False
  381. def prune_dead_loci(self):
  382. for item in list(self.dead_loci):
  383. if random.randrange(2):
  384. self.dead_loci.remove(item)
  385. def iterate(self):
  386. if not self.tiles:
  387. self.put(0,0,0)
  388. self.history.append((0,0))
  389. return True
  390. mid_y = 0.0
  391. mid_x = 0.0
  392. for y, x in self.dirty.keys():
  393. if (y,x) in self.tiles or not self.any_links_to(y,x):
  394. del self.dirty[(y,x)]
  395. continue
  396. mid_y += y
  397. mid_x += x
  398. if not self.dirty:
  399. return False
  400. mid_y /= len(self.dirty)
  401. mid_x /= len(self.dirty)
  402. point_list = [ ]
  403. for y, x in self.dirty.keys():
  404. yy = y - mid_y
  405. xx = x - mid_x
  406. sorter = ((yy*2)**2+(xx*2+yy)**2)
  407. point_list.append( (sorter,y,x) )
  408. point_list.sort()
  409. best = None
  410. for sorter, y, x in point_list:
  411. options = self.options(y,x)
  412. if len(options) < 2:
  413. score = 0
  414. else:
  415. score = 1
  416. if best == None or score < best_score:
  417. best = options
  418. best_score = score
  419. best_x = x
  420. best_y = y
  421. if score == 0: break
  422. if best == None: return False
  423. best = self.filter_options(best_y,best_x,best)
  424. if len(best) > 0:
  425. self.put(best_y,best_x,random.choice(best))
  426. self.history.append((best_y,best_x))
  427. return True
  428. #otherwise, backtrack:
  429. for i in xrange(len(self.connections)):
  430. locus, _, relevant = self.locus(best_y,best_x,i)
  431. if locus is None: break
  432. self.dead_loci.add(locus)
  433. if len(locus) > 8: break
  434. if len(self.dead_loci) > 10000:
  435. self.prune_dead_loci()
  436. # Shape of distribution
  437. autism = 1.0 # 1.0 == normal, >1.0 == autistic (just a theory :-) )
  438. # Overall level
  439. adhd = 2.0 # Lower == more adhd
  440. n = 1
  441. while n < len(self.tiles)-1 and random.random() < ( n/(n+autism) )**adhd:
  442. n += 1
  443. while self.history and (n > 0 or
  444. self.locus(best_y,best_x)[0] in self.dead_loci):
  445. item = self.history.pop(-1)
  446. self.put(item[0],item[1],None)
  447. n -= 1
  448. if not self.tiles:
  449. return False
  450. return True
  451. # ========================================================================
  452. class Interface:
  453. def __init__(self):
  454. self.iteration = 0
  455. self.idle_enabled = False
  456. self.drawing = gtk.DrawingArea()
  457. self.drawing.unset_flags(gtk.DOUBLE_BUFFERED)
  458. #self.drawing.add_events(gtk.gdk.BUTTON_PRESS_MASK)
  459. self.drawing.connect('expose-event', self.on_expose)
  460. self.drawing.connect('size-allocate', self.on_size)
  461. #self.drawing.connect('button-press-event', self.on_click)
  462. scroller = gtk.ScrolledWindow()
  463. scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
  464. scroller.add_with_viewport(self.drawing)
  465. self.combo = gtk.Combo()
  466. for item in catalogue:
  467. item = gtk.ListItem(item)
  468. item.get_children()[0].modify_font(pango.FontDescription("mono"))
  469. item.show()
  470. self.combo.list.append_items([item])
  471. self.combo.list.connect('select-child', lambda widget, child: self.reset())
  472. self.combo.entry.modify_font(pango.FontDescription("mono"))
  473. knot_box = gtk.CheckButton("knotwork")
  474. knot_box.set_active(False)
  475. self.knot = False
  476. knot_box.connect("toggled", self.on_knot_changed)
  477. scale_label = gtk.Label(' Size: ')
  478. scale = gtk.SpinButton()
  479. scale.set_digits(1)
  480. scale.set_increments(1.0,1.0)
  481. scale.set_range(3.0, 50.0)
  482. scale.set_value(10.0)
  483. scale.connect('value-changed', self.on_set_scale)
  484. random_button = gtk.Button(' Random ')
  485. random_button.connect('clicked', lambda widget: self.random())
  486. reset = gtk.Button(' Restart ')
  487. reset.connect('clicked', lambda widget: self.reset())
  488. save = gtk.Button(' Save image... ')
  489. save.connect('clicked', self.on_save)
  490. hbox = gtk.HBox(False,5)
  491. hbox.set_border_width(3)
  492. hbox.pack_start(knot_box, False, False, 0)
  493. hbox.pack_end(save, False,False,0)
  494. hbox.pack_end(reset, False,False,0)
  495. hbox.pack_end(random_button, False,False,0)
  496. hbox.pack_end(scale, False,False,0)
  497. hbox.pack_end(scale_label, False,False,0)
  498. vbox = gtk.VBox(False,5)
  499. vbox.pack_start(self.combo, False,False,0)
  500. vbox.pack_start(hbox, False,False,0)
  501. vbox.pack_start(scroller, True,True,0)
  502. self.window = gtk.Window()
  503. self.window.set_default_size(600,650)
  504. #self.window.set_default_size(200,200)
  505. self.window.set_title('Ghost Diagrams')
  506. self.window.add(vbox)
  507. self.window.connect('destroy', lambda widget: gtk.main_quit())
  508. self.drawing.realize()
  509. self.gc = gtk.gdk.GC(self.drawing.window)
  510. self.pixbuf_ready = False
  511. self.render_iterator = None
  512. self.randomizing = False
  513. self.set_scale(scale.get_value())
  514. def on_size(self, widget, event):
  515. self.pixbuf_ready = False
  516. self.width = event.width
  517. self.height = event.height
  518. self.pixmap = gtk.gdk.Pixmap(self.drawing.window, self.width*factor,self.height*factor)
  519. self.pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8,
  520. self.width*factor,self.height*factor)
  521. self.scaled_pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8,
  522. self.width,self.height)
  523. self.reset()
  524. def on_set_scale(self, widget):
  525. self.set_scale(widget.get_value())
  526. self.reset()
  527. def set_scale(self, value):
  528. self.scale = value
  529. def on_knot_changed(self, widget):
  530. self.knot = widget.get_active()
  531. self.reset()
  532. def reset(self):
  533. try:
  534. self.config = eval('Config('+self.combo.entry.get_text()+')')
  535. except:
  536. import traceback
  537. traceback.print_exc()
  538. self.config = Config(" ", grid=False, colors=["#f66"])
  539. colormap = self.drawing.get_colormap()
  540. self.colors = [ colormap.alloc_color(item) for item in self.config.colors ]
  541. point_set = { }
  542. yr = int( self.height/self.scale/4 )
  543. xr = int( self.width/self.scale/4 )
  544. if self.config.labels:
  545. bound = self.scale * 3
  546. for y in xrange(-yr,yr):
  547. for x in xrange(-yr,xr):
  548. point = self.pos(x*2,y*2)
  549. if point.x > bound and point.x < self.width-bound and \
  550. point.y > bound and point.y < self.height-bound-90:
  551. point_set[(y,x)] = True
  552. else:
  553. bound = self.scale * 3
  554. for y in xrange(-yr,yr):
  555. for x in xrange(-yr,xr):
  556. point = self.pos(x*2,y*2)
  557. if point.x > -bound and point.x < self.width+bound and \
  558. point.y > -bound and point.y < self.height+bound:
  559. point_set[(y,x)] = True
  560. self.pixbuf_ready = False
  561. self.render_iterator = None
  562. self.randomizing = False
  563. self.iteration = 0
  564. self.drawing.queue_draw()
  565. self.shapes = { }
  566. self.polys = { }
  567. self.assembler = Assembler(self.config.connections, compatabilities,
  568. self.config.forms, point_set)
  569. if not self.idle_enabled:
  570. gobject.idle_add(self.idle)
  571. self.idle_enabled = True
  572. self.drawing.set_size_request(self.config.width, self.config.height)
  573. self.drawing.queue_draw()
  574. def run(self):
  575. self.window.show_all()
  576. gtk.main()
  577. def idle(self):
  578. if self.render_iterator:
  579. try:
  580. self.render_iterator.next()
  581. except StopIteration:
  582. self.render_iterator = None
  583. return True
  584. if not self.idle_enabled:
  585. return False
  586. self.idle_enabled = self.assembler.iterate()
  587. self.iteration += 1
  588. if self.randomizing and \
  589. (self.iteration == 100 or not self.idle_enabled):
  590. self.randomizing = False
  591. forms_present = { }
  592. for item in self.assembler.tiles.values():
  593. forms_present[self.assembler.form_id[item]] = 1
  594. if len(self.assembler.tiles) < 10 \
  595. or len(forms_present) < len(self.assembler.basic_forms):
  596. self.idle_enabled = False
  597. self.random(True)
  598. return False
  599. if not self.idle_enabled or len(self.assembler.changes) >= 8:
  600. changes = self.assembler.changes
  601. self.assembler.changes = { }
  602. for y,x in changes:
  603. old = changes[(y,x)]
  604. if old is not None:
  605. self.draw_poly(y,x,old,1, self.drawing.window, True)
  606. for y,x in changes:
  607. new = self.assembler.tiles.get((y,x),None)
  608. if new is not None:
  609. self.draw_poly(y,x,new,1, self.drawing.window, False)
  610. if not self.idle_enabled and not self.assembler.dirty:
  611. self.render_iterator = self.make_render_iter()
  612. return True
  613. def pos(self, x,y, center=True):
  614. result = (self.config.x_mapper*x + self.config.y_mapper*y) * (self.scale*2)
  615. if center:
  616. return result + Point(self.width/2.0,self.height/2.0)
  617. else:
  618. return result
  619. def make_shape(self, form_number):
  620. if form_number in self.shapes:
  621. return self.shapes[form_number]
  622. result = [ ]
  623. connections = { }
  624. for i in xrange(len(self.assembler.connections)):
  625. yy, xx = self.assembler.connections[i][:2]
  626. symbol = self.assembler.forms[form_number][i]
  627. if symbol in ' -': continue
  628. edge = self.pos(xx,yy,0)
  629. out = edge
  630. left = out.left90()
  631. if symbol in 'aA1':
  632. r = 0.4
  633. #r = 0.6
  634. elif symbol in 'bB2':
  635. r = 0.3
  636. elif symbol in 'cC3':
  637. r = 0.225
  638. else:
  639. r = 0.15
  640. if symbol in 'ABCD':
  641. poke = 0.3 #r
  642. elif symbol in 'abcd':
  643. poke = -0.3 #-r
  644. else:
  645. poke = 0.0
  646. points = [
  647. edge + left*-r,
  648. edge + out*poke,
  649. edge + left*r,
  650. ]
  651. result.append( (out * (1.0/out.length()), points, 0.5)) #0.625))
  652. connections[i] = points
  653. # Note: set constant to ~0.35 for old-style circular look
  654. if len(result) == 1:
  655. point = result[0][0]*(self.scale*-0.7)
  656. result.append( (result[0][0].left90()*-1.0, [point], 0.8) )
  657. result.append( (result[0][0].left90(), [point], 0.8) )
  658. poly = [ ]
  659. for i in xrange(len(result)):
  660. a = result[i-1][1][-1]
  661. d = result[i][1][0]
  662. length = (d-a).length() * ((result[i][2]+result[i-1][2])*0.5)
  663. b = a - result[i-1][0]*length
  664. c = d - result[i][0]*length
  665. poly.extend(bezier(a,b,c,d))
  666. poly.extend(result[i][1])
  667. links = [ ]
  668. if self.knot:
  669. form = self.assembler.forms[form_number]
  670. items = connections.keys()
  671. cords = [ ]
  672. if len(items)%2 != 0:
  673. for item in items[:]:
  674. if (item+len(form)/2) % len(form) not in items and \
  675. (item+len(form)/2+1) % len(form) not in items and \
  676. (item+len(form)/2-1) % len(form) not in items:
  677. items.remove(item)
  678. if len(items)%2 != 0:
  679. for i in xrange(len(form)):
  680. if form[i] not in ' -' and \
  681. form.count(form[i]) == 1 and \
  682. (compatabilities[form[i]] == form[i] or \
  683. form.count(compatabilities[form[i]])%2 == 0):
  684. items.remove(i)
  685. if len(items)%2 != 0:
  686. for item in items[:]:
  687. if (item+len(form)/2) % len(form) not in items:
  688. items.remove(item)
  689. if len(items)%2 == 0:
  690. rot = self.assembler.rotation[form_number]
  691. mod = len(self.assembler.connections)
  692. items.sort(lambda a,b: cmp((a+rot)%mod,(b+rot)%mod))
  693. step = len(items)/2
  694. for ii in xrange(len(items)/2):
  695. i = items[ii]
  696. j = items[ii-step]
  697. cords.append((i,j))
  698. for i,j in cords:
  699. a = connections[i]
  700. b = connections[j]
  701. a_in = (a[-1]-a[0]).left90()
  702. a_in = a_in*(self.scale*1.25/a_in.length())
  703. b_in = (b[-1]-b[0]).left90()
  704. b_in = b_in*(self.scale*1.25/b_in.length())
  705. a = [(a[0]+a[1])*0.5,a[1],(a[-2]+a[-1])*0.5]
  706. b = [(b[0]+b[1])*0.5,b[1],(b[-2]+b[-1])*0.5]
  707. bez1 = bezier(a[-1],a[-1]+a_in,b[0]+b_in,b[0])
  708. bez2 = bezier(b[-1],b[-1]+b_in,a[0]+a_in,a[0])
  709. linker = a + bez1 + b + bez2
  710. links.append((linker,a[-1:]+bez1+b[:1],b[-1:]+bez2+a[:1]))
  711. self.shapes[form_number] = poly, links
  712. return poly, links
  713. def draw_poly(self, y,x,form_number,factor, drawable, erase=False):
  714. id = (y,x,form_number,factor)
  715. if id not in self.polys:
  716. def intify(points): return [ ((middle+point)*factor).int_xy() for point in points ]
  717. middle = self.pos(x*2,y*2)
  718. poly, links = self.make_shape(form_number)
  719. poly = intify(poly)
  720. links = [ (intify(link), intify(line1), intify(line2)) for link,line1,line2 in links ]
  721. self.polys[id] = poly, links
  722. else:
  723. poly, links = self.polys[id]
  724. if len(poly) > 0:
  725. if erase:
  726. color = 0
  727. else:
  728. color = self.assembler.form_id[form_number] % (len(self.colors)-2) + 2
  729. self.gc.set_rgb_fg_color(self.colors[color])
  730. drawable.draw_polygon(self.gc, True, poly)
  731. if self.knot:
  732. self.gc.set_line_attributes(max(factor,int(self.scale*factor * self.config.thickness)),
  733. gtk.gdk.LINE_SOLID, gtk.gdk.CAP_ROUND, gtk.gdk.JOIN_ROUND)
  734. for link, line1, line2 in links:
  735. if not erase: self.gc.set_rgb_fg_color(self.colors[1])
  736. drawable.draw_polygon(self.gc, True, link)
  737. if not erase: self.gc.set_rgb_fg_color(self.colors[color])
  738. #drawable.draw_polygon(self.gc, False, link)
  739. drawable.draw_lines(self.gc, line1)
  740. drawable.draw_lines(self.gc, line2)
  741. #drawable.draw_line(self.gc, *(connections[i][-1]+connections[j][0]))
  742. #drawable.draw_line(self.gc, *(connections[j][-1]+connections[i][0]))
  743. if self.config.border:
  744. self.gc.set_line_attributes(max(factor,int(self.scale*factor * self.config.thickness)),
  745. gtk.gdk.LINE_SOLID, gtk.gdk.CAP_ROUND, gtk.gdk.JOIN_ROUND)
  746. if not erase: self.gc.set_rgb_fg_color(self.colors[1])
  747. self.pixmap.draw_polygon(self.gc, False, poly)
  748. drawable.draw_polygon(self.gc, False, poly)
  749. def make_render_iter(self):
  750. yield None
  751. if not self.assembler.tiles:
  752. self.pixbuf_ready = False
  753. return
  754. self.gc.set_rgb_fg_color(self.colors[0])
  755. self.pixmap.draw_rectangle(self.gc, True, 0,0,self.width*factor,self.height*factor)
  756. if self.config.labels and False:
  757. font = pango.FontDescription("mono bold 36")
  758. self.gc.set_rgb_fg_color(self.colors[1])
  759. for i, form in enumerate(self.assembler.basic_forms):
  760. layout = self.drawing.create_pango_layout(" "+form.replace(" ","-")+" ")
  761. layout.set_font_description(font)
  762. x = (i+1)*(len(form)+3)*30
  763. y = (self.height-70) *factor
  764. width, height = layout.get_pixel_size()
  765. self.pixmap.draw_rectangle(self.gc, True, x-6,y-6, width+12,height+12)
  766. self.pixmap.draw_layout(self.gc, x,y, layout, self.colors[1], self.colors[i+2])
  767. if self.config.grid:
  768. colormap = self.drawing.get_colormap()
  769. self.gc.set_rgb_fg_color(colormap.alloc_color("#eee"))
  770. self.gc.set_line_attributes(factor,
  771. gtk.gdk.LINE_SOLID, gtk.gdk.CAP_ROUND, gtk.gdk.JOIN_ROUND)
  772. f = 4.0 / len(self.config.connections)
  773. for (y,x) in self.assembler.point_set.keys():
  774. poly = [ ]
  775. for i in xrange(len(self.config.connections)):
  776. a = self.config.connections[i-1]
  777. b = self.config.connections[i]
  778. poly.append((self.pos(x*2+(a[0]+b[0])*f,y*2+(a[1]+b[1])*f)*factor).int_xy())
  779. self.pixmap.draw_polygon(self.gc, False, poly)
  780. yield None
  781. for (y,x), form_number in self.assembler.tiles.items():
  782. self.draw_poly(y,x,form_number,factor, self.pixmap)
  783. yield None
  784. self.pixbuf.get_from_drawable(self.pixmap, self.pixmap.get_colormap(),
  785. 0,0,0,0, self.width*factor,self.height*factor)
  786. yield None
  787. self.pixbuf.scale(self.scaled_pixbuf, 0,0,self.width,self.height,
  788. 0,0,1.0/factor,1.0/factor,gtk.gdk.INTERP_BILINEAR)
  789. self.pixbuf_ready = True
  790. self.drawing.queue_draw()
  791. def on_expose(self, widget, event):
  792. self.assembler.changes = dict([ (item, None) for item in self.assembler.tiles ])
  793. if self.pixbuf_ready:
  794. self.drawing.window.draw_pixbuf(self.gc, self.scaled_pixbuf, 0,0,0,0,-1,-1)
  795. else:
  796. self.gc.set_rgb_fg_color(self.colors[0])
  797. self.drawing.window.draw_rectangle(self.gc, True, 0,0,self.width,self.height)
  798. def random(self, same_form=False):
  799. if same_form:
  800. n = len(self.assembler.basic_forms)
  801. sides = len(self.assembler.basic_forms[0])
  802. else:
  803. n = random.choice([1,1,2,2,2,3,3,3,4])
  804. if self.knot:
  805. sides = 6
  806. else:
  807. sides = random.choice([4,6])
  808. while True:
  809. if self.knot:
  810. edge_counts = [ random.choice(range(2,sides+1,2))
  811. for i in xrange(n) ]
  812. else:
  813. edge_counts = [ random.choice(range(1,sides+1))
  814. for i in xrange(n) ]
  815. edge_counts.sort()
  816. edge_counts.reverse()
  817. if edge_counts[0] != 1: break
  818. while True:
  819. try:
  820. result = [ ]
  821. previous = '1234' + 'aAbBcCdD' #* 3
  822. for edge_count in edge_counts:
  823. item = [' ']*(sides-edge_count)
  824. for j in xrange(edge_count):
  825. selection = random.choice(previous)
  826. previous += compatabilities[selection]*6 #12
  827. item.append(selection)
  828. random.shuffle(item)
  829. item = normalize(string.join(item,''))
  830. if item in result: raise "repeat"
  831. result.append(item)
  832. all = string.join(result,'')
  833. for a, b in compatabilities.items():
  834. if a in all and b not in all: raise "repeat"
  835. break
  836. except "repeat":
  837. pass
  838. self.combo.entry.set_text(repr(result)[1:-1])
  839. self.reset()
  840. self.randomizing = True
  841. def on_save(self, widget):
  842. selecter = gtk.FileSelection('Save image')
  843. selecter.set_filename('diagram.png')
  844. def on_ok(widget):
  845. filename = selecter.get_filename()
  846. selecter.destroy()
  847. if self.pixbuf_ready:
  848. self.scaled_pixbuf.save(filename, 'png')
  849. else:
  850. pass
  851. # TODO: show error
  852. def on_cancel(widget):
  853. selecter.destroy()
  854. selecter.ok_button.connect('clicked', on_ok)
  855. selecter.cancel_button.connect('clicked', on_ok)
  856. selecter.show()
  857. if __name__ == '__main__':
  858. Interface().run()
  859. sys.exit(0)
  860. # Just some phd stuff...
  861. interface = Interface()
  862. interface.window.show_all()
  863. base = 2
  864. chars = " 1"
  865. n = len(connections)
  866. done = { }
  867. for i in xrange(1, base ** n):
  868. result = ""
  869. for j in xrange(n):
  870. result += chars[(i / (base ** j)) % base]
  871. if normalize(result) in done or normalize(result.swapcase()) in done: continue
  872. print result
  873. done[normalize(result)] = True
  874. interface.combo.entry.set_text("'"+result+"', width=350, height=400")
  875. interface.reset()
  876. while gtk.events_pending():
  877. gtk.main_iteration()
  878. if interface.assembler.dirty:
  879. print "--- failed"
  880. continue
  881. interface.scaled_pixbuf.save("/tmp/T" + result.replace(" ","-") + ".png", "png")