hatch.py 6.7 KB

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