Skip to content

Commit

Permalink
Merge pull request #420 from jdegenstein/RotOrder2
Browse files Browse the repository at this point in the history
Add rotation ordering to Location, Rotation, and Plane.rotated
  • Loading branch information
gumyr authored Dec 5, 2023
2 parents 5aa3aa8 + 0b3c04e commit 671db72
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 26 deletions.
2 changes: 2 additions & 0 deletions src/build123d/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@
"ApproxOption",
"AngularDirection",
"CenterOf",
"Extrinsic",
"FontStyle",
"FrameMethod",
"GeomType",
"HeadType",
"Intrinsic",
"Keep",
"Kind",
"LengthMode",
Expand Down
42 changes: 42 additions & 0 deletions src/build123d/build_enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,27 @@ def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"


class Extrinsic(Enum):
"""Order to apply extrinsic rotations by axis"""

XYZ = auto()
XZY = auto()
YZX = auto()
YXZ = auto()
ZXY = auto()
ZYX = auto()

XYX = auto()
XZX = auto()
YZY = auto()
YXY = auto()
ZXZ = auto()
ZYZ = auto()

def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"


class FrameMethod(Enum):
"""Moving frame calculation method"""

Expand Down Expand Up @@ -117,6 +138,27 @@ def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"


class Intrinsic(Enum):
"""Order to apply intrinsic rotations by axis"""

XYZ = auto()
XZY = auto()
YZX = auto()
YXZ = auto()
ZXY = auto()
ZYX = auto()

XYX = auto()
XZX = auto()
YZY = auto()
YXY = auto()
ZXZ = auto()
ZYZ = auto()

def __repr__(self):
return f"<{self.__class__.__name__}.{self.name}>"


class Keep(Enum):
"""Split options"""

Expand Down
160 changes: 135 additions & 25 deletions src/build123d/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,7 @@
from OCP.TopLoc import TopLoc_Location
from OCP.TopoDS import TopoDS_Face, TopoDS_Shape

from build123d.build_enums import (
Align,
)
from build123d.build_enums import Align, Intrinsic, Extrinsic

# Create a build123d logger to distinguish these logs from application logs.
# If the user doesn't configure logging, all build123d logs will be discarded.
Expand Down Expand Up @@ -426,7 +424,7 @@ def to_dir(self) -> gp_Dir:

def transform(self, affine_transform: Matrix) -> Vector:
"""Apply affine transformation"""
# to gp_Pnt to obey cq transformation convention (in OCP.vectors do not translate)
# to gp_Pnt to obey build123d transformation convention (in OCP.vectors do not translate)
pnt = self.to_pnt()
pnt_t = pnt.Transformed(affine_transform.wrapped.Trsf())

Expand Down Expand Up @@ -975,13 +973,40 @@ class Location:
This class wraps the TopLoc_Location class from OCCT. It can be used to move Shape
objects in both relative and absolute manner. It is the preferred type to locate objects
in CQ.
in build123d.
Attributes:
wrapped (TopLoc_Location): the OCP location object
"""

_rot_order_dict = {
Intrinsic.XYZ: gp_EulerSequence.gp_Intrinsic_XYZ,
Intrinsic.XZY: gp_EulerSequence.gp_Intrinsic_XZY,
Intrinsic.YZX: gp_EulerSequence.gp_Intrinsic_YZX,
Intrinsic.YXZ: gp_EulerSequence.gp_Intrinsic_YXZ,
Intrinsic.ZXY: gp_EulerSequence.gp_Intrinsic_ZXY,
Intrinsic.ZYX: gp_EulerSequence.gp_Intrinsic_ZYX,
Intrinsic.XYX: gp_EulerSequence.gp_Intrinsic_XYX,
Intrinsic.XZX: gp_EulerSequence.gp_Intrinsic_XZX,
Intrinsic.YZY: gp_EulerSequence.gp_Intrinsic_YZY,
Intrinsic.YXY: gp_EulerSequence.gp_Intrinsic_YXY,
Intrinsic.ZXZ: gp_EulerSequence.gp_Intrinsic_ZXZ,
Intrinsic.ZYZ: gp_EulerSequence.gp_Intrinsic_ZYZ,
Extrinsic.XYZ: gp_EulerSequence.gp_Extrinsic_XYZ,
Extrinsic.XZY: gp_EulerSequence.gp_Extrinsic_XZY,
Extrinsic.YZX: gp_EulerSequence.gp_Extrinsic_YZX,
Extrinsic.YXZ: gp_EulerSequence.gp_Extrinsic_YXZ,
Extrinsic.ZXY: gp_EulerSequence.gp_Extrinsic_ZXY,
Extrinsic.ZYX: gp_EulerSequence.gp_Extrinsic_ZYX,
Extrinsic.XYX: gp_EulerSequence.gp_Extrinsic_XYX,
Extrinsic.XZX: gp_EulerSequence.gp_Extrinsic_XZX,
Extrinsic.YZY: gp_EulerSequence.gp_Extrinsic_YZY,
Extrinsic.YXY: gp_EulerSequence.gp_Extrinsic_YXY,
Extrinsic.ZXZ: gp_EulerSequence.gp_Extrinsic_ZXZ,
Extrinsic.ZYZ: gp_EulerSequence.gp_Extrinsic_ZYZ,
}

@property
def position(self) -> Vector:
"""Extract Position component of self
Expand Down Expand Up @@ -1022,14 +1047,17 @@ def orientation(self, rotation: VectorLike):
Args:
rotation (VectorLike): Intrinsic XYZ angles in degrees
"""

ordering = Intrinsic.XYZ

position_xyz = self.wrapped.Transformation().TranslationPart()
trsf_position = gp_Trsf()
trsf_position.SetTranslationPart(
gp_Vec(position_xyz.X(), position_xyz.Y(), position_xyz.Z())
)
rotation = [radians(a) for a in rotation]
quaternion = gp_Quaternion()
quaternion.SetEulerAngles(gp_EulerSequence.gp_Intrinsic_XYZ, *rotation)
quaternion.SetEulerAngles(self._rot_order_dict[ordering], *rotation)
trsf_orientation = gp_Trsf()
trsf_orientation.SetRotation(quaternion)
self.wrapped = TopLoc_Location(trsf_position * trsf_orientation)
Expand Down Expand Up @@ -1073,6 +1101,18 @@ def __init__(
If rotation is not None then the location includes the rotation (see also Rotation class)
"""

