shapes.py 55 KB

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