box.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. from typing import List, Sequence, TYPE_CHECKING, Iterable, Tuple
  2. import math
  3. from .vector import Vec2
  4. from .bbox import BoundingBox2d
  5. from .line import ConstructionLine
  6. from .construct2d import left_of_line, ConstructionTool
  7. if TYPE_CHECKING:
  8. from ezdxf.eztypes import Vertex
  9. class ConstructionBox(ConstructionTool):
  10. def __init__(self, center: 'Vertex' = (0, 0), width: float = 1, height: float = 1, angle: float = 0):
  11. self._center = Vec2(center)
  12. self._width = abs(width) # type: float
  13. self._height = abs(height) # type: float
  14. self._angle = angle # type: float # in degrees
  15. self._corners = None # type: Tuple[Vec2, Vec2, Vec2, Vec2]
  16. self._tainted = True
  17. @classmethod
  18. def from_points(cls, p1: 'Vertex', p2: 'Vertex') -> 'ConstructionBox':
  19. p1 = Vec2(p1)
  20. p2 = Vec2(p2)
  21. width = abs(p2.x - p1.x)
  22. height = abs(p2.y - p1.y)
  23. center = p1.lerp(p2)
  24. return cls(center=center, width=width, height=height)
  25. def update(self) -> None:
  26. if not self._tainted:
  27. return
  28. center = self.center
  29. w2 = Vec2.from_deg_angle(self._angle, self._width / 2.)
  30. h2 = Vec2.from_deg_angle(self._angle + 90, self._height / 2.)
  31. self._corners = (
  32. center - w2 - h2, # lower left
  33. center + w2 - h2, # lower right
  34. center + w2 + h2, # upper right
  35. center - w2 + h2, # upper left
  36. )
  37. self._tainted = False
  38. @property
  39. def bounding_box(self) -> BoundingBox2d:
  40. return BoundingBox2d(self.corners)
  41. @property
  42. def center(self) -> Vec2:
  43. return self._center
  44. @center.setter
  45. def center(self, c: 'Vertex') -> None:
  46. self._center = Vec2(c)
  47. self._tainted = True
  48. @property
  49. def width(self) -> float:
  50. return self._width
  51. @width.setter
  52. def width(self, w: float) -> None:
  53. self._width = abs(w)
  54. self._tainted = True
  55. @property
  56. def height(self) -> float:
  57. return self._height
  58. @height.setter
  59. def height(self, h: float) -> None:
  60. self._height = abs(h)
  61. self._tainted = True
  62. @property
  63. def incircle_radius(self) -> float:
  64. return min(self._width, self._height) / 2.
  65. @property
  66. def circumcircle_radius(self) -> float:
  67. return math.hypot(self._width, self._height) / 2.
  68. @property
  69. def angle(self) -> float:
  70. return self._angle
  71. @angle.setter
  72. def angle(self, a: float) -> None:
  73. self._angle = a
  74. self._tainted = True
  75. @property
  76. def corners(self) -> Sequence[Vec2]:
  77. self.update()
  78. return self._corners
  79. def __iter__(self) -> Iterable[Vec2]:
  80. return iter(self.corners)
  81. def __getitem__(self, corner) -> Vec2:
  82. return self.corners[corner]
  83. def __repr__(self) -> str:
  84. return "ConstructionBox({0.center}, {0.width}, {0.height}, {0.angle})".format(self)
  85. def move(self, dx: float, dy: float) -> None:
  86. self.center += Vec2((dx, dy))
  87. def expand(self, dw: float, dh: float) -> None:
  88. self.width += dw
  89. self.height += dh
  90. def scale(self, sx: float, sy: float) -> None:
  91. self.width *= sx
  92. self.height *= sy
  93. def rotate(self, angle: float) -> None:
  94. self.angle += angle
  95. def is_inside(self, point: 'Vertex') -> bool:
  96. point = Vec2(point)
  97. delta = self.center - point
  98. if math.isclose(self.angle, 0.): # fast path for horizontal rectangles
  99. return abs(delta.x) <= (self._width / 2.) and abs(delta.y) <= (self._height / 2.)
  100. else:
  101. distance = delta.magnitude
  102. if distance > self.circumcircle_radius:
  103. return False
  104. elif distance <= self.incircle_radius:
  105. return True
  106. else:
  107. # inside if point is "left of line" of all border lines.
  108. p1, p2, p3, p4 = self.corners
  109. return all(
  110. (left_of_line(point, a, b, online=True) for a, b in [(p1, p2), (p2, p3), (p3, p4), (p4, p1)])
  111. )
  112. def is_any_corner_inside(self, other: 'ConstructionBox') -> bool:
  113. return any(self.is_inside(p) for p in other.corners)
  114. def is_overlapping(self, other: 'ConstructionBox') -> bool:
  115. distance = (self.center - other.center).magnitude
  116. max_distance = self.circumcircle_radius + other.circumcircle_radius
  117. if distance > max_distance:
  118. return False
  119. min_distance = self.incircle_radius + other.incircle_radius
  120. if distance <= min_distance:
  121. return True
  122. if self.is_any_corner_inside(other):
  123. return True
  124. if other.is_any_corner_inside(self):
  125. return True
  126. # no corner inside of any box, maybe crossing boxes?
  127. # check intersection of diagonals
  128. c1, c2, c3, c4 = self.corners
  129. diag1 = ConstructionLine(c1, c3)
  130. diag2 = ConstructionLine(c2, c4)
  131. t1, t2, t3, t4 = other.corners
  132. test_diag = ConstructionLine(t1, t3)
  133. if test_diag.has_intersection(diag1) or test_diag.has_intersection(diag2):
  134. return True
  135. test_diag = ConstructionLine(t2, t4)
  136. if test_diag.has_intersection(diag1) or test_diag.has_intersection(diag2):
  137. return True
  138. return False
  139. def border_lines(self) -> Sequence[ConstructionLine]:
  140. p1, p2, p3, p4 = self.corners
  141. return (
  142. ConstructionLine(p1, p2),
  143. ConstructionLine(p2, p3),
  144. ConstructionLine(p3, p4),
  145. ConstructionLine(p4, p1),
  146. )
  147. def intersect(self, line: ConstructionLine) -> List[Vec2]:
  148. """
  149. Returns 0, 1 or 2 intersection points between `line` and `TextBox` border lines.
  150. Args:
  151. line: line to intersect with border lines
  152. Returns: list of intersection points
  153. """
  154. result = set()
  155. for border_line in self.border_lines():
  156. p = line.intersect(border_line)
  157. if p is not None:
  158. result.add(p)
  159. return sorted(result)