Skip to content

Commit

Permalink
improve type-annotation for AbstractBoundingBox
Browse files Browse the repository at this point in the history
  • Loading branch information
mozman committed Jan 6, 2024
1 parent 93f0ece commit 5bf8a15
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 126 deletions.
172 changes: 88 additions & 84 deletions src/ezdxf/math/bbox.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
# Copyright (c) 2019-2023, Manfred Moitzi
# Copyright (c) 2019-2024, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import Iterable, Optional, Iterator, Sequence, Any
from typing import Iterable, Optional, Iterator, Sequence, TypeVar, Generic
import abc

import math
import numpy as np

from ezdxf.math import Vec3, Vec2, UVec, AnyVec
from ezdxf.math import Vec3, Vec2, UVec

T = TypeVar("T", Vec2, Vec3)

__all__ = ["BoundingBox2d", "BoundingBox", "AbstractBoundingBox"]


class AbstractBoundingBox:
__slots__ = ("extmin", "extmax")
class AbstractBoundingBox(Generic[T], abc.ABC):
extmin: T
extmax: T

@abc.abstractmethod
def __init__(self, vertices: Optional[Iterable[UVec]] = None):
self.extmax: Any = None
self.extmin: Any = None
if vertices is not None:
try:
self.extmin, self.extmax = self.extends_detector(vertices)
except ValueError:
# No or invalid data creates an empty BoundingBox
pass
...

def copy(self):
box = self.__class__()
Expand All @@ -41,14 +37,14 @@ def __repr__(self) -> str:
else:
return f"{name}()"

def __iter__(self) -> Iterator[AnyVec]:
def __iter__(self) -> Iterator[T]:
if self.has_data:
yield self.extmin
yield self.extmax

@abc.abstractmethod
def extends_detector(self, vertices: Iterable[UVec]) -> tuple[AnyVec, AnyVec]:
pass
def extend(self, vertices: Iterable[UVec]) -> None:
...

@property
@abc.abstractmethod
Expand All @@ -60,18 +56,18 @@ def inside(self, vertex: UVec) -> bool:
...

@abc.abstractmethod
def has_intersection(self, other: AbstractBoundingBox) -> bool:
def has_intersection(self, other: AbstractBoundingBox[T]) -> bool:
...

@abc.abstractmethod
def has_overlap(self, other: AbstractBoundingBox) -> bool:
def has_overlap(self, other: AbstractBoundingBox[T]) -> bool:
...

@abc.abstractmethod
def intersection(self, other: AbstractBoundingBox) -> AbstractBoundingBox:
def intersection(self, other: AbstractBoundingBox[T]) -> AbstractBoundingBox[T]:
...

def contains(self, other: AbstractBoundingBox) -> bool:
def contains(self, other: AbstractBoundingBox[T]) -> bool:
"""Returns ``True`` if the `other` bounding box is completely inside
this bounding box.
Expand Down Expand Up @@ -105,37 +101,23 @@ def all_inside(self, vertices: Iterable[UVec]) -> bool:
@property
def has_data(self) -> bool:
"""Returns ``True`` if the bonding box has known limits."""
return self.extmin is not None
return math.isfinite(self.extmin.x)

@property
def size(self):
def size(self) -> T:
"""Returns size of bounding box."""
return self.extmax - self.extmin

@property
def center(self):
def center(self) -> T:
"""Returns center of bounding box."""
return self.extmin.lerp(self.extmax)

def extend(self, vertices: Iterable[UVec]) -> None:
"""Extend bounds by `vertices`.
Args:
vertices: iterable of vertices
"""
v = list(vertices)
if not v:
return
if self.has_data:
v.extend([self.extmin, self.extmax])
self.extmin, self.extmax = self.extends_detector(v)

def union(self, other: AbstractBoundingBox):
def union(self, other: AbstractBoundingBox[T]) -> AbstractBoundingBox[T]:
"""Returns a new bounding box as union of this and `other` bounding
box.
"""
vertices: list[AnyVec] = []
vertices: list[T] = []
if self.has_data:
vertices.extend(self)
if other.has_data:
Expand All @@ -146,9 +128,9 @@ def rect_vertices(self) -> Sequence[Vec2]:
"""Returns the corners of the bounding box in the xy-plane as
:class:`Vec2` objects.
"""
if self.has_data: # extmin is not None!
x0, y0, *_ = self.extmin # type: ignore
x1, y1, *_ = self.extmax # type: ignore
if self.has_data:
x0, y0, *_ = self.extmin
x1, y1, *_ = self.extmax
return Vec2(x0, y0), Vec2(x1, y0), Vec2(x1, y1), Vec2(x0, y1)
else:
raise ValueError("empty bounding box")
Expand All @@ -164,11 +146,11 @@ def grow(self, value: float) -> None:
min_ext = min(self.size)
if -value >= min_ext / 2.0:
raise ValueError("shrinking one or more dimensions <= 0")
self.extmax += Vec3(value, value, value) # type: ignore
self.extmin += Vec3(-value, -value, -value) # type: ignore
self.extmax += Vec3(value, value, value)
self.extmin += Vec3(-value, -value, -value)


class BoundingBox(AbstractBoundingBox):
class BoundingBox(AbstractBoundingBox[Vec3]):
"""3D bounding box.
Args:
Expand All @@ -178,6 +160,16 @@ class BoundingBox(AbstractBoundingBox):

