shapes.py 75 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131
  1. from numpy import linspace, sin, cos, pi, array, asarray, ndarray, sqrt, abs
  2. import pprint, copy, glob, os
  3. from math import radians
  4. from MatplotlibDraw import MatplotlibDraw
  5. drawing_tool = MatplotlibDraw()
  6. def point(x, y, check_inside=False):
  7. if isinstance(x, (float,int)) and isinstance(y, (float,int)):
  8. pass
  9. else:
  10. raise TypeError('x=%s,y=%s must be float,float, not %s,%s' %
  11. (x, y, type(x), type(y)))
  12. if check_inside:
  13. ok, msg = drawing_tool.inside((x,y), exception=True)
  14. if not ok:
  15. print msg
  16. return array((x, y), dtype=float)
  17. def distance(p1, p2):
  18. p1 = arr2D(p1); p2 = arr2D(p2)
  19. d = p2 - p1
  20. return sqrt(d[0]**2 + d[1]**2)
  21. def unit_vec(x, y=None):
  22. """Return unit vector of the vector (x,y), or just x if x is a 2D point."""
  23. if isinstance(x, (float,int)) and isinstance(y, (float,int)):
  24. x = point(x, y)
  25. elif isinstance(x, (list,tuple,ndarray)) and y is None:
  26. return arr2D(x)/sqrt(x[0]**2 + x[1]**2)
  27. else:
  28. raise TypeError('x=%s is %s, must be float or ndarray 2D point' %
  29. (x, type(x)))
  30. def arr2D(x, check_inside=False):
  31. if isinstance(x, (tuple,list,ndarray)):
  32. if len(x) == 2:
  33. pass
  34. else:
  35. raise ValueError('x=%s has length %d, not 2' % (x, len(x)))
  36. else:
  37. raise TypeError('x=%s must be list/tuple/ndarray, not %s' %
  38. (x, type(x)))
  39. if check_inside:
  40. ok, msg = drawing_tool.inside(x, exception=True)
  41. if not ok:
  42. print msg
  43. return asarray(x, dtype=float)
  44. def _is_sequence(seq, length=None,
  45. can_be_None=False, error_message=True):
  46. if can_be_None:
  47. legal_types = (list,tuple,ndarray,None)
  48. else:
  49. legal_types = (list,tuple,ndarray)
  50. if isinstance(seq, legal_types):
  51. if length is not None:
  52. if length == len(seq):
  53. return True
  54. elif error_message:
  55. raise TypeError('%s is %s; must be %s of length %d' %
  56. (str(seq), type(seq),
  57. ', '.join([str(t) for t in legal_types]),
  58. len(seq)))
  59. else:
  60. return False
  61. else:
  62. return True
  63. elif error_message:
  64. raise TypeError('%s is %s, %s; must be %s' %
  65. (str(seq), seq.__class__.__name__, type(seq),
  66. ','.join([str(t)[5:-1] for t in legal_types])))
  67. else:
  68. return False
  69. def is_sequence(*sequences, **kwargs):
  70. length = kwargs.get('length', 2)
  71. can_be_None = kwargs.get('can_be_None', False)
  72. error_message = kwargs.get('error_message', True)
  73. check_inside = kwargs.get('check_inside', False)
  74. for x in sequences:
  75. _is_sequence(x, length=length, can_be_None=can_be_None,
  76. error_message=error_message)
  77. if check_inside:
  78. ok, msg = drawing_tool.inside(x, exception=True)
  79. if not ok:
  80. print msg
  81. def animate(fig, time_points, action, moviefiles=False,
  82. pause_per_frame=0.5, **action_kwargs):
  83. if moviefiles:
  84. # Clean up old frame files
  85. framefilestem = 'tmp_frame_'
  86. framefiles = glob.glob('%s*.png' % framefilestem)
  87. for framefile in framefiles:
  88. os.remove(framefile)
  89. for n, t in enumerate(time_points):
  90. drawing_tool.erase()
  91. action(t, fig, **action_kwargs)
  92. #could demand returning fig, but in-place modifications
  93. #are done anyway
  94. #fig = action(t, fig)
  95. #if fig is None:
  96. # raise TypeError(
  97. # 'animate: action returns None, not fig\n'
  98. # '(a Shape object with the whole figure)')
  99. fig.draw()
  100. drawing_tool.display()
  101. if moviefiles:
  102. drawing_tool.savefig('%s%04d.png' % (framefilestem, n))
  103. if moviefiles:
  104. return '%s*.png' % framefilestem
  105. class Shape:
  106. """
  107. Superclass for drawing different geometric shapes.
  108. Subclasses define shapes, but drawing, rotation, translation,
  109. etc. are done in generic functions in this superclass.
  110. """
  111. def __init__(self):
  112. """
  113. Until new version of shapes.py is ready:
  114. Never to be called from subclasses.
  115. """
  116. raise NotImplementedError(
  117. 'class %s must implement __init__,\nwhich defines '
  118. 'self.shapes as a list of Shape objects\n'
  119. '(and preferably self._repr string).\n'
  120. 'Do not call Shape.__init__!' % \
  121. self.__class__.__name__)
  122. def __iter__(self):
  123. # We iterate over self.shapes many places, and will
  124. # get here if self.shapes is just a Shape object and
  125. # not the assumed list.
  126. print 'Warning: class %s does not define self.shapes\n'\
  127. 'as a *list* of Shape objects'
  128. return [self] # Make the iteration work
  129. def copy(self):
  130. return copy.deepcopy(self)
  131. def __getitem__(self, name):
  132. """
  133. Allow indexing like::
  134. obj1['name1']['name2']
  135. all the way down to ``Curve`` or ``Point`` (``Text``)
  136. objects.
  137. """
  138. if hasattr(self, 'shapes'):
  139. if name in self.shapes:
  140. return self.shapes[name]
  141. else:
  142. for shape in self.shapes:
  143. if isinstance(self.shapes[shape], (Curve,Point)):
  144. # Indexing of Curve/Point/Text is not possible
  145. raise TypeError(
  146. 'Index "%s" (%s) is illegal' %
  147. (name, self.__class__.__name__))
  148. return self.shapes[shape][name]
  149. else:
  150. raise Exception('This is a bug')
  151. def _for_all_shapes(self, func, *args, **kwargs):
  152. if not hasattr(self, 'shapes'):
  153. # When self.shapes is lacking, we either come to
  154. # a special implementation of func or we come here
  155. # because Shape.func is just inherited. This is
  156. # an error if the class is not Curve or Point
  157. if isinstance(self, (Curve, Point)):
  158. return # ok: no shapes and no func
  159. else:
  160. raise AttributeError('class %s has no shapes attribute!' %
  161. self.__class__.__name__)
  162. is_dict = True if isinstance(self.shapes, dict) else False
  163. for k, shape in enumerate(self.shapes):
  164. if is_dict:
  165. shape_name = shape
  166. shape = self.shapes[shape]
  167. else:
  168. shape_name = k
  169. if not isinstance(shape, Shape):
  170. if isinstance(shape, dict):
  171. raise TypeError(
  172. 'class %s has a self.shapes member "%s" that is just\n'
  173. 'a plain dictionary,\n%s\n'
  174. 'Did you mean to embed this dict in a Composition\n'
  175. 'object?' % (self.__class__.__name__, shape_name,
  176. str(shape)))
  177. elif isinstance(shape, (list,tuple)):
  178. raise TypeError(
  179. 'class %s has self.shapes member "%s" containing\n'
  180. 'a %s object %s,\n'
  181. 'Did you mean to embed this list in a Composition\n'
  182. 'object?' % (self.__class__.__name__, shape_name,
  183. type(shape), str(shape)))
  184. elif shape is None:
  185. raise TypeError(
  186. 'class %s has a self.shapes member "%s" that is None.\n'
  187. 'Some variable name is wrong, or some function\n'
  188. 'did not return the right object...' \
  189. % (self.__class__.__name__, shape_name))
  190. else:
  191. raise TypeError(
  192. 'class %s has a self.shapes member "%s" of %s which '
  193. 'is not a Shape object\n%s' %
  194. (self.__class__.__name__, shape_name, type(shape),
  195. pprint.pformat(self.shapes)))
  196. getattr(shape, func)(*args, **kwargs)
  197. def draw(self):
  198. self._for_all_shapes('draw')
  199. return self
  200. def draw_dimensions(self):
  201. if hasattr(self, 'dimensions'):
  202. for shape in self.dimensions:
  203. self.dimensions[shape].draw()
  204. return self
  205. else:
  206. #raise AttributeError('no self.dimensions dict for defining dimensions of class %s' % self.__classname__.__name__)
  207. return self
  208. def rotate(self, angle, center):
  209. is_sequence(center, length=2)
  210. self._for_all_shapes('rotate', angle, center)
  211. return self
  212. def translate(self, vec):
  213. is_sequence(vec, length=2)
  214. self._for_all_shapes('translate', vec)
  215. return self
  216. def scale(self, factor):
  217. self._for_all_shapes('scale', factor)
  218. return self
  219. def deform(self, displacement_function):
  220. self._for_all_shapes('deform', displacement_function)
  221. return self
  222. def minmax_coordinates(self, minmax=None):
  223. if minmax is None:
  224. minmax = {'xmin': 1E+20, 'xmax': -1E+20,
  225. 'ymin': 1E+20, 'ymax': -1E+20}
  226. self._for_all_shapes('minmax_coordinates', minmax)
  227. return minmax
  228. def recurse(self, name, indent=0):
  229. if not isinstance(self.shapes, dict):
  230. raise TypeError('recurse works only with dict self.shape, not %s' %
  231. type(self.shapes))
  232. space = ' '*indent
  233. print space, '%s: %s.shapes has entries' % \
  234. (self.__class__.__name__, name), \
  235. str(list(self.shapes.keys()))[1:-1]
  236. for shape in self.shapes:
  237. print space,
  238. print 'call %s.shapes["%s"].recurse("%s", %d)' % \
  239. (name, shape, shape, indent+2)
  240. self.shapes[shape].recurse(shape, indent+2)
  241. def graphviz_dot(self, name, classname=True):
  242. if not isinstance(self.shapes, dict):
  243. raise TypeError('recurse works only with dict self.shape, not %s' %
  244. type(self.shapes))
  245. dotfile = name + '.dot'
  246. pngfile = name + '.png'
  247. if classname:
  248. name = r"%s:\n%s" % (self.__class__.__name__, name)
  249. couplings = self._object_couplings(name, classname=classname)
  250. # Insert counter for similar names
  251. from collections import defaultdict
  252. count = defaultdict(lambda: 0)
  253. couplings2 = []
  254. for i in range(len(couplings)):
  255. parent, child = couplings[i]
  256. count[child] += 1
  257. parent += ' (%d)' % count[parent]
  258. child += ' (%d)' % count[child]
  259. couplings2.append((parent, child))
  260. print 'graphviz', couplings, count
  261. # Remove counter for names there are only one of
  262. for i in range(len(couplings)):
  263. parent2, child2 = couplings2[i]
  264. parent, child = couplings[i]
  265. if count[parent] > 1:
  266. parent = parent2
  267. if count[child] > 1:
  268. child = child2
  269. couplings[i] = (parent, child)
  270. print couplings
  271. f = open(dotfile, 'w')
  272. f.write('digraph G {\n')
  273. for parent, child in couplings:
  274. f.write('"%s" -> "%s";\n' % (parent, child))
  275. f.write('}\n')
  276. f.close()
  277. print 'Run dot -Tpng -o %s %s' % (pngfile, dotfile)
  278. def _object_couplings(self, parent, couplings=[], classname=True):
  279. """Find all couplings of parent and child objects in a figure."""
  280. for shape in self.shapes:
  281. if classname:
  282. childname = r"%s:\n%s" % \
  283. (self.shapes[shape].__class__.__name__, shape)
  284. else:
  285. childname = shape
  286. couplings.append((parent, childname))
  287. self.shapes[shape]._object_couplings(childname, couplings,
  288. classname)
  289. return couplings
  290. def set_linestyle(self, style):
  291. styles = ('solid', 'dashed', 'dashdot', 'dotted')
  292. if style not in styles:
  293. raise ValueError('%s: style=%s must be in %s' %
  294. (self.__class__.__name__ + '.set_linestyle:',
  295. style, str(styles)))
  296. self._for_all_shapes('set_linestyle', style)
  297. return self
  298. def set_linewidth(self, width):
  299. if not isinstance(width, int) and width >= 0:
  300. raise ValueError('%s: width=%s must be positive integer' %
  301. (self.__class__.__name__ + '.set_linewidth:',
  302. width))
  303. self._for_all_shapes('set_linewidth', width)
  304. return self
  305. def set_linecolor(self, color):
  306. if color in drawing_tool.line_colors:
  307. color = drawing_tool.line_colors[color]
  308. elif color in drawing_tool.line_colors.values():
  309. pass # color is ok
  310. else:
  311. raise ValueError('%s: invalid color "%s", must be in %s' %
  312. (self.__class__.__name__ + '.set_linecolor:',
  313. color, list(drawing_tool.line_colors.keys())))
  314. self._for_all_shapes('set_linecolor', color)
  315. return self
  316. def set_arrow(self, style):
  317. styles = ('->', '<-', '<->')
  318. if not style in styles:
  319. raise ValueError('%s: style=%s must be in %s' %
  320. (self.__class__.__name__ + '.set_arrow:',
  321. style, styles))
  322. self._for_all_shapes('set_arrow', style)
  323. return self
  324. def set_filled_curves(self, color='', pattern=''):
  325. if color in drawing_tool.line_colors:
  326. color = drawing_tool.line_colors[color]
  327. elif color in drawing_tool.line_colors.values():
  328. pass # color is ok
  329. else:
  330. raise ValueError('%s: invalid color "%s", must be in %s' %
  331. (self.__class__.__name__ + '.set_filled_curves:',
  332. color, list(drawing_tool.line_colors.keys())))
  333. self._for_all_shapes('set_filled_curves', color, pattern)
  334. return self
  335. def show_hierarchy(self, indent=0, format='std'):
  336. """Recursive pretty print of hierarchy of objects."""
  337. if not isinstance(self.shapes, dict):
  338. print 'cannot print hierarchy when %s.shapes is not a dict' % \
  339. self.__class__.__name__
  340. s = ''
  341. if format == 'dict':
  342. s += '{'
  343. for shape in self.shapes:
  344. if format == 'dict':
  345. shape_str = repr(shape) + ':'
  346. elif format == 'plain':
  347. shape_str = shape
  348. else:
  349. shape_str = shape + ':'
  350. if format == 'dict' or format == 'plain':
  351. class_str = ''
  352. else:
  353. class_str = ' (%s)' % \
  354. self.shapes[shape].__class__.__name__
  355. s += '\n%s%s%s %s' % (
  356. ' '*indent,
  357. shape_str,
  358. class_str,
  359. self.shapes[shape].show_hierarchy(indent+4, format))
  360. if format == 'dict':
  361. s += '}'
  362. return s
  363. def __str__(self):
  364. """Display hierarchy with minimum information (just object names)."""
  365. return self.show_hierarchy(format='plain')
  366. def __repr__(self):
  367. """Display hierarchy as a dictionary."""
  368. return self.show_hierarchy(format='dict')
  369. #return pprint.pformat(self.shapes)
  370. class Curve(Shape):
  371. """General curve as a sequence of (x,y) coordintes."""
  372. def __init__(self, x, y):
  373. """
  374. `x`, `y`: arrays holding the coordinates of the curve.
  375. """
  376. self.x = asarray(x, dtype=float)
  377. self.y = asarray(y, dtype=float)
  378. #self.shapes must not be defined in this class
  379. #as self.shapes holds children objects:
  380. #Curve has no children (end leaf of self.shapes tree)
  381. self.linestyle = None
  382. self.linewidth = None
  383. self.linecolor = None
  384. self.fillcolor = None
  385. self.fillpattern = None
  386. self.arrow = None
  387. def inside_plot_area(self, verbose=True):
  388. """Check that all coordinates are within drawing_tool's area."""
  389. xmin, xmax = self.x.min(), self.x.max()
  390. ymin, ymax = self.y.min(), self.y.max()
  391. t = drawing_tool
  392. inside = True
  393. if xmin < t.xmin:
  394. inside = False
  395. if verbose:
  396. print 'x_min=%g < plot area x_min=%g' % (xmin, t.xmin)
  397. if xmax > t.xmax:
  398. inside = False
  399. if verbose:
  400. print 'x_max=%g > plot area x_max=%g' % (xmax, t.xmax)
  401. if ymin < t.ymin:
  402. inside = False
  403. if verbose:
  404. print 'y_min=%g < plot area y_min=%g' % (ymin, t.ymin)
  405. if xmax > t.xmax:
  406. inside = False
  407. if verbose:
  408. print 'y_max=%g > plot area y_max=%g' % (ymax, t.ymax)
  409. return inside
  410. def draw(self):
  411. """
  412. Send the curve to the plotting engine. That is, convert
  413. coordinate information in self.x and self.y, together
  414. with optional settings of linestyles, etc., to
  415. plotting commands for the chosen engine.
  416. """
  417. self.inside_plot_area()
  418. drawing_tool.plot_curve(
  419. self.x, self.y,
  420. self.linestyle, self.linewidth, self.linecolor,
  421. self.arrow, self.fillcolor, self.fillpattern)
  422. def rotate(self, angle, center):
  423. """
  424. Rotate all coordinates: `angle` is measured in degrees and
  425. (`x`,`y`) is the "origin" of the rotation.
  426. """
  427. angle = radians(angle)
  428. x, y = center
  429. c = cos(angle); s = sin(angle)
  430. xnew = x + (self.x - x)*c - (self.y - y)*s
  431. ynew = y + (self.x - x)*s + (self.y - y)*c
  432. self.x = xnew
  433. self.y = ynew
  434. return self
  435. def scale(self, factor):
  436. """Scale all coordinates by `factor`: ``x = factor*x``, etc."""
  437. self.x = factor*self.x
  438. self.y = factor*self.y
  439. return self
  440. def translate(self, vec):
  441. """Translate all coordinates by a vector `vec`."""
  442. self.x += vec[0]
  443. self.y += vec[1]
  444. return self
  445. def deform(self, displacement_function):
  446. """Displace all coordinates according to displacement_function(x,y)."""
  447. for i in range(len(self.x)):
  448. self.x[i], self.y[i] = displacement_function(self.x[i], self.y[i])
  449. return self
  450. def minmax_coordinates(self, minmax=None):
  451. if minmax is None:
  452. minmax = {'xmin': [], 'xmax': [], 'ymin': [], 'ymax': []}
  453. minmax['xmin'] = min(self.x.min(), minmax['xmin'])
  454. minmax['xmax'] = max(self.x.max(), minmax['xmax'])
  455. minmax['ymin'] = min(self.y.min(), minmax['ymin'])
  456. minmax['ymax'] = max(self.y.max(), minmax['ymax'])
  457. return minmax
  458. def recurse(self, name, indent=0):
  459. space = ' '*indent
  460. print space, 'reached "bottom" object %s' % \
  461. self.__class__.__name__
  462. def _object_couplings(self, parent, couplings=[], classname=True):
  463. return
  464. def set_linecolor(self, color):
  465. self.linecolor = color
  466. return self
  467. def set_linewidth(self, width):
  468. self.linewidth = width
  469. return self
  470. def set_linestyle(self, style):
  471. self.linestyle = style
  472. return self
  473. def set_arrow(self, style=None):
  474. self.arrow = style
  475. return self
  476. def set_name(self, name):
  477. self.name = name
  478. return self
  479. def set_filled_curves(self, color='', pattern=''):
  480. self.fillcolor = color
  481. self.fillpattern = pattern
  482. return self
  483. def show_hierarchy(self, indent=0, format='std'):
  484. if format == 'dict':
  485. return '"%s"' % str(self)
  486. elif format == 'plain':
  487. return ''
  488. else:
  489. return str(self)
  490. def __str__(self):
  491. """Compact pretty print of a Curve object."""
  492. s = '%d coords' % self.x.size
  493. if not self.inside_plot_area(verbose=False):
  494. s += ', some coordinates are outside plotting area!\n'
  495. props = ('linecolor', 'linewidth', 'linestyle', 'arrow',
  496. 'fillcolor', 'fillpattern')
  497. for prop in props:
  498. value = getattr(self, prop)
  499. if value is not None:
  500. s += ' %s=%s' % (prop, repr(value))
  501. return s
  502. def __repr__(self):
  503. return str(self)
  504. class Spline(Shape):
  505. def __init__(self, x, y, degree=3, resolution=501):
  506. from scipy.interpolate import UnivariateSpline
  507. self.smooth = UnivariateSpline(x, y, s=0, k=degree)
  508. xs = linspace(x[0], x[-1], resolution)
  509. ys = self.smooth(xs)
  510. self.shapes = {'smooth': Curve(xs, ys)}
  511. def geometric_features(self):
  512. s = self.shapes['smooth']
  513. return {'start': point(s.x[0], s.y[0]),
  514. 'end': point(s.x[-1], s.y[-1]),
  515. 'interval': [s.x[0], s.x[-1]]}
  516. def __call__(self, x):
  517. return self.smooth(x)
  518. class SketchyFunc(Spline):
  519. def __init__(self, name=None):
  520. x = [1, 2, 3, 4, 5, 6]
  521. y = [5, 3.5, 3.8, 3, 2.5, 2.4]
  522. Spline.__init__(self, x, y)
  523. self.shapes['smooth'].set_linecolor('black')
  524. if name is not None:
  525. self.shapes['name'] = Text(name, self.geometric_features()['start'] + point(0,0.1))
  526. class Point(Shape):
  527. """A point (x,y) which can be rotated, translated, and scaled."""
  528. def __init__(self, x, y):
  529. self.x, self.y = x, y
  530. #self.shapes is not needed in this class
  531. def __add__(self, other):
  532. if isinstance(other, (list,tuple)):
  533. other = Point(other)
  534. return Point(self.x+other.x, self.y+other.y)
  535. # class Point is an abstract class - only subclasses are useful
  536. # and must implement draw
  537. def draw(self):
  538. raise NotImplementedError(
  539. 'class %s must implement the draw method' %
  540. self.__class__.__name__)
  541. def rotate(self, angle, center):
  542. """Rotate point an `angle` (in degrees) around (`x`,`y`)."""
  543. angle = angle*pi/180
  544. x, y = center
  545. c = cos(angle); s = sin(angle)
  546. xnew = x + (self.x - x)*c - (self.y - y)*s
  547. ynew = y + (self.x - x)*s + (self.y - y)*c
  548. self.x = xnew
  549. self.y = ynew
  550. return self
  551. def scale(self, factor):
  552. """Scale point coordinates by `factor`: ``x = factor*x``, etc."""
  553. self.x = factor*self.x
  554. self.y = factor*self.y
  555. return self
  556. def translate(self, vec):
  557. """Translate point by a vector `vec`."""
  558. self.x += vec[0]
  559. self.y += vec[1]
  560. return self
  561. def deform(self, displacement_function):
  562. """Displace coordinates according to displacement_function(x,y)."""
  563. for i in range(len(self.x)):
  564. self.x, self.y = displacement_function(self.x, self.y)
  565. return self
  566. def minmax_coordinates(self, minmax=None):
  567. if minmax is None:
  568. minmax = {'xmin': [], 'xmax': [], 'ymin': [], 'ymax': []}
  569. minmax['xmin'] = min(self.x, minmax['xmin'])
  570. minmax['xmax'] = max(self.x, minmax['xmax'])
  571. minmax['ymin'] = min(self.y, minmax['ymin'])
  572. minmax['ymax'] = max(self.y, minmax['ymax'])
  573. return minmax
  574. def recurse(self, name, indent=0):
  575. space = ' '*indent
  576. print space, 'reached "bottom" object %s' % \
  577. self.__class__.__name__
  578. def _object_couplings(self, parent, couplings=[], classname=True):
  579. return
  580. def show_hierarchy(self, indent=0, format='std'):
  581. s = '%s at (%g,%g)' % (self.__class__.__name__, self.x, self.y)
  582. if format == 'dict':
  583. return '"%s"' % s
  584. elif format == 'plain':
  585. return ''
  586. else:
  587. return s
  588. # no need to store input data as they are invalid after rotations etc.
  589. class Rectangle(Shape):
  590. """
  591. Rectangle specified by the point `lower_left_corner`, `width`,
  592. and `height`.
  593. """
  594. def __init__(self, lower_left_corner, width, height):
  595. is_sequence(lower_left_corner)
  596. p = arr2D(lower_left_corner) # short form
  597. x = [p[0], p[0] + width,
  598. p[0] + width, p[0], p[0]]
  599. y = [p[1], p[1], p[1] + height,
  600. p[1] + height, p[1]]
  601. self.shapes = {'rectangle': Curve(x,y)}
  602. # Dimensions
  603. dims = {
  604. 'width': Distance_wText(p + point(0, -height/5.),
  605. p + point(width, -height/5.),
  606. 'width'),
  607. 'height': Distance_wText(p + point(width + width/5., 0),
  608. p + point(width + width/5., height),
  609. 'height'),
  610. 'lower_left_corner': Text_wArrow('lower_left_corner',
  611. p - point(width/5., height/5.), p)
  612. }
  613. self.dimensions = dims
  614. def geometric_features(self):
  615. """
  616. Return dictionary with
  617. ==================== =============================================
  618. Attribute Description
  619. ==================== =============================================
  620. lower_left Lower left corner point.
  621. upper_left Upper left corner point.
  622. lower_right Lower right corner point.
  623. upper_right Upper right corner point.
  624. lower_mid Middle point on lower side.
  625. upper_mid Middle point on upper side.
  626. ==================== =============================================
  627. """
  628. r = self.shapes['rectangle']
  629. d = {'lower_left': point(r.x[0], r.y[0]),
  630. 'lower_right': point(r.x[1], r.y[1]),
  631. 'upper_right': point(r.x[2], r.y[2]),
  632. 'upper_left': point(r.x[3], r.y[3])}
  633. d['lower_mid'] = 0.5*(d['lower_left'] + d['lower_right'])
  634. d['upper_mid'] = 0.5*(d['upper_left'] + d['upper_right'])
  635. d['left_mid'] = 0.5*(d['lower_left'] + d['upper_left'])
  636. d['right_mid'] = 0.5*(d['lower_right'] + d['upper_right'])
  637. return d
  638. class Triangle(Shape):
  639. """
  640. Triangle defined by its three vertices p1, p2, and p3.
  641. Recorded geometric features:
  642. ==================== =============================================
  643. Attribute Description
  644. ==================== =============================================
  645. p1, p2, p3 Corners as given to the constructor.
  646. ==================== =============================================
  647. """
  648. def __init__(self, p1, p2, p3):
  649. is_sequence(p1, p2, p3)
  650. x = [p1[0], p2[0], p3[0], p1[0]]
  651. y = [p1[1], p2[1], p3[1], p1[1]]
  652. self.shapes = {'triangle': Curve(x,y)}
  653. # Dimensions
  654. self.dimensions = {'p1': Text('p1', p1),
  655. 'p2': Text('p2', p2),
  656. 'p3': Text('p3', p3)}
  657. def geometric_features(self):
  658. t = self.shapes['triangle']
  659. return {'p1': point(t.x[0], t.y[0]),
  660. 'p2': point(t.x[1], t.y[1]),
  661. 'p3': point(t.x[2], t.y[2])}
  662. class Line(Shape):
  663. def __init__(self, start, end):
  664. is_sequence(start, end, length=2)
  665. x = [start[0], end[0]]
  666. y = [start[1], end[1]]
  667. self.shapes = {'line': Curve(x, y)}
  668. def geometric_features(self):
  669. line = self.shapes['line']
  670. return {'start': point(line.x[0], line.y[0]),
  671. 'end': point(line.x[1], line.y[1]),}
  672. def compute_formulas(self):
  673. x, y = self.shapes['line'].x, self.shapes['line'].y
  674. # Define equations for line:
  675. # y = a*x + b, x = c*y + d
  676. try:
  677. self.a = (y[1] - y[0])/(x[1] - x[0])
  678. self.b = y[0] - self.a*x[0]
  679. except ZeroDivisionError:
  680. # Vertical line, y is not a function of x
  681. self.a = None
  682. self.b = None
  683. try:
  684. if self.a is None:
  685. self.c = 0
  686. else:
  687. self.c = 1/float(self.a)
  688. if self.b is None:
  689. self.d = x[1]
  690. except ZeroDivisionError:
  691. # Horizontal line, x is not a function of y
  692. self.c = None
  693. self.d = None
  694. def compute_formulas(self):
  695. x, y = self.shapes['line'].x, self.shapes['line'].y
  696. tol = 1E-14
  697. # Define equations for line:
  698. # y = a*x + b, x = c*y + d
  699. if abs(x[1] - x[0]) > tol:
  700. self.a = (y[1] - y[0])/(x[1] - x[0])
  701. self.b = y[0] - self.a*x[0]
  702. else:
  703. # Vertical line, y is not a function of x
  704. self.a = None
  705. self.b = None
  706. if self.a is None:
  707. self.c = 0
  708. elif abs(self.a) > tol:
  709. self.c = 1/float(self.a)
  710. self.d = x[1]
  711. else: # self.a is 0
  712. # Horizontal line, x is not a function of y
  713. self.c = None
  714. self.d = None
  715. def __call__(self, x=None, y=None):
  716. """Given x, return y on the line, or given y, return x."""
  717. self.compute_formulas()
  718. if x is not None and self.a is not None:
  719. return self.a*x + self.b
  720. elif y is not None and self.c is not None:
  721. return self.c*y + self.d
  722. else:
  723. raise ValueError(
  724. 'Line.__call__(x=%s, y=%s) not meaningful' % \
  725. (x, y))
  726. def new_interval(self, x=None, y=None):
  727. """Redefine current Line to cover interval in x or y."""
  728. if x is not None:
  729. is_sequence(x, length=2)
  730. xL, xR = x
  731. new_line = Line((xL, self(x=xL)), (xR, self(x=xR)))
  732. elif y is not None:
  733. is_sequence(y, length=2)
  734. yL, yR = y
  735. new_line = Line((xL, self(y=xL)), (xR, self(y=xR)))
  736. self.shapes['line'] = new_line['line']
  737. return self
  738. # First implementation of class Circle
  739. class Circle(Shape):
  740. def __init__(self, center, radius, resolution=180):
  741. self.center, self.radius = center, radius
  742. self.resolution = resolution
  743. t = linspace(0, 2*pi, resolution+1)
  744. x0 = center[0]; y0 = center[1]
  745. R = radius
  746. x = x0 + R*cos(t)
  747. y = y0 + R*sin(t)
  748. self.shapes = {'circle': Curve(x, y)}
  749. def __call__(self, theta):
  750. """
  751. Return (x, y) point corresponding to angle theta.
  752. Not valid after a translation, rotation, or scaling.
  753. """
  754. return self.center[0] + self.radius*cos(theta), \
  755. self.center[1] + self.radius*sin(theta)
  756. class Arc(Shape):
  757. def __init__(self, center, radius,
  758. start_angle, arc_angle,
  759. resolution=180):
  760. is_sequence(center)
  761. # Must record some parameters for __call__
  762. self.center = arr2D(center)
  763. self.radius = radius
  764. self.start_angle = radians(start_angle)
  765. self.arc_angle = radians(arc_angle)
  766. t = linspace(self.start_angle,
  767. self.start_angle + self.arc_angle,
  768. resolution+1)
  769. x0 = center[0]; y0 = center[1]
  770. R = radius
  771. x = x0 + R*cos(t)
  772. y = y0 + R*sin(t)
  773. self.shapes = {'arc': Curve(x, y)}
  774. # Cannot set dimensions (Arc_wText recurses into this
  775. # constructor forever). Set in test_Arc instead.
  776. # Stored geometric features
  777. def geometric_features(self):
  778. a = self.shapes['arc']
  779. m = len(a.x)/2 # mid point in array
  780. d = {'start': point(a.x[0], a.y[0]),
  781. 'end': point(a.x[-1], a.y[-1]),
  782. 'mid': point(a.x[m], a.y[m])}
  783. return d
  784. def __call__(self, theta):
  785. """
  786. Return (x,y) point at start_angle + theta.
  787. Not valid after translation, rotation, or scaling.
  788. """
  789. theta = radians(theta)
  790. t = self.start_angle + theta
  791. x0 = self.center[0]
  792. y0 = self.center[1]
  793. R = self.radius
  794. x = x0 + R*cos(t)
  795. y = y0 + R*sin(t)
  796. return (x, y)
  797. # Alternative for small arcs: Parabola
  798. class Parabola(Shape):
  799. def __init__(self, start, mid, stop, resolution=21):
  800. self.p1, self.p2, self.p3 = start, mid, stop
  801. # y as function of x? (no point on line x=const?)
  802. tol = 1E-14
  803. if abs(self.p1[0] - self.p2[0]) > 1E-14 and \
  804. abs(self.p2[0] - self.p3[0]) > 1E-14 and \
  805. abs(self.p3[0] - self.p1[0]) > 1E-14:
  806. self.y_of_x = True
  807. else:
  808. self.y_of_x = False
  809. # x as function of y? (no point on line y=const?)
  810. tol = 1E-14
  811. if abs(self.p1[1] - self.p2[1]) > 1E-14 and \
  812. abs(self.p2[1] - self.p3[1]) > 1E-14 and \
  813. abs(self.p3[1] - self.p1[1]) > 1E-14:
  814. self.x_of_y = True
  815. else:
  816. self.x_of_y = False
  817. if self.y_of_x:
  818. x = linspace(start[0], end[0], resolution)
  819. y = self(x=x)
  820. elif self.x_of_y:
  821. y = linspace(start[1], end[1], resolution)
  822. x = self(y=y)
  823. else:
  824. raise ValueError(
  825. 'Parabola: two or more points lie on x=const '
  826. 'or y=const - not allowed')
  827. self.shapes = {'parabola': Curve(x, y)}
  828. def __call__(self, x=None, y=None):
  829. if x is not None and self.y_of_x:
  830. return self._L2x(self.p1, self.p2)*self.p3[1] + \
  831. self._L2x(self.p2, self.p3)*self.p1[1] + \
  832. self._L2x(self.p3, self.p1)*self.p2[1]
  833. elif y is not None and self.x_of_y:
  834. return self._L2y(self.p1, self.p2)*self.p3[0] + \
  835. self._L2y(self.p2, self.p3)*self.p1[0] + \
  836. self._L2y(self.p3, self.p1)*self.p2[0]
  837. else:
  838. raise ValueError(
  839. 'Parabola.__call__(x=%s, y=%s) not meaningful' % \
  840. (x, y))
  841. def _L2x(self, x, pi, pj, pk):
  842. return (x - pi[0])*(x - pj[0])/((pk[0] - pi[0])*(pk[0] - pj[0]))
  843. def _L2y(self, y, pi, pj, pk):
  844. return (y - pi[1])*(y - pj[1])/((pk[1] - pi[1])*(pk[1] - pj[1]))
  845. class Circle(Arc):
  846. def __init__(self, center, radius, resolution=180):
  847. Arc.__init__(self, center, radius, 0, 360, resolution)
  848. class Wall(Shape):
  849. def __init__(self, x, y, thickness, pattern='/'):
  850. is_sequence(x, y, length=len(x))
  851. if isinstance(x[0], (tuple,list,ndarray)):
  852. # x is list of curves
  853. x1 = concatenate(x)
  854. else:
  855. x1 = asarray(x, float)
  856. if isinstance(y[0], (tuple,list,ndarray)):
  857. # x is list of curves
  858. y1 = concatenate(y)
  859. else:
  860. y1 = asarray(y, float)
  861. self.x1 = x1; self.y1 = y1
  862. # Displaced curve (according to thickness)
  863. x2 = x1
  864. y2 = y1 + thickness
  865. # Combine x1,y1 with x2,y2 reversed
  866. from numpy import concatenate
  867. x = concatenate((x1, x2[-1::-1]))
  868. y = concatenate((y1, y2[-1::-1]))
  869. wall = Curve(x, y)
  870. wall.set_filled_curves(color='white', pattern=pattern)
  871. x = [x1[-1]] + x2[-1::-1].tolist() + [x1[0]]
  872. y = [y1[-1]] + y2[-1::-1].tolist() + [y1[0]]
  873. self.shapes = {'wall': wall}
  874. white_eraser = Curve(x, y)
  875. white_eraser.set_linecolor('white')
  876. from collections import OrderedDict
  877. self.shapes = OrderedDict()
  878. self.shapes['wall'] = wall
  879. self.shapes['eraser'] = white_eraser
  880. def geometric_features(self):
  881. d = {'start': point(self.x1[0], self.y1[0]),
  882. 'end': point(self.x1[-1], self.y1[-1])}
  883. return d
  884. class Wall2(Shape):
  885. def __init__(self, x, y, thickness, pattern='/'):
  886. is_sequence(x, y, length=len(x))
  887. if isinstance(x[0], (tuple,list,ndarray)):
  888. # x is list of curves
  889. x1 = concatenate(x)
  890. else:
  891. x1 = asarray(x, float)
  892. if isinstance(y[0], (tuple,list,ndarray)):
  893. # x is list of curves
  894. y1 = concatenate(y)
  895. else:
  896. y1 = asarray(y, float)
  897. self.x1 = x1; self.y1 = y1
  898. # Displaced curve (according to thickness)
  899. x2 = x1.copy()
  900. y2 = y1.copy()
  901. def displace(idx, idx_m, idx_p):
  902. # Find tangent and normal
  903. tangent = point(x1[idx_m], y1[idx_m]) - point(x1[idx_p], y1[idx_p])
  904. tangent = unit_vec(tangent)
  905. normal = point(tangent[1], -tangent[0])
  906. # Displace length "thickness" in "positive" normal direction
  907. displaced_pt = point(x1[idx], y1[idx]) + thickness*normal
  908. x2[idx], y2[idx] = displaced_pt
  909. for i in range(1, len(x1)-1):
  910. displace(i-1, i+1, i) # centered difference for normal comp.
  911. # One-sided differences at the end points
  912. i = 0
  913. displace(i, i+1, i)
  914. i = len(x1)-1
  915. displace(i-1, i, i)
  916. # Combine x1,y1 with x2,y2 reversed
  917. from numpy import concatenate
  918. x = concatenate((x1, x2[-1::-1]))
  919. y = concatenate((y1, y2[-1::-1]))
  920. wall = Curve(x, y)
  921. wall.set_filled_curves(color='white', pattern=pattern)
  922. x = [x1[-1]] + x2[-1::-1].tolist() + [x1[0]]
  923. y = [y1[-1]] + y2[-1::-1].tolist() + [y1[0]]
  924. self.shapes['wall'] = wall
  925. def geometric_features(self):
  926. d = {'start': point(self.x1[0], self.y1[0]),
  927. 'end': point(self.x1[-1], self.y1[-1])}
  928. return d
  929. class VelocityProfile(Shape):
  930. def __init__(self, start, height, profile, num_arrows, scaling=1):
  931. # vx, vy = profile(y)
  932. shapes = {}
  933. # Draw left line
  934. shapes['start line'] = Line(start, (start[0], start[1]+height))
  935. # Draw velocity arrows
  936. dy = float(height)/(num_arrows-1)
  937. x = start[0]
  938. y = start[1]
  939. r = profile(y) # Test on return type
  940. if not isinstance(r, (list,tuple,ndarray)) and len(r) != 2:
  941. raise TypeError('VelocityProfile constructor: profile(y) function must return velocity vector (vx,vy), not %s' % type(r))
  942. for i in range(num_arrows):
  943. y = i*dy
  944. vx, vy = profile(y)
  945. if abs(vx) < 1E-8:
  946. continue
  947. vx *= scaling
  948. vy *= scaling
  949. arr = Arrow1((x,y), (x+vx, y+vy), '->')
  950. shapes['arrow%d' % i] = arr
  951. # Draw smooth profile
  952. xs = []
  953. ys = []
  954. n = 100
  955. dy = float(height)/n
  956. for i in range(n+2):
  957. y = i*dy
  958. vx, vy = profile(y)
  959. vx *= scaling
  960. vy *= scaling
  961. xs.append(x+vx)
  962. ys.append(y+vy)
  963. shapes['smooth curve'] = Curve(xs, ys)
  964. self.shapes = shapes
  965. class Arrow1(Shape):
  966. """Draw an arrow as Line with arrow."""
  967. def __init__(self, start, end, style='->'):
  968. arrow = Line(start, end)
  969. arrow.set_arrow(style)
  970. self.shapes = {'arrow': arrow}
  971. def geometric_features(self):
  972. return self.shapes['arrow'].geometric_features()
  973. class Arrow3(Shape):
  974. """
  975. Build a vertical line and arrow head from Line objects.
  976. Then rotate `rotation_angle`.
  977. """
  978. def __init__(self, start, length, rotation_angle=0):
  979. self.bottom = start
  980. self.length = length
  981. self.angle = rotation_angle
  982. top = (self.bottom[0], self.bottom[1] + self.length)
  983. main = Line(self.bottom, top)
  984. #head_length = self.length/8.0
  985. head_length = drawing_tool.xrange/50.
  986. head_degrees = radians(30)
  987. head_left_pt = (top[0] - head_length*sin(head_degrees),
  988. top[1] - head_length*cos(head_degrees))
  989. head_right_pt = (top[0] + head_length*sin(head_degrees),
  990. top[1] - head_length*cos(head_degrees))
  991. head_left = Line(head_left_pt, top)
  992. head_right = Line(head_right_pt, top)
  993. head_left.set_linestyle('solid')
  994. head_right.set_linestyle('solid')
  995. self.shapes = {'line': main, 'head left': head_left,
  996. 'head right': head_right}
  997. # rotate goes through self.shapes so self.shapes
  998. # must be initialized first
  999. self.rotate(rotation_angle, start)
  1000. def geometric_features(self):
  1001. return self.shapes['line'].geometric_features()
  1002. class Text(Point):
  1003. """
  1004. Place `text` at the (x,y) point `position`, with the given
  1005. fontsize (0 indicates that the default fontsize set in drawing_tool
  1006. is to be used). The text is centered around `position` if `alignment` is
  1007. 'center'; if 'left', the text starts at `position`, and if
  1008. 'right', the right and of the text is located at `position`.
  1009. """
  1010. def __init__(self, text, position, alignment='center', fontsize=0):
  1011. is_sequence(position)
  1012. is_sequence(position, length=2, can_be_None=True)
  1013. self.text = text
  1014. self.position = position
  1015. self.alignment = alignment
  1016. self.fontsize = fontsize
  1017. Point.__init__(self, position[0], position[1])
  1018. #no need for self.shapes here
  1019. def draw(self):
  1020. drawing_tool.text(self.text, (self.x, self.y),
  1021. self.alignment, self.fontsize)
  1022. def __str__(self):
  1023. return 'text "%s" at (%g,%g)' % (self.text, self.x, self.y)
  1024. def __repr__(self):
  1025. return str(self)
  1026. class Text_wArrow(Text):
  1027. """
  1028. As class Text, but an arrow is drawn from the mid part of the text
  1029. to some point `arrow_tip`.
  1030. """
  1031. def __init__(self, text, position, arrow_tip,
  1032. alignment='center', fontsize=0):
  1033. is_sequence(arrow_tip, length=2, can_be_None=True)
  1034. is_sequence(position)
  1035. self.arrow_tip = arrow_tip
  1036. Text.__init__(self, text, position, alignment, fontsize)
  1037. def draw(self):
  1038. drawing_tool.text(self.text, self.position,
  1039. self.alignment, self.fontsize,
  1040. self.arrow_tip)
  1041. def __str__(self):
  1042. return 'annotation "%s" at (%g,%g) with arrow to (%g,%g)' % \
  1043. (self.text, self.x, self.y,
  1044. self.arrow_tip[0], self.arrow_tip[1])
  1045. def __repr__(self):
  1046. return str(self)
  1047. class Axis(Shape):
  1048. def __init__(self, start, length, label,
  1049. rotation_angle=0, fontsize=0,
  1050. label_spacing=1./45, label_alignment='left'):
  1051. """
  1052. Draw axis from start with `length` to the right
  1053. (x axis). Place label at the end of the arrow tip.
  1054. Then return `rotation_angle` (in degrees).
  1055. The `label_spacing` denotes the space between the label
  1056. and the arrow tip as a fraction of the length of the plot
  1057. in x direction. With `label_alignment` one can place
  1058. the axis label text such that the arrow tip is to the 'left',
  1059. 'right', or 'center' with respect to the text field.
  1060. The `label_spacing` and `label_alignment` parameters can
  1061. be used to fine-tune the location of the label.
  1062. """
  1063. # Arrow is vertical arrow, make it horizontal
  1064. arrow = Arrow3(start, length, rotation_angle=-90)
  1065. arrow.rotate(rotation_angle, start)
  1066. spacing = drawing_tool.xrange*label_spacing
  1067. # should increase spacing for downward pointing axis
  1068. label_pos = [start[0] + length + spacing, start[1]]
  1069. label = Text(label, position=label_pos, fontsize=fontsize)
  1070. label.rotate(rotation_angle, start)
  1071. self.shapes = {'arrow': arrow, 'label': label}
  1072. def geometric_features(self):
  1073. return self.shapes['arrow'].geometric_features()
  1074. # Maybe Axis3 with label below/above?
  1075. class Force(Arrow1):
  1076. """
  1077. Indication of a force by an arrow and a text (symbol). Draw an
  1078. arrow, starting at `start` and with the tip at `end`. The symbol
  1079. is placed at `text_pos`, which can be 'start', 'end' or the
  1080. coordinates of a point. If 'end' or 'start', the text is placed at
  1081. a distance `text_spacing` times the width of the total plotting
  1082. area away from the specified point.
  1083. """
  1084. def __init__(self, start, end, text, text_spacing=1./60,
  1085. fontsize=0, text_pos='start', text_alignment='center'):
  1086. Arrow1.__init__(self, start, end, style='->')
  1087. spacing = drawing_tool.xrange*text_spacing
  1088. start, end = arr2D(start), arr2D(end)
  1089. # Two cases: label at bottom of line or top, need more
  1090. # spacing if bottom
  1091. downward = (end-start)[1] < 0
  1092. upward = not downward # for easy code reading
  1093. if isinstance(text_pos, str):
  1094. if text_pos == 'start':
  1095. spacing_dir = unit_vec(start - end)
  1096. if upward:
  1097. spacing *= 1.7
  1098. text_pos = start + spacing*spacing_dir
  1099. elif text_pos == 'end':
  1100. spacing_dir = unit_vec(end - start)
  1101. if downward:
  1102. spacing *= 1.7
  1103. text_pos = end + spacing*spacing_dir
  1104. self.shapes['text'] = Text(text, text_pos, fontsize=fontsize,
  1105. alignment=text_alignment)
  1106. def geometric_features(self):
  1107. d = Arrow1.geometric_features(self)
  1108. d['symbol_location'] = self.shapes['text'].position
  1109. return d
  1110. class Axis2(Force):
  1111. def __init__(self, start, length, label,
  1112. rotation_angle=0, fontsize=0,
  1113. label_spacing=1./45, label_alignment='left'):
  1114. direction = point(cos(radians(rotation_angle)),
  1115. sin(radians(rotation_angle)))
  1116. Force.__init__(start=start, end=length*direction, text=label,
  1117. text_spacing=label_spacing,
  1118. fontsize=fontsize, text_pos='end',
  1119. text_alignment=label_alignment)
  1120. # Substitute text by label for axis
  1121. self.shapes['label'] = self.shapes['text']
  1122. del self.shapes['text']
  1123. # geometric features from Force is ok
  1124. class Gravity(Axis):
  1125. """Downward-pointing gravity arrow with the symbol g."""
  1126. def __init__(self, start, length, fontsize=0):
  1127. Axis.__init__(self, start, length, '$g$', below=False,
  1128. rotation_angle=-90, label_spacing=1./30,
  1129. fontsize=fontsize)
  1130. self.shapes['arrow'].set_linecolor('black')
  1131. class Gravity(Force):
  1132. """Downward-pointing gravity arrow with the symbol g."""
  1133. def __init__(self, start, length, text='$g$', fontsize=0):
  1134. Force.__init__(self, start, (start[0], start[1]-length),
  1135. text, text_spacing=1./60,
  1136. fontsize=0, text_pos='end')
  1137. self.shapes['arrow'].set_linecolor('black')
  1138. class Distance_wText(Shape):
  1139. """
  1140. Arrow <-> with text (usually a symbol) at the midpoint, used for
  1141. identifying a some distance in a figure. The text is placed
  1142. slightly to the right of vertical-like arrows, with text displaced
  1143. `text_spacing` times to total distance in x direction of the plot
  1144. area. The text is by default aligned 'left' in this case. For
  1145. horizontal-like arrows, the text is placed the same distance
  1146. above, but aligned 'center' by default (when `alignment` is None).
  1147. """
  1148. def __init__(self, start, end, text, fontsize=0, text_spacing=1/60.,
  1149. alignment=None, text_pos='mid'):
  1150. start = arr2D(start)
  1151. end = arr2D(end)
  1152. # Decide first if we have a vertical or horizontal arrow
  1153. vertical = abs(end[0]-start[0]) < 2*abs(end[1]-start[1])
  1154. if vertical:
  1155. # Assume end above start
  1156. if end[1] < start[1]:
  1157. start, end = end, start
  1158. if alignment is None:
  1159. alignment = 'left'
  1160. else: # horizontal arrow
  1161. # Assume start to the right of end
  1162. if start[0] < end[0]:
  1163. start, end = end, start
  1164. if alignment is None:
  1165. alignment = 'center'
  1166. tangent = end - start
  1167. # Tangeng goes always to the left and upward
  1168. normal = unit_vec([tangent[1], -tangent[0]])
  1169. mid = 0.5*(start + end) # midpoint of start-end line
  1170. if text_pos == 'mid':
  1171. text_pos = mid + normal*drawing_tool.xrange*text_spacing
  1172. text = Text(text, text_pos, fontsize=fontsize,
  1173. alignment=alignment)
  1174. else:
  1175. is_sequence(text_pos, length=2)
  1176. text = Text_wArrow(text, text_pos, mid, alignment='left',
  1177. fontsize=fontsize)
  1178. arrow = Arrow1(start, end, style='<->')
  1179. arrow.set_linecolor('black')
  1180. arrow.set_linewidth(1)
  1181. self.shapes = {'arrow': arrow, 'text': text}
  1182. def geometric_features(self):
  1183. d = self.shapes['arrow'].geometric_features()
  1184. d['text_position'] = self.shapes['text'].position
  1185. return d
  1186. class Arc_wText(Shape):
  1187. def __init__(self, text, center, radius,
  1188. start_angle, arc_angle, fontsize=0,
  1189. resolution=180, text_spacing=1/60.):
  1190. arc = Arc(center, radius, start_angle, arc_angle,
  1191. resolution)
  1192. mid = arr2D(arc(arc_angle/2.))
  1193. normal = unit_vec(mid - arr2D(center))
  1194. text_pos = mid + normal*drawing_tool.xrange*text_spacing
  1195. self.shapes = {'arc': arc,
  1196. 'text': Text(text, text_pos, fontsize=fontsize)}
  1197. class Composition(Shape):
  1198. def __init__(self, shapes):
  1199. """shapes: list or dict of Shape objects."""
  1200. self.shapes = shapes
  1201. # can make help methods: Line.midpoint, Line.normal(pt, dir='left') -> (x,y)
  1202. # list annotations in each class? contains extra annotations for explaining
  1203. # important parameters to the constructor, e.g., Line.annotations holds
  1204. # start and end as Text objects. Shape.demo calls shape.draw and
  1205. # for annotation in self.demo: annotation.draw() YES!
  1206. # Can make overall demo of classes by making objects and calling demo
  1207. # Could include demo fig in each constructor
  1208. class SimplySupportedBeam(Shape):
  1209. def __init__(self, pos, size):
  1210. pos = arr2D(pos)
  1211. P0 = (pos[0] - size/2., pos[1]-size)
  1212. P1 = (pos[0] + size/2., pos[1]-size)
  1213. triangle = Triangle(P0, P1, pos)
  1214. gap = size/5.
  1215. h = size/4. # height of rectangle
  1216. P2 = (P0[0], P0[1]-gap-h)
  1217. rectangle = Rectangle(P2, size, h).set_filled_curves(pattern='/')
  1218. self.shapes = {'triangle': triangle, 'rectangle': rectangle}
  1219. self.dimensions = {'pos': Text('pos', pos),
  1220. 'size': Distance_wText((P2[0], P2[1]-size),
  1221. (P2[0]+size, P2[1]-size),
  1222. 'size')}
  1223. def geometric_features(self):
  1224. t = self.shapes['triangle']
  1225. r = self.shapes['rectangle']
  1226. d = {'pos': point(t.x[2], t.y[2]), # "p2"/pos
  1227. 'mid_support': r.geometric_features()['lower_mid']}
  1228. return d
  1229. class ConstantBeamLoad(Shape):
  1230. """
  1231. Downward-pointing arrows indicating a vertical load.
  1232. The arrows are of equal length and filling a rectangle
  1233. specified as in the :class:`Rectangle` class.
  1234. Recorded geometric features:
  1235. ==================== =============================================
  1236. Attribute Description
  1237. ==================== =============================================
  1238. mid_point Middle point at the top of the row of
  1239. arrows (often used for positioning a text).
  1240. ==================== =============================================
  1241. """
  1242. def __init__(self, lower_left_corner, width, height, num_arrows=10):
  1243. box = Rectangle(lower_left_corner, width, height)
  1244. self.shapes = {'box': box}
  1245. dx = float(width)/(num_arrows-1)
  1246. y_top = lower_left_corner[1] + height
  1247. y_tip = lower_left_corner[1]
  1248. for i in range(num_arrows):
  1249. x = lower_left_corner[0] + i*dx
  1250. self.shapes['arrow%d' % i] = Arrow1((x, y_top), (x, y_tip))
  1251. def geometric_features(self):
  1252. return {'mid_top': self.shapes['box'].geometric_features()['upper_mid']}
  1253. class Moment(Arc_wText):
  1254. def __init__(self, text, center, radius,
  1255. left=True, counter_clockwise=True,
  1256. fontsize=0, text_spacing=1/60.):
  1257. style = '->' if counter_clockwise else '<-'
  1258. start_angle = 90 if left else -90
  1259. Arc_wText.__init__(self, text, center, radius,
  1260. start_angle=start_angle,
  1261. arc_angle=180, fontsize=fontsize,
  1262. text_spacing=text_spacing,
  1263. resolution=180)
  1264. self.shapes['arc'].set_arrow(style)
  1265. class Wheel(Shape):
  1266. def __init__(self, center, radius, inner_radius=None, nlines=10):
  1267. if inner_radius is None:
  1268. inner_radius = radius/5.0
  1269. outer = Circle(center, radius)
  1270. inner = Circle(center, inner_radius)
  1271. lines = []
  1272. # Draw nlines+1 since the first and last coincide
  1273. # (then nlines lines will be visible)
  1274. t = linspace(0, 2*pi, self.nlines+1)
  1275. Ri = inner_radius; Ro = radius
  1276. x0 = center[0]; y0 = center[1]
  1277. xinner = x0 + Ri*cos(t)
  1278. yinner = y0 + Ri*sin(t)
  1279. xouter = x0 + Ro*cos(t)
  1280. youter = y0 + Ro*sin(t)
  1281. lines = [Line((xi,yi),(xo,yo)) for xi, yi, xo, yo in \
  1282. zip(xinner, yinner, xouter, youter)]
  1283. self.shapes = {'inner': inner, 'outer': outer,
  1284. 'spokes': Composition(
  1285. {'spoke%d' % i: lines[i]
  1286. for i in range(len(lines))})}
  1287. class SineWave(Shape):
  1288. def __init__(self, xstart, xstop,
  1289. wavelength, amplitude, mean_level):
  1290. self.xstart = xstart
  1291. self.xstop = xstop
  1292. self.wavelength = wavelength
  1293. self.amplitude = amplitude
  1294. self.mean_level = mean_level
  1295. npoints = (self.xstop - self.xstart)/(self.wavelength/61.0)
  1296. x = linspace(self.xstart, self.xstop, npoints)
  1297. k = 2*pi/self.wavelength # frequency
  1298. y = self.mean_level + self.amplitude*sin(k*x)
  1299. self.shapes = {'waves': Curve(x,y)}
  1300. class Spring(Shape):
  1301. """
  1302. Specify a *vertical* spring, starting at `start` and with `length`
  1303. as total vertical length. In the middle of the spring there are
  1304. `num_windings` circular windings to illustrate the spring. If
  1305. `teeth` is true, the spring windings look like saw teeth,
  1306. otherwise the windings are smooth circles. The parameters `width`
  1307. (total width of spring) and `bar_length` (length of first and last
  1308. bar are given sensible default values if they are not specified
  1309. (these parameters can later be extracted as attributes, see table
  1310. below).
  1311. """
  1312. spring_fraction = 1./2 # fraction of total length occupied by spring
  1313. def __init__(self, start, length, width=None, bar_length=None,
  1314. num_windings=11, teeth=False):
  1315. B = start
  1316. n = num_windings - 1 # n counts teeth intervals
  1317. if n <= 6:
  1318. n = 7
  1319. # n must be odd:
  1320. if n % 2 == 0:
  1321. n = n+1
  1322. L = length
  1323. if width is None:
  1324. w = L/10.
  1325. else:
  1326. w = width/2.0
  1327. s = bar_length
  1328. # [0, x, L-x, L], f = (L-2*x)/L
  1329. # x = L*(1-f)/2.
  1330. # B: start point
  1331. # w: half-width
  1332. # L: total length
  1333. # s: length of first bar
  1334. # P0: start of dashpot (B[0]+s)
  1335. # P1: end of dashpot
  1336. # P2: end point
  1337. shapes = {}
  1338. if s is None:
  1339. f = Spring.spring_fraction
  1340. s = L*(1-f)/2. # start of spring
  1341. self.bar_length = s # record
  1342. self.width = 2*w
  1343. P0 = (B[0], B[1] + s)
  1344. P1 = (B[0], B[1] + L-s)
  1345. P2 = (B[0], B[1] + L)
  1346. if s >= L:
  1347. raise ValueError('length of first bar: %g is larger than total length: %g' % (s, L))
  1348. shapes['bar1'] = Line(B, P0)
  1349. spring_length = L - 2*s
  1350. t = spring_length/n # height increment per winding
  1351. if teeth:
  1352. resolution = 4
  1353. else:
  1354. resolution = 90
  1355. q = linspace(0, n, n*resolution + 1)
  1356. x = P0[0] + w*sin(2*pi*q)
  1357. y = P0[1] + q*t
  1358. shapes['sprial'] = Curve(x, y)
  1359. shapes['bar2'] = Line(P1,P2)
  1360. self.shapes = shapes
  1361. # Dimensions
  1362. start = Text_wArrow('start', (B[0]-1.5*w,B[1]-1.5*w), B)
  1363. width = Distance_wText((B[0]-w, B[1]-3.5*w), (B[0]+w, B[1]-3.5*w),
  1364. 'width')
  1365. length = Distance_wText((B[0]+3*w, B[1]), (B[0]+3*w, B[1]+L),
  1366. 'length')
  1367. num_windings = Text_wArrow('num_windings',
  1368. (B[0]+2*w,P2[1]+w),
  1369. (B[0]+1.2*w, B[1]+L/2.))
  1370. blength1 = Distance_wText((B[0]-2*w, B[1]), (B[0]-2*w, P0[1]),
  1371. 'bar_length',
  1372. text_pos=(P0[0]-7*w, P0[1]+w))
  1373. blength2 = Distance_wText((P1[0]-2*w, P1[1]), (P2[0]-2*w, P2[1]),
  1374. 'bar_length',
  1375. text_pos=(P2[0]-7*w, P2[1]+w))
  1376. dims = {'start': start, 'width': width, 'length': length,
  1377. 'num_windings': num_windings, 'bar_length1': blength1,
  1378. 'bar_length2': blength2}
  1379. self.dimensions = dims
  1380. def geometric_features(self):
  1381. """
  1382. Recorded geometric features:
  1383. ==================== =============================================
  1384. Attribute Description
  1385. ==================== =============================================
  1386. start Start point of spring.
  1387. end End point of spring.
  1388. width Total width of spring.
  1389. bar_length Length of first (and last) bar part.
  1390. ==================== =============================================
  1391. """
  1392. b1 = self.shapes['bar1']
  1393. d = {'start': b1.geometric_features()['start'],
  1394. 'end': self.shapes['bar2'].geometric_features()['end'],
  1395. 'bar_length': self.bar_length,
  1396. 'width': self.width}
  1397. return d
  1398. class Dashpot(Shape):
  1399. """
  1400. Specify a vertical dashpot of height `total_length` and `start` as
  1401. bottom/starting point. The first bar part has length `bar_length`.
  1402. Then comes the dashpot as a rectangular construction of total
  1403. width `width` and height `dashpot_length`. The position of the
  1404. piston inside the rectangular dashpot area is given by
  1405. `piston_pos`, which is the distance between the first bar (given
  1406. by `bar_length`) to the piston.
  1407. If some of `dashpot_length`, `bar_length`, `width` or `piston_pos`
  1408. are not given, suitable default values are calculated. Their
  1409. values can be extracted as keys in the dict returned from
  1410. ``geometric_features``.
  1411. """
  1412. dashpot_fraction = 1./2 # fraction of total_length
  1413. piston_gap_fraction = 1./6 # fraction of width
  1414. piston_thickness_fraction = 1./8 # fraction of dashplot_length
  1415. def __init__(self, start, total_length, bar_length=None,
  1416. width=None, dashpot_length=None, piston_pos=None):
  1417. B = start
  1418. L = total_length
  1419. if width is None:
  1420. w = L/10. # total width 1/5 of length
  1421. else:
  1422. w = width/2.0
  1423. s = bar_length
  1424. # [0, x, L-x, L], f = (L-2*x)/L
  1425. # x = L*(1-f)/2.
  1426. # B: start point
  1427. # w: half-width
  1428. # L: total length
  1429. # s: length of first bar
  1430. # P0: start of dashpot (B[0]+s)
  1431. # P1: end of dashpot
  1432. # P2: end point
  1433. shapes = {}
  1434. # dashpot is P0-P1 in y and width 2*w
  1435. if dashpot_length is None:
  1436. if s is None:
  1437. f = Dashpot.dashpot_fraction
  1438. s = L*(1-f)/2. # default
  1439. P1 = (B[0], B[1]+L-s)
  1440. dashpot_length = f*L
  1441. else:
  1442. if s is None:
  1443. f = 1./2 # the bar lengths are taken as f*dashpot_length
  1444. s = f*dashpot_length # default
  1445. P1 = (B[0], B[1]+s+dashpot_length)
  1446. P0 = (B[0], B[1]+s)
  1447. P2 = (B[0], B[1]+L)
  1448. if P2[1] > P1[1] > P0[1]:
  1449. pass # ok
  1450. else:
  1451. raise ValueError('Dashpot has inconsistent dimensions! start: %g, dashpot begin: %g, dashpot end: %g, very end: %g' % (B[1], P0[1], P1[1], P2[1]))
  1452. shapes['line start'] = Line(B, P0)
  1453. shapes['pot'] = Curve([P1[0]-w, P0[0]-w, P0[0]+w, P1[0]+w],
  1454. [P1[1], P0[1], P0[1], P1[1]])
  1455. piston_thickness = dashpot_length*Dashpot.piston_thickness_fraction
  1456. if piston_pos is None:
  1457. piston_pos = 1/3.*dashpot_length
  1458. if piston_pos < 0:
  1459. piston_pos = 0
  1460. elif piston_pos > dashpot_length:
  1461. piston_pos = dashpot_length - piston_thickness
  1462. abs_piston_pos = P0[1] + piston_pos
  1463. gap = w*Dashpot.piston_gap_fraction
  1464. shapes['piston'] = Composition(
  1465. {'line': Line(P2, (B[0], abs_piston_pos + piston_thickness)),
  1466. 'rectangle': Rectangle((B[0] - w+gap, abs_piston_pos),
  1467. 2*w-2*gap, piston_thickness),
  1468. })
  1469. shapes['piston']['rectangle'].set_filled_curves(pattern='X')
  1470. self.shapes = shapes
  1471. self.bar_length = s
  1472. self.width = 2*w
  1473. self.piston_pos = piston_pos
  1474. self.dashpot_length = dashpot_length
  1475. # Dimensions
  1476. start = Text_wArrow('start', (B[0]-1.5*w,B[1]-1.5*w), B)
  1477. width = Distance_wText((B[0]-w, B[1]-3.5*w), (B[0]+w, B[1]-3.5*w),
  1478. 'width')
  1479. dplength = Distance_wText((B[0]+2*w, P0[1]), (B[0]+2*w, P1[1]),
  1480. 'dashpot_length', text_pos=(B[0]+w,B[1]-w))
  1481. blength = Distance_wText((B[0]-2*w, B[1]), (B[0]-2*w, P0[1]),
  1482. 'bar_length', text_pos=(B[0]-6*w,P0[1]-w))
  1483. ppos = Distance_wText((B[0]-2*w, P0[1]), (B[0]-2*w, P0[1]+piston_pos),
  1484. 'piston_pos', text_pos=(B[0]-6*w,P0[1]+piston_pos-w))
  1485. tlength = Distance_wText((B[0]+4*w, B[1]), (B[0]+4*w, B[1]+L),
  1486. 'total_length',
  1487. text_pos=(B[0]+4.5*w, B[1]+L-2*w))
  1488. line = Line((B[0]+w, abs_piston_pos), (B[0]+7*w, abs_piston_pos)).set_linestyle('dashed').set_linecolor('black').set_linewidth(1)
  1489. pp = Text('abs_piston_pos', (B[0]+7*w, abs_piston_pos), alignment='left')
  1490. dims = {'start': start, 'width': width, 'dashpot_length': dplength,
  1491. 'bar_length': blength, 'total_length': tlength,
  1492. 'piston_pos': ppos,}
  1493. #'abs_piston_pos': Composition({'line': line, 'text': pp})}
  1494. self.dimensions = dims
  1495. def geometric_features(self):
  1496. """
  1497. Recorded geometric features:
  1498. ==================== =============================================
  1499. Attribute Description
  1500. ==================== =============================================
  1501. start Start point of dashpot.
  1502. end End point of dashpot.
  1503. bar_length Length of first bar (from start to spring).
  1504. dashpot_length Length of dashpot middle part.
  1505. width Total width of dashpot.
  1506. piston_pos Position of piston in dashpot, relative to
  1507. start[1] + bar_length.
  1508. ==================== =============================================
  1509. """
  1510. d = {'start': self.shapes['line start'].geometric_features()['start'],
  1511. 'end': self.shapes['piston']['line'].geometric_features()['start'],
  1512. 'bar_length': self.bar_length,
  1513. 'piston_pos': self.piston_pos,
  1514. 'width': self.width,
  1515. 'dashpot_length': self.dashpot_length,
  1516. }
  1517. return d
  1518. # COMPOSITE types:
  1519. # MassSpringForce: Line(horizontal), Spring, Rectangle, Arrow/Line(w/arrow)
  1520. # must be easy to find the tip of the arrow
  1521. # Maybe extra dict: self.name['mass'] = Rectangle object - YES!
  1522. def test_Axis():
  1523. drawing_tool.set_coordinate_system(
  1524. xmin=0, xmax=15, ymin=-7, ymax=8, axis=True,
  1525. instruction_file='tmp_Axis.py')
  1526. x_axis = Axis((7.5,2), 5, 'x', rotation_angle=0)
  1527. y_axis = Axis((7.5,2), 5, 'y', rotation_angle=90)
  1528. system = Composition({'x axis': x_axis, 'y axis': y_axis})
  1529. system.draw()
  1530. drawing_tool.display()
  1531. system.set_linestyle('dashed')
  1532. system.rotate(40, (7.5,2))
  1533. system.draw()
  1534. drawing_tool.display()
  1535. system.set_linestyle('dotted')
  1536. system.rotate(220, (7.5,2))
  1537. system.draw()
  1538. drawing_tool.display()
  1539. drawing_tool.display('Axis')
  1540. drawing_tool.savefig('tmp_Axis.png')
  1541. print repr(system)
  1542. def test_Distance_wText():
  1543. drawing_tool.set_coordinate_system(xmin=0, xmax=10,
  1544. ymin=0, ymax=6,
  1545. axis=True,
  1546. instruction_file='tmp_Distance_wText.py')
  1547. #drawing_tool.arrow_head_width = 0.1
  1548. fontsize=14
  1549. t = r'$ 2\pi R^2 $'
  1550. dims2 = Composition({
  1551. 'a0': Distance_wText((4,5), (8, 5), t, fontsize),
  1552. 'a6': Distance_wText((4,5), (4, 4), t, fontsize),
  1553. 'a1': Distance_wText((0,2), (2, 4.5), t, fontsize),
  1554. 'a2': Distance_wText((0,2), (2, 0), t, fontsize),
  1555. 'a3': Distance_wText((2,4.5), (0, 5.5), t, fontsize),
  1556. 'a4': Distance_wText((8,4), (10, 3), t, fontsize,
  1557. text_spacing=-1./60),
  1558. 'a5': Distance_wText((8,2), (10, 1), t, fontsize,
  1559. text_spacing=-1./40, alignment='right'),
  1560. 'c1': Text_wArrow('text_spacing=-1./60',
  1561. (4, 3.5), (9, 3.2),
  1562. fontsize=10, alignment='left'),
  1563. 'c2': Text_wArrow('text_spacing=-1./40, alignment="right"',
  1564. (4, 0.5), (9, 1.2),
  1565. fontsize=10, alignment='left'),
  1566. })
  1567. dims2.draw()
  1568. drawing_tool.display('Distance_wText and text positioning')
  1569. drawing_tool.savefig('tmp_Distance_wText.png')
  1570. def test_Rectangle():
  1571. L = 3.0
  1572. W = 4.0
  1573. drawing_tool.set_coordinate_system(xmin=0, xmax=2*W,
  1574. ymin=-L/2, ymax=2*L,
  1575. axis=True,
  1576. instruction_file='tmp_Rectangle.py')
  1577. drawing_tool.set_linecolor('blue')
  1578. drawing_tool.set_grid(True)
  1579. xpos = W/2
  1580. r = Rectangle(lower_left_corner=(xpos,0), width=W, height=L)
  1581. r.draw()
  1582. r.draw_dimensions()
  1583. drawing_tool.display('Rectangle')
  1584. drawing_tool.savefig('tmp_Rectangle.png')
  1585. def test_Triangle():
  1586. L = 3.0
  1587. W = 4.0
  1588. drawing_tool.set_coordinate_system(xmin=0, xmax=2*W,
  1589. ymin=-L/2, ymax=1.2*L,
  1590. axis=True,
  1591. instruction_file='tmp_Triangle.py')
  1592. drawing_tool.set_linecolor('blue')
  1593. drawing_tool.set_grid(True)
  1594. xpos = 1
  1595. t = Triangle(p1=(W/2,0), p2=(3*W/2,W/2), p3=(4*W/5.,L))
  1596. t.draw()
  1597. t.draw_dimensions()
  1598. drawing_tool.display('Triangle')
  1599. drawing_tool.savefig('tmp_Triangle.png')
  1600. def test_Arc():
  1601. L = 4.0
  1602. W = 4.0
  1603. drawing_tool.set_coordinate_system(xmin=-W/2, xmax=W,
  1604. ymin=-L/2, ymax=1.5*L,
  1605. axis=True,
  1606. instruction_file='tmp_Arc.py')
  1607. drawing_tool.set_linecolor('blue')
  1608. drawing_tool.set_grid(True)
  1609. center = point(0,0)
  1610. radius = L/2
  1611. start_angle = 60
  1612. arc_angle = 45
  1613. a = Arc(center, radius, start_angle, arc_angle)
  1614. a.set_arrow('->')
  1615. a.draw()
  1616. R1 = 1.25*radius
  1617. R2 = 1.5*radius
  1618. R = 2*radius
  1619. a.dimensions = {
  1620. 'start_angle': Arc_wText(
  1621. 'start_angle', center, R1, start_angle=0,
  1622. arc_angle=start_angle, text_spacing=1/10.),
  1623. 'arc_angle': Arc_wText(
  1624. 'arc_angle', center, R2, start_angle=start_angle,
  1625. arc_angle=arc_angle, text_spacing=1/20.),
  1626. 'r=0': Line(center, center +
  1627. point(R*cos(radians(start_angle)),
  1628. R*sin(radians(start_angle)))),
  1629. 'r=start_angle': Line(center, center +
  1630. point(R*cos(radians(start_angle+arc_angle)),
  1631. R*sin(radians(start_angle+arc_angle)))),
  1632. 'r=start+arc_angle': Line(center, center +
  1633. point(R, 0)).set_linestyle('dashed'),
  1634. 'radius': Distance_wText(center, a(0), 'radius', text_spacing=1/40.),
  1635. 'center': Text('center', center-point(radius/10., radius/10.)),
  1636. }
  1637. for dimension in a.dimensions:
  1638. dim = a.dimensions[dimension]
  1639. dim.set_linestyle('dashed')
  1640. dim.set_linewidth(1)
  1641. dim.set_linecolor('black')
  1642. a.draw_dimensions()
  1643. drawing_tool.display('Arc')
  1644. drawing_tool.savefig('tmp_Arc.png')
  1645. def test_Spring():
  1646. L = 5.0
  1647. W = 2.0
  1648. drawing_tool.set_coordinate_system(xmin=0, xmax=7*W,
  1649. ymin=-L/2, ymax=1.5*L,
  1650. axis=True,
  1651. instruction_file='tmp_Spring.py')
  1652. drawing_tool.set_linecolor('blue')
  1653. drawing_tool.set_grid(True)
  1654. xpos = W
  1655. s1 = Spring((W,0), L, teeth=True)
  1656. s1_title = Text('Default Spring', s1.geometric_features()['end'] + point(0,L/10))
  1657. s1.draw()
  1658. s1_title.draw()
  1659. #s1.draw_dimensions()
  1660. xpos += 3*W
  1661. s2 = Spring(start=(xpos,0), length=L, width=W/2.,
  1662. bar_length=L/6., teeth=False)
  1663. s2.draw()
  1664. s2.draw_dimensions()
  1665. drawing_tool.display('Spring')
  1666. drawing_tool.savefig('tmp_Spring.png')
  1667. def test_Dashpot():
  1668. L = 5.0
  1669. W = 2.0
  1670. xpos = 0
  1671. drawing_tool.set_coordinate_system(xmin=xpos, xmax=xpos+5.5*W,
  1672. ymin=-L/2, ymax=1.5*L,
  1673. axis=True,
  1674. instruction_file='tmp_Dashpot.py')
  1675. drawing_tool.set_linecolor('blue')
  1676. drawing_tool.set_grid(True)
  1677. # Default (simple) dashpot
  1678. xpos = 1.5
  1679. d1 = Dashpot(start=(xpos,0), total_length=L)
  1680. d1_title = Text('Dashpot (default)', d1.geometric_features()['end'] + point(0,L/10))
  1681. d1.draw()
  1682. d1_title.draw()
  1683. # Dashpot for animation with fixed bar_length, dashpot_length and
  1684. # prescribed piston_pos
  1685. xpos += 2.5*W
  1686. d2 = Dashpot(start=(xpos,0), total_length=1.2*L, width=W/2,
  1687. bar_length=W, dashpot_length=L/2, piston_pos=2*W)
  1688. d2.draw()
  1689. d2.draw_dimensions()
  1690. drawing_tool.display('Dashpot')
  1691. drawing_tool.savefig('tmp_Dashpot.png')
  1692. def diff_files(files1, files2, mode='HTML'):
  1693. import difflib, time
  1694. n = 3
  1695. for fromfile, tofile in zip(files1, files2):
  1696. fromdate = time.ctime(os.stat(fromfile).st_mtime)
  1697. todate = time.ctime(os.stat(tofile).st_mtime)
  1698. fromlines = open(fromfile, 'U').readlines()
  1699. tolines = open(tofile, 'U').readlines()
  1700. diff_html = difflib.HtmlDiff().\
  1701. make_file(fromlines,tolines,
  1702. fromfile,tofile,context=True,numlines=n)
  1703. diff_plain = difflib.unified_diff(fromlines, tolines, fromfile, tofile, fromdate, todate, n=n)
  1704. filename_plain = fromfile + '.diff.txt'
  1705. filename_html = fromfile + '.diff.html'
  1706. if os.path.isfile(filename_plain):
  1707. os.remove(filename_plain)
  1708. if os.path.isfile(filename_html):
  1709. os.remove(filename_html)
  1710. f = open(filename_plain, 'w')
  1711. f.writelines(diff_plain)
  1712. f.close()
  1713. size = os.path.getsize(filename_plain)
  1714. if size > 4:
  1715. print 'found differences:', fromfile, tofile
  1716. f = open(filename_html, 'w')
  1717. f.writelines(diff_html)
  1718. f.close()
  1719. def test_test():
  1720. os.chdir('test')
  1721. funcs = [name for name in globals() if name.startswith('test_') and callable(globals()[name])]
  1722. funcs.remove('test_test')
  1723. new_files = []
  1724. res_files = []
  1725. for func in funcs:
  1726. mplfile = func.replace('test_', 'tmp_') + '.py'
  1727. #exec(func + '()')
  1728. new_files.append(mplfile)
  1729. resfile = mplfile.replace('tmp_', 'res_')
  1730. res_files.append(resfile)
  1731. diff_files(new_files, res_files)
  1732. def _test1():
  1733. set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
  1734. l1 = Line((0,0), (1,1))
  1735. l1.draw()
  1736. input(': ')
  1737. c1 = Circle((5,2), 1)
  1738. c2 = Circle((6,2), 1)
  1739. w1 = Wheel((7,2), 1)
  1740. c1.draw()
  1741. c2.draw()
  1742. w1.draw()
  1743. hardcopy()
  1744. display() # show the plot
  1745. def _test2():
  1746. set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
  1747. l1 = Line((0,0), (1,1))
  1748. l1.draw()
  1749. input(': ')
  1750. c1 = Circle((5,2), 1)
  1751. c2 = Circle((6,2), 1)
  1752. w1 = Wheel((7,2), 1)
  1753. filled_curves(True)
  1754. set_linecolor('blue')
  1755. c1.draw()
  1756. set_linecolor('aqua')
  1757. c2.draw()
  1758. filled_curves(False)
  1759. set_linecolor('red')
  1760. w1.draw()
  1761. hardcopy()
  1762. display() # show the plot
  1763. def _test3():
  1764. """Test example from the book."""
  1765. set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
  1766. l1 = Line(start=(0,0), stop=(1,1)) # define line
  1767. l1.draw() # make plot data
  1768. r1 = Rectangle(lower_left_corner=(0,1), width=3, height=5)
  1769. r1.draw()
  1770. Circle(center=(5,7), radius=1).draw()
  1771. Wheel(center=(6,2), radius=2, inner_radius=0.5, nlines=7).draw()
  1772. hardcopy()
  1773. display()
  1774. def _test4():
  1775. """Second example from the book."""
  1776. set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
  1777. r1 = Rectangle(lower_left_corner=(0,1), width=3, height=5)
  1778. c1 = Circle(center=(5,7), radius=1)
  1779. w1 = Wheel(center=(6,2), radius=2, inner_radius=0.5, nlines=7)
  1780. c2 = Circle(center=(7,7), radius=1)
  1781. filled_curves(True)
  1782. c1.draw()
  1783. set_linecolor('blue')
  1784. r1.draw()
  1785. set_linecolor('aqua')
  1786. c2.draw()
  1787. # Add thick aqua line around rectangle:
  1788. filled_curves(False)
  1789. set_linewidth(4)
  1790. r1.draw()
  1791. set_linecolor('red')
  1792. # Draw wheel with thick lines:
  1793. w1.draw()
  1794. hardcopy('tmp_colors')
  1795. display()
  1796. def _test5():
  1797. set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
  1798. c = 6. # center point of box
  1799. w = 2. # size of box
  1800. L = 3
  1801. r1 = Rectangle((c-w/2, c-w/2), w, w)
  1802. l1 = Line((c,c-w/2), (c,c-w/2-L))
  1803. linecolor('blue')
  1804. filled_curves(True)
  1805. r1.draw()
  1806. linecolor('aqua')
  1807. filled_curves(False)
  1808. l1.draw()
  1809. hardcopy()
  1810. display() # show the plot
  1811. def rolling_wheel(total_rotation_angle):
  1812. """Animation of a rotating wheel."""
  1813. set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
  1814. import time
  1815. center = (6,2)
  1816. radius = 2.0
  1817. angle = 2.0
  1818. pngfiles = []
  1819. w1 = Wheel(center=center, radius=radius, inner_radius=0.5, nlines=7)
  1820. for i in range(int(total_rotation_angle/angle)):
  1821. w1.draw()
  1822. print 'XXXXXXXXXXXXXXXXXXXXXX BIG PROBLEM WITH ANIMATE!!!'
  1823. display()
  1824. filename = 'tmp_%03d' % i
  1825. pngfiles.append(filename + '.png')
  1826. hardcopy(filename)
  1827. time.sleep(0.3) # pause
  1828. L = radius*angle*pi/180 # translation = arc length
  1829. w1.rotate(angle, center)
  1830. w1.translate((-L, 0))
  1831. center = (center[0] - L, center[1])
  1832. erase()
  1833. cmd = 'convert -delay 50 -loop 1000 %s tmp_movie.gif' \
  1834. % (' '.join(pngfiles))
  1835. print 'converting PNG files to animated GIF:\n', cmd
  1836. import commands
  1837. failure, output = commands.getstatusoutput(cmd)
  1838. if failure: print 'Could not run', cmd
  1839. if __name__ == '__main__':
  1840. #rolling_wheel(40)
  1841. #_test1()
  1842. #_test3()
  1843. funcs = [
  1844. #test_Axis,
  1845. test_inclined_plane,
  1846. ]
  1847. for func in funcs:
  1848. func()
  1849. raw_input('Type Return: ')