1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
|
# Copyright (c) 2018 Ultimaker B.V.
# Uranium is released under the terms of the LGPLv3 or higher.
from UM.Math.Float import Float
from UM.Math.Ray import Ray #For typing.
from UM.Math.Vector import Vector
import numpy
from typing import Optional, Tuple, Union
class AxisAlignedBox:
"""Axis aligned bounding box."""
class IntersectionResult:
NoIntersection = 1
PartialIntersection = 2
FullIntersection = 3
def __init__(self, minimum: Vector = Vector.Null, maximum: Vector = Vector.Null) -> None:
if minimum.x > maximum.x or minimum.y > maximum.y or minimum.z > maximum.z:
swapped_minimum = Vector(min(minimum.x, maximum.x), min(minimum.y, maximum.y), min(minimum.z, maximum.z))
swapped_maximum = Vector(max(minimum.x, maximum.x), max(minimum.y, maximum.y), max(minimum.z, maximum.z))
minimum = swapped_minimum
maximum = swapped_maximum
minimum.setRoundDigits(3)
maximum.setRoundDigits(3)
self._min = minimum #type: Vector
self._max = maximum #type: Vector
def set(self, minimum: Optional[Vector] = None, maximum: Optional[Vector] = None, left: Optional[float] = None,
right: Optional[float] = None, top: Optional[float] = None, bottom: Optional[float] = None,
front: Optional[float] = None, back: Optional[float] = None) -> "AxisAlignedBox":
if minimum is None:
minimum = self._min
if maximum is None:
maximum = self._max
if left is not None or bottom is not None or back is not None:
left = minimum.x if left is None else left
bottom = minimum.y if bottom is None else bottom
back = minimum.z if back is None else back
minimum = Vector(left, bottom, back)
if right is not None or top is not None or front is not None:
right = maximum.x if right is None else right
top = maximum.y if top is None else top
front = maximum.z if front is None else front
maximum = Vector(right, top, front)
return AxisAlignedBox(minimum, maximum)
def __add__(self, other: object) -> "AxisAlignedBox":
if other is None or not isinstance(other, AxisAlignedBox) or not other.isValid():
return self
new_min = Vector(min(self._min.x, other.left), min(self._min.y, other.bottom),
min(self._min.z, other.back))
new_max = Vector(max(self._max.x, other.right), max(self._max.y, other.top),
max(self._max.z, other.front))
return AxisAlignedBox(minimum=new_min, maximum=new_max)
def __iadd__(self, other: object) -> "AxisAlignedBox":
raise NotImplementedError()
@property
def width(self) -> float:
return self._max.x - self._min.x
@property
def height(self) -> float:
return self._max.y - self._min.y
@property
def depth(self) -> float:
return self._max.z - self._min.z
@property
def center(self) -> Vector:
return self._min + ((self._max - self._min) / 2.0)
@property
def left(self) -> float:
return self._min.x
@property
def right(self) -> float:
return self._max.x
@property
def bottom(self) -> float:
return self._min.y
@property
def top(self) -> float:
return self._max.y
@property
def back(self) -> float:
return self._min.z
@property
def front(self) -> float:
return self._max.z
@property
def minimum(self) -> Vector:
return self._min
@property
def maximum(self) -> Vector:
return self._max
def isValid(self) -> bool:
"""Check if the bounding box is valid.
Uses fuzzycompare to validate.
:sa Float::fuzzyCompare()
"""
return not(Float.fuzzyCompare(self._min.x, self._max.x) or
Float.fuzzyCompare(self._min.y, self._max.y) or
Float.fuzzyCompare(self._min.z, self._max.z))
def intersectsRay(self, ray: Ray) -> Union[Tuple[float, float], bool]:
"""Intersect the bounding box with a ray
:sa Ray
"""
inv = ray.inverseDirection
t = numpy.empty((2,3), dtype=numpy.float32)
t[0, 0] = inv.x * (self._min.x - ray.origin.x)
t[0, 1] = inv.y * (self._min.y - ray.origin.y)
t[0, 2] = inv.z * (self._min.z - ray.origin.z)
t[1, 0] = inv.x * (self._max.x - ray.origin.x)
t[1, 1] = inv.y * (self._max.y - ray.origin.y)
t[1, 2] = inv.z * (self._max.z - ray.origin.z)
tmin = numpy.min(t, axis=0)
tmax = numpy.max(t, axis=0)
largest_min = numpy.max(tmin)
smallest_max = numpy.min(tmax)
if smallest_max > largest_min:
return (largest_min, smallest_max)
else:
return False
def intersectsBox(self, box: "AxisAlignedBox") -> int:
"""Check to see if this box intersects another box.
:param box: The box to check for intersection.
:return: NoIntersection when no intersection occurs, PartialIntersection when partially intersected, FullIntersection when box is fully contained inside this box.
"""
if self._min.x > box._max.x or box._min.x > self._max.x:
return self.IntersectionResult.NoIntersection
if self._min.y > box._max.y or box._min.y > self._max.y:
return self.IntersectionResult.NoIntersection
if self._min.z > box._max.z or box._min.z > self._max.z:
return self.IntersectionResult.NoIntersection
if box._min >= self._min and box._max <= self._max:
return self.IntersectionResult.FullIntersection
return self.IntersectionResult.PartialIntersection
def __repr__(self) -> str:
return "AxisAlignedBox(min = {0}, max = {1})".format(self._min, self._max)
# This field is filled in below. This is needed to help static analysis tools (read: PyCharm)
Null = None # type: AxisAlignedBox
AxisAlignedBox.Null = AxisAlignedBox()
|