__slots__ = ("extmin", "extmax")

def __init__(self, vertices: Optional[Iterable[UVec]] = None):
self.extmin = Vec3(math.inf, math.inf, math.inf)
self.extmax = self.extmin
if vertices is not None:
try:
self.extmin, self.extmax = extents3d(vertices)
except ValueError:
# No or invalid data creates an empty BoundingBox
pass

@property
def is_empty(self) -> bool:
"""Returns ``True`` if the bounding box is empty or the bounding box
Expand All @@ -189,22 +181,33 @@ def is_empty(self) -> bool:
return sx * sy * sz == 0.0
return True

def extends_detector(self, vertices: Iterable[UVec]) -> tuple[Vec3, Vec3]:
return extents3d(vertices)
def extend(self, vertices: Iterable[UVec]) -> None:
"""Extend bounds by `vertices`.
Args:
vertices: iterable of vertices
"""
v = list(vertices)
if not v:
return
if self.has_data:
v.extend([self.extmin, self.extmax])
self.extmin, self.extmax = extents3d(v)

def inside(self, vertex: UVec) -> bool:
"""Returns ``True`` if `vertex` is inside this bounding box.
Vertices at the box border are inside!
"""
if self.extmin is None or self.extmax is None:
if not self.has_data:
return False
x, y, z = Vec3(vertex).xyz
xmin, ymin, zmin = self.extmin.xyz
xmax, ymax, zmax = self.extmax.xyz
return (xmin <= x <= xmax) and (ymin <= y <= ymax) and (zmin <= z <= zmax)

def has_intersection(self, other: AbstractBoundingBox) -> bool:
def has_intersection(self, other: AbstractBoundingBox[T]) -> bool:
"""Returns ``True`` if this bounding box intersects with `other` but does
not include touching bounding boxes, see also :meth:`has_overlap`::
Expand All @@ -215,12 +218,7 @@ def has_intersection(self, other: AbstractBoundingBox) -> bool:
"""
# Source: https://gamemath.com/book/geomtests.html#intersection_two_aabbs
# Check for a separating axis:
if (
self.extmin is None
or self.extmax is None
or other.extmin is None
or other.extmax is None
):
if not self.has_data or not other.has_data:
return False
o_min = Vec3(other.extmin) # could be a 2D bounding box
o_max = Vec3(other.extmax) # could be a 2D bounding box
Expand All @@ -240,7 +238,7 @@ def has_intersection(self, other: AbstractBoundingBox) -> bool:
return False
return True

def has_overlap(self, other: AbstractBoundingBox) -> bool:
def has_overlap(self, other: AbstractBoundingBox[T]) -> bool:
"""Returns ``True`` if this bounding box intersects with `other` but
in contrast to :meth:`has_intersection` includes touching bounding boxes too::
Expand All @@ -251,12 +249,7 @@ def has_overlap(self, other: AbstractBoundingBox) -> bool:
"""
# Source: https://gamemath.com/book/geomtests.html#intersection_two_aabbs
# Check for a separating axis:
if (
self.extmin is None
or self.extmax is None
or other.extmin is None
or other.extmax is None
):
if not self.has_data or not other.has_data:
return False
o_min = Vec3(other.extmin) # could be a 2D bounding box
o_max = Vec3(other.extmax) # could be a 2D bounding box
Expand All @@ -277,7 +270,7 @@ def has_overlap(self, other: AbstractBoundingBox) -> bool:

def cube_vertices(self) -> Sequence[Vec3]:
"""Returns the 3D corners of the bounding box as :class:`Vec3` objects."""
if self.extmin is not None and self.extmax is not None:
if self.has_data:
x0, y0, z0 = self.extmin
x1, y1, z1 = self.extmax
return (
Expand All @@ -293,7 +286,7 @@ def cube_vertices(self) -> Sequence[Vec3]:
else:
raise ValueError("empty bounding box")

def intersection(self, other: AbstractBoundingBox) -> BoundingBox:
def intersection(self, other: AbstractBoundingBox[T]) -> BoundingBox:
"""Returns the bounding box of the intersection cube of both
3D bounding boxes. Returns an empty bounding box if the intersection
volume is 0.
Expand Down Expand Up @@ -323,7 +316,7 @@ def intersection(self, other: AbstractBoundingBox) -> BoundingBox:
return new_bbox


