shapes.py 65 KB

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