hatch.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. """
  2. Contains a classes for generating hatch patterns.
  3. """
  4. import numpy as np
  5. from matplotlib.path import Path
  6. class HatchPatternBase:
  7. """
  8. The base class for a hatch pattern.
  9. """
  10. pass
  11. class HorizontalHatch(HatchPatternBase):
  12. def __init__(self, hatch, density):
  13. self.num_lines = int((hatch.count('-') + hatch.count('+')) * density)
  14. self.num_vertices = self.num_lines * 2
  15. def set_vertices_and_codes(self, vertices, codes):
  16. steps, stepsize = np.linspace(0.0, 1.0, self.num_lines, False,
  17. retstep=True)
  18. steps += stepsize / 2.
  19. vertices[0::2, 0] = 0.0
  20. vertices[0::2, 1] = steps
  21. vertices[1::2, 0] = 1.0
  22. vertices[1::2, 1] = steps
  23. codes[0::2] = Path.MOVETO
  24. codes[1::2] = Path.LINETO
  25. class VerticalHatch(HatchPatternBase):
  26. def __init__(self, hatch, density):
  27. self.num_lines = int((hatch.count('|') + hatch.count('+')) * density)
  28. self.num_vertices = self.num_lines * 2
  29. def set_vertices_and_codes(self, vertices, codes):
  30. steps, stepsize = np.linspace(0.0, 1.0, self.num_lines, False,
  31. retstep=True)
  32. steps += stepsize / 2.
  33. vertices[0::2, 0] = steps
  34. vertices[0::2, 1] = 0.0
  35. vertices[1::2, 0] = steps
  36. vertices[1::2, 1] = 1.0
  37. codes[0::2] = Path.MOVETO
  38. codes[1::2] = Path.LINETO
  39. class NorthEastHatch(HatchPatternBase):
  40. def __init__(self, hatch, density):
  41. self.num_lines = int((hatch.count('/') + hatch.count('x') +
  42. hatch.count('X')) * density)
  43. if self.num_lines:
  44. self.num_vertices = (self.num_lines + 1) * 2
  45. else:
  46. self.num_vertices = 0
  47. def set_vertices_and_codes(self, vertices, codes):
  48. steps = np.linspace(-0.5, 0.5, self.num_lines + 1)
  49. vertices[0::2, 0] = 0.0 + steps
  50. vertices[0::2, 1] = 0.0 - steps
  51. vertices[1::2, 0] = 1.0 + steps
  52. vertices[1::2, 1] = 1.0 - steps
  53. codes[0::2] = Path.MOVETO
  54. codes[1::2] = Path.LINETO
  55. class SouthEastHatch(HatchPatternBase):
  56. def __init__(self, hatch, density):
  57. self.num_lines = int((hatch.count('\\') + hatch.count('x') +
  58. hatch.count('X')) * density)
  59. self.num_vertices = (self.num_lines + 1) * 2
  60. if self.num_lines:
  61. self.num_vertices = (self.num_lines + 1) * 2
  62. else:
  63. self.num_vertices = 0
  64. def set_vertices_and_codes(self, vertices, codes):
  65. steps = np.linspace(-0.5, 0.5, self.num_lines + 1)
  66. vertices[0::2, 0] = 0.0 + steps
  67. vertices[0::2, 1] = 1.0 + steps
  68. vertices[1::2, 0] = 1.0 + steps
  69. vertices[1::2, 1] = 0.0 + steps
  70. codes[0::2] = Path.MOVETO
  71. codes[1::2] = Path.LINETO
  72. class Shapes(HatchPatternBase):
  73. filled = False
  74. def __init__(self, hatch, density):
  75. if self.num_rows == 0:
  76. self.num_shapes = 0
  77. self.num_vertices = 0
  78. else:
  79. self.num_shapes = ((self.num_rows // 2 + 1) * (self.num_rows + 1) +
  80. (self.num_rows // 2) * (self.num_rows))
  81. self.num_vertices = (self.num_shapes *
  82. len(self.shape_vertices) *
  83. (1 if self.filled else 2))
  84. def set_vertices_and_codes(self, vertices, codes):
  85. offset = 1.0 / self.num_rows
  86. shape_vertices = self.shape_vertices * offset * self.size
  87. if not self.filled:
  88. inner_vertices = shape_vertices[::-1] * 0.9
  89. shape_codes = self.shape_codes
  90. shape_size = len(shape_vertices)
  91. cursor = 0
  92. for row in range(self.num_rows + 1):
  93. if row % 2 == 0:
  94. cols = np.linspace(0, 1, self.num_rows + 1)
  95. else:
  96. cols = np.linspace(offset / 2, 1 - offset / 2, self.num_rows)
  97. row_pos = row * offset
  98. for col_pos in cols:
  99. vertices[cursor:cursor + shape_size] = (shape_vertices +
  100. (col_pos, row_pos))
  101. codes[cursor:cursor + shape_size] = shape_codes
  102. cursor += shape_size
  103. if not self.filled:
  104. vertices[cursor:cursor + shape_size] = (inner_vertices +
  105. (col_pos, row_pos))
  106. codes[cursor:cursor + shape_size] = shape_codes
  107. cursor += shape_size
  108. class Circles(Shapes):
  109. def __init__(self, hatch, density):
  110. path = Path.unit_circle()
  111. self.shape_vertices = path.vertices
  112. self.shape_codes = path.codes
  113. Shapes.__init__(self, hatch, density)
  114. class SmallCircles(Circles):
  115. size = 0.2
  116. def __init__(self, hatch, density):
  117. self.num_rows = (hatch.count('o')) * density
  118. Circles.__init__(self, hatch, density)
  119. class LargeCircles(Circles):
  120. size = 0.35
  121. def __init__(self, hatch, density):
  122. self.num_rows = (hatch.count('O')) * density
  123. Circles.__init__(self, hatch, density)
  124. class SmallFilledCircles(SmallCircles):
  125. size = 0.1
  126. filled = True
  127. def __init__(self, hatch, density):
  128. self.num_rows = (hatch.count('.')) * density
  129. Circles.__init__(self, hatch, density)
  130. class Stars(Shapes):
  131. size = 1.0 / 3.0
  132. filled = True
  133. def __init__(self, hatch, density):
  134. self.num_rows = (hatch.count('*')) * density
  135. path = Path.unit_regular_star(5)
  136. self.shape_vertices = path.vertices
  137. self.shape_codes = np.full(len(self.shape_vertices), Path.LINETO,
  138. dtype=Path.code_type)
  139. self.shape_codes[0] = Path.MOVETO
  140. Shapes.__init__(self, hatch, density)
  141. _hatch_types = [
  142. HorizontalHatch,
  143. VerticalHatch,
  144. NorthEastHatch,
  145. SouthEastHatch,
  146. SmallCircles,
  147. LargeCircles,
  148. SmallFilledCircles,
  149. Stars
  150. ]
  151. def get_path(hatchpattern, density=6):
  152. """
  153. Given a hatch specifier, *hatchpattern*, generates Path to render
  154. the hatch in a unit square. *density* is the number of lines per
  155. unit square.
  156. """
  157. density = int(density)
  158. patterns = [hatch_type(hatchpattern, density)
  159. for hatch_type in _hatch_types]
  160. num_vertices = sum([pattern.num_vertices for pattern in patterns])
  161. if num_vertices == 0:
  162. return Path(np.empty((0, 2)))
  163. vertices = np.empty((num_vertices, 2))
  164. codes = np.empty(num_vertices, Path.code_type)
  165. cursor = 0
  166. for pattern in patterns:
  167. if pattern.num_vertices != 0:
  168. vertices_chunk = vertices[cursor:cursor + pattern.num_vertices]
  169. codes_chunk = codes[cursor:cursor + pattern.num_vertices]
  170. pattern.set_vertices_and_codes(vertices_chunk, codes_chunk)
  171. cursor += pattern.num_vertices
  172. return Path(vertices, codes)