@overload
def __init__(
self,
translation: VectorLike,
rotation: RotationLike,
ordering: Union[Extrinsic, Intrinsic],
): # pragma: no cover
"""Location with translation with respect to the original location.
If rotation is not None then the location includes the rotation (see also Rotation class)
ordering defaults to Intrinsic.XYZ, but can also be set to Extrinsic
"""

@overload
def __init__(self, plane: Plane): # pragma: no cover
"""Location corresponding to the location of the Plane."""
Expand All @@ -1092,9 +1132,9 @@ def __init__(self, gp_trsf: gp_Trsf): # pragma: no cover

@overload
def __init__(
self, translation: VectorLike, axis: VectorLike, angle: float
self, translation: VectorLike, direction: VectorLike, angle: float
): # pragma: no cover
"""Location with translation t and rotation around axis by angle
"""Location with translation t and rotation around direction by angle
with respect to the original location."""

def __init__(self, *args):
Expand Down Expand Up @@ -1129,21 +1169,20 @@ def __init__(self, *args):
raise TypeError("Unexpected parameters")

elif len(args) == 2:
ordering = Intrinsic.XYZ
if isinstance(args[0], (Vector, Iterable)):
if isinstance(args[1], (Vector, Iterable)):
rotation = [radians(a) for a in args[1]]
quaternion = gp_Quaternion()
quaternion.SetEulerAngles(
gp_EulerSequence.gp_Intrinsic_XYZ, *rotation
)
quaternion.SetEulerAngles(self._rot_order_dict[ordering], *rotation)
transform.SetRotation(quaternion)
elif isinstance(args[0], (Vector, tuple)) and isinstance(
args[1], (int, float)
):
angle = radians(args[1])
quaternion = gp_Quaternion()
quaternion.SetEulerAngles(
gp_EulerSequence.gp_Intrinsic_XYZ, 0, 0, angle
self._rot_order_dict[ordering], 0, 0, angle
)
transform.SetRotation(quaternion)

Expand All @@ -1158,13 +1197,31 @@ def __init__(self, *args):
)
transform.SetTransformation(coordinate_system)
transform.Invert()
else:
translation, axis, angle = args
transform.SetRotation(
gp_Ax1(Vector().to_pnt(), Vector(axis).to_dir()), angle * pi / 180.0
)
transform.SetTranslationPart(Vector(translation).wrapped)
elif len(args) == 3:
if (
isinstance(args[0], (Vector, Iterable))
and isinstance(args[1], (Vector, Iterable))
and isinstance(args[2], (int, float))
):
translation, axis, angle = args
transform.SetRotation(
gp_Ax1(Vector().to_pnt(), Vector(axis).to_dir()), angle * pi / 180.0
)
elif (
isinstance(args[0], (Vector, Iterable))
and isinstance(args[1], (Vector, Iterable))
and isinstance(args[2], (Extrinsic, Intrinsic))
):
translation = args[0]
rotation = [radians(a) for a in args[1]]
ordering = args[2]
quaternion = gp_Quaternion()
quaternion.SetEulerAngles(self._rot_order_dict[ordering], *rotation)
transform.SetRotation(quaternion)
else:
raise TypeError("Unsupported argument types for Location")

transform.SetTranslationPart(Vector(translation).wrapped)
self.wrapped = TopLoc_Location(transform)

def inverse(self) -> Location:
Expand Down Expand Up @@ -1315,14 +1372,58 @@ class Rotation(Location):
X (float): rotation in degrees about X axis
Y (float): rotation in degrees about Y axis
Z (float): rotation in degrees about Z axis
optionally specify rotation ordering with Intrinsic or Extrinsic enums, defaults to Intrinsic.XYZ
"""