class BoundingBox2d(AbstractBoundingBox):
class BoundingBox2d(AbstractBoundingBox[Vec2]):
"""2D bounding box.
Args:
Expand All @@ -333,6 +326,16 @@ class BoundingBox2d(AbstractBoundingBox):

__slots__ = ("extmin", "extmax")

def __init__(self, vertices: Optional[Iterable[UVec]] = None):
self.extmin = Vec2(math.inf, math.inf)
self.extmax = self.extmin
if vertices is not None:
try:
self.extmin, self.extmax = extents2d(vertices)
except ValueError:
# No or invalid data creates an empty BoundingBox
pass

@property
def is_empty(self) -> bool:
"""Returns ``True`` if the bounding box is empty. The bounding box has a
Expand All @@ -343,22 +346,33 @@ def is_empty(self) -> bool:
return sx * sy == 0.0
return True

def extends_detector(self, vertices: Iterable[UVec]) -> tuple[Vec2, Vec2]:
return extents2d(vertices)
def extend(self, vertices: Iterable[UVec]) -> None:
"""Extend bounds by `vertices`.
Args:
vertices: iterable of vertices
"""
v = list(vertices)
if not v:
return
if self.has_data:
v.extend([self.extmin, self.extmax])
self.extmin, self.extmax = extents2d(v)

def inside(self, vertex: UVec) -> bool:
"""Returns ``True`` if `vertex` is inside this bounding box.
Vertices at the box border are inside!
"""
if self.extmin is None or self.extmax is None:
if not self.has_data:
return False
v = Vec2(vertex)
min_ = self.extmin
max_ = self.extmax
return (min_.x <= v.x <= max_.x) and (min_.y <= v.y <= max_.y)

def has_intersection(self, other: AbstractBoundingBox) -> bool:
def has_intersection(self, other: AbstractBoundingBox[T]) -> bool:
"""Returns ``True`` if this bounding box intersects with `other` but does
not include touching bounding boxes, see also :meth:`has_overlap`::
Expand All @@ -368,12 +382,7 @@ def has_intersection(self, other: AbstractBoundingBox) -> bool:
"""
# Source: https://gamemath.com/book/geomtests.html#intersection_two_aabbs
if (
self.extmin is None
or self.extmax is None
or other.extmin is None
or other.extmax is None
):
if not self.has_data or not other.has_data:
return False
# Check for a separating axis:
if self.extmin.x >= other.extmax.x:
Expand All @@ -386,7 +395,7 @@ def has_intersection(self, other: AbstractBoundingBox) -> bool:
return False
return True

def intersection(self, other: AbstractBoundingBox) -> BoundingBox2d:
def intersection(self, other: AbstractBoundingBox[T]) -> BoundingBox2d:
"""Returns the bounding box of the intersection rectangle of both
2D bounding boxes. Returns an empty bounding box if the intersection
area is 0.
Expand All @@ -406,7 +415,7 @@ def intersection(self, other: AbstractBoundingBox) -> BoundingBox2d:
)
return new_bbox

def has_overlap(self, other: AbstractBoundingBox) -> bool:
def has_overlap(self, other: AbstractBoundingBox[T]) -> bool:
"""Returns ``True`` if this bounding box intersects with `other` but
in contrast to :meth:`has_intersection` includes touching bounding boxes too::
Expand All @@ -416,12 +425,7 @@ def has_overlap(self, other: AbstractBoundingBox) -> bool:
"""
# Source: https://gamemath.com/book/geomtests.html#intersection_two_aabbs
if (
self.extmin is None
or self.extmax is None
or other.extmin is None
or other.extmax is None
):
if not self.has_data or not other.has_data:
return False
# Check for a separating axis:
if self.extmin.x > other.extmax.x:
Expand Down
2 changes: 1 addition & 1 deletion src/ezdxf/tools/clipping_portal.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def __init__(self, vertices: Iterable[UVec], remove_outside=True) -> None:
self.remove_all = False
self.remove_none = False
bbox = BoundingBox2d(vertices)
if bbox.extmin is None or bbox.extmax is None:
if not bbox.has_data:
raise ValueError("clipping box not detectable")
size: Vec2 = bbox.size
if size.x * size.y < 1e-9:
Expand Down
Loading

0 comments on commit 5bf8a15

Please sign in to comment.