123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- from typing import List, Sequence, TYPE_CHECKING, Iterable, Tuple
- import math
- from .vector import Vec2
- from .bbox import BoundingBox2d
- from .line import ConstructionLine
- from .construct2d import left_of_line, ConstructionTool
- if TYPE_CHECKING:
- from ezdxf.eztypes import Vertex
- class ConstructionBox(ConstructionTool):
- def __init__(self, center: 'Vertex' = (0, 0), width: float = 1, height: float = 1, angle: float = 0):
- self._center = Vec2(center)
- self._width = abs(width) # type: float
- self._height = abs(height) # type: float
- self._angle = angle # type: float # in degrees
- self._corners = None # type: Tuple[Vec2, Vec2, Vec2, Vec2]
- self._tainted = True
- @classmethod
- def from_points(cls, p1: 'Vertex', p2: 'Vertex') -> 'ConstructionBox':
- p1 = Vec2(p1)
- p2 = Vec2(p2)
- width = abs(p2.x - p1.x)
- height = abs(p2.y - p1.y)
- center = p1.lerp(p2)
- return cls(center=center, width=width, height=height)
- def update(self) -> None:
- if not self._tainted:
- return
- center = self.center
- w2 = Vec2.from_deg_angle(self._angle, self._width / 2.)
- h2 = Vec2.from_deg_angle(self._angle + 90, self._height / 2.)
- self._corners = (
- center - w2 - h2, # lower left
- center + w2 - h2, # lower right
- center + w2 + h2, # upper right
- center - w2 + h2, # upper left
- )
- self._tainted = False
- @property
- def bounding_box(self) -> BoundingBox2d:
- return BoundingBox2d(self.corners)
- @property
- def center(self) -> Vec2:
- return self._center
- @center.setter
- def center(self, c: 'Vertex') -> None:
- self._center = Vec2(c)
- self._tainted = True
- @property
- def width(self) -> float:
- return self._width
- @width.setter
- def width(self, w: float) -> None:
- self._width = abs(w)
- self._tainted = True
- @property
- def height(self) -> float:
- return self._height
- @height.setter
- def height(self, h: float) -> None:
- self._height = abs(h)
- self._tainted = True
- @property
- def incircle_radius(self) -> float:
- return min(self._width, self._height) / 2.
- @property
- def circumcircle_radius(self) -> float:
- return math.hypot(self._width, self._height) / 2.
- @property
- def angle(self) -> float:
- return self._angle
- @angle.setter
- def angle(self, a: float) -> None:
- self._angle = a
- self._tainted = True
- @property
- def corners(self) -> Sequence[Vec2]:
- self.update()
- return self._corners
- def __iter__(self) -> Iterable[Vec2]:
- return iter(self.corners)
- def __getitem__(self, corner) -> Vec2:
- return self.corners[corner]
- def __repr__(self) -> str:
- return "ConstructionBox({0.center}, {0.width}, {0.height}, {0.angle})".format(self)
- def move(self, dx: float, dy: float) -> None:
- self.center += Vec2((dx, dy))
- def expand(self, dw: float, dh: float) -> None:
- self.width += dw
- self.height += dh
- def scale(self, sx: float, sy: float) -> None:
- self.width *= sx
- self.height *= sy
- def rotate(self, angle: float) -> None:
- self.angle += angle
- def is_inside(self, point: 'Vertex') -> bool:
- point = Vec2(point)
- delta = self.center - point
- if math.isclose(self.angle, 0.): # fast path for horizontal rectangles
- return abs(delta.x) <= (self._width / 2.) and abs(delta.y) <= (self._height / 2.)
- else:
- distance = delta.magnitude
- if distance > self.circumcircle_radius:
- return False
- elif distance <= self.incircle_radius:
- return True
- else:
- # inside if point is "left of line" of all border lines.
- p1, p2, p3, p4 = self.corners
- return all(
- (left_of_line(point, a, b, online=True) for a, b in [(p1, p2), (p2, p3), (p3, p4), (p4, p1)])
- )
- def is_any_corner_inside(self, other: 'ConstructionBox') -> bool:
- return any(self.is_inside(p) for p in other.corners)
- def is_overlapping(self, other: 'ConstructionBox') -> bool:
- distance = (self.center - other.center).magnitude
- max_distance = self.circumcircle_radius + other.circumcircle_radius
- if distance > max_distance:
- return False
- min_distance = self.incircle_radius + other.incircle_radius
- if distance <= min_distance:
- return True
- if self.is_any_corner_inside(other):
- return True
- if other.is_any_corner_inside(self):
- return True
- # no corner inside of any box, maybe crossing boxes?
- # check intersection of diagonals
- c1, c2, c3, c4 = self.corners
- diag1 = ConstructionLine(c1, c3)
- diag2 = ConstructionLine(c2, c4)
- t1, t2, t3, t4 = other.corners
- test_diag = ConstructionLine(t1, t3)
- if test_diag.has_intersection(diag1) or test_diag.has_intersection(diag2):
- return True
- test_diag = ConstructionLine(t2, t4)
- if test_diag.has_intersection(diag1) or test_diag.has_intersection(diag2):
- return True
- return False
- def border_lines(self) -> Sequence[ConstructionLine]:
- p1, p2, p3, p4 = self.corners
- return (
- ConstructionLine(p1, p2),
- ConstructionLine(p2, p3),
- ConstructionLine(p3, p4),
- ConstructionLine(p4, p1),
- )
- def intersect(self, line: ConstructionLine) -> List[Vec2]:
- """
- Returns 0, 1 or 2 intersection points between `line` and `TextBox` border lines.
- Args:
- line: line to intersect with border lines
- Returns: list of intersection points
- """
- result = set()
- for border_line in self.border_lines():
- p = line.intersect(border_line)
- if p is not None:
- result.add(p)
- return sorted(result)
|