def __init__(self, X: float = 0, Y: float = 0, Z: float = 0):
self.X = X
self.Y = Y
self.Z = Z
super().__init__((0, 0, 0), (X, Y, Z))
@overload
def __init__(
self,
rotation: RotationLike,
ordering: Union[Extrinsic, Intrinsic] == Intrinsic.XYZ,
):
"""Subclass of Location used only for object rotation
ordering is for order of rotations in Intrinsic or Extrinsic enums"""

@overload
def __init__(
self,
X: float = 0,
Y: float = 0,
Z: float = 0,
ordering: Union[Extrinsic, Intrinsic] = Intrinsic.XYZ,
):
"""Subclass of Location used only for object rotation
ordering is for order of rotations in Intrinsic or Extrinsic enums"""

def __init__(self, *args, **kwargs):
if not all(key in ("X", "Y", "Z", "rotation", "ordering") for key in kwargs):
raise TypeError("Invalid key for Rotation")
angles, rotations, orderings = [0, 0, 0], [], []
if args:
angles = list(filter(lambda item: isinstance(item, (int, float)), args))
vectors = list(filter(lambda item: isinstance(item, Vector), args))
tuples = list(filter(lambda item: isinstance(item, tuple), args))
if tuples:
angles = list(*tuples)
if vectors:
angles = vectors[0].to_tuple()
if len(angles) < 3:
angles.extend([0.0] * (3 - len(angles)))
rotations = list(filter(lambda item: isinstance(item, Rotation), args))
orderings = list(
filter(lambda item: isinstance(item, (Extrinsic, Intrinsic)), args)
)
kwargs.setdefault("X", angles[0])
kwargs.setdefault("Y", angles[1])
kwargs.setdefault("Z", angles[2])
kwargs.setdefault("ordering", orderings[0] if orderings else Intrinsic.XYZ)
if rotations:
super().__init__(rotations[0])
else:
super().__init__(
(0, 0, 0), (kwargs["X"], kwargs["Y"], kwargs["Z"]), kwargs["ordering"]
)


Rot = Rotation # Short form for Algebra users who like compact notation
Expand Down Expand Up @@ -1897,7 +1998,11 @@ def shift_origin(self, locator: Union[Axis, VectorLike, "Vertex"]) -> Plane:
raise TypeError(f"Invalid locate type: {type(locator)}")
return Plane(origin=new_origin, x_dir=self.x_dir, z_dir=self.z_dir)

def rotated(self, rotation: VectorLike = (0, 0, 0)) -> Plane:
def rotated(
self,
rotation: VectorLike = (0, 0, 0),
ordering: Union[Extrinsic, Intrinsic] = None,
) -> Plane:
"""Returns a copy of this plane, rotated about the specified axes
Since the z axis is always normal the plane, rotating around Z will
Expand All @@ -1910,14 +2015,19 @@ def rotated(self, rotation: VectorLike = (0, 0, 0)) -> Plane:
Args:
rotation (VectorLike, optional): (xDegrees, yDegrees, zDegrees). Defaults to (0, 0, 0).
ordering (Union[Intrinsic, Extrinsic], optional): order of rotations in Intrinsic or Extrinsic rotation mode, defaults to Intrinsic.XYZ
Returns:
Plane: a copy of this plane rotated as requested.
"""

if ordering is None:
ordering = Intrinsic.XYZ

# Note: this is not a geometric Vector
rotation = [radians(a) for a in rotation]
quaternion = gp_Quaternion()
quaternion.SetEulerAngles(gp_EulerSequence.gp_Intrinsic_XYZ, *rotation)
quaternion.SetEulerAngles(Location._rot_order_dict[ordering], *rotation)
trsf_rotation = gp_Trsf()
trsf_rotation.SetRotation(quaternion)
transformation = Matrix(gp_GTrsf(trsf_rotation))
Expand Down
10 changes: 10 additions & 0 deletions tests/test_build_enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,27 @@ def test_repr(self):
enums = [
Align,
AngularDirection,
ApproxOption,
CenterOf,
Extrinsic,
FontStyle,
FrameMethod,
GeomType,
HeadType,
Intrinsic,
Keep,
Kind,
LengthMode,
MeshType,
Mode,
NumberDisplay,
PositionMode,
PageSize,
Select,
Side,
SortBy,
Transition,
Unit,
Until,
]
for enum in enums:
Expand Down
Loading

0 comments on commit 671db72

Please sign in to comment.