diff --git a/docs/assets/tutorial_joint.svg b/docs/assets/tutorial_joint.svg index 0523ed2..07b8bb9 100644 --- a/docs/assets/tutorial_joint.svg +++ b/docs/assets/tutorial_joint.svg @@ -615,6 +615,17 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/tutorial_joint_box_outer.svg b/docs/assets/tutorial_joint_box_outer.svg index ee528aa..6fe6725 100644 --- a/docs/assets/tutorial_joint_box_outer.svg +++ b/docs/assets/tutorial_joint_box_outer.svg @@ -230,6 +230,14 @@ + + + + + + + + \ No newline at end of file diff --git a/docs/assets/tutorial_joint_box_outer_inner.svg b/docs/assets/tutorial_joint_box_outer_inner.svg index 7796748..4dcd9e1 100644 --- a/docs/assets/tutorial_joint_box_outer_inner.svg +++ b/docs/assets/tutorial_joint_box_outer_inner.svg @@ -314,6 +314,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/tutorial_joint_box_outer_inner_lid.svg b/docs/assets/tutorial_joint_box_outer_inner_lid.svg index c5913a1..b97328a 100644 --- a/docs/assets/tutorial_joint_box_outer_inner_lid.svg +++ b/docs/assets/tutorial_joint_box_outer_inner_lid.svg @@ -325,6 +325,14 @@ + + + + + + + + \ No newline at end of file diff --git a/docs/assets/tutorial_joint_inner_leaf.svg b/docs/assets/tutorial_joint_inner_leaf.svg index 1c25b9e..04b0169 100644 --- a/docs/assets/tutorial_joint_inner_leaf.svg +++ b/docs/assets/tutorial_joint_inner_leaf.svg @@ -196,6 +196,10 @@ + + + + \ No newline at end of file diff --git a/docs/assets/tutorial_joint_m6_screw.svg b/docs/assets/tutorial_joint_m6_screw.svg index 58393e1..5106599 100644 --- a/docs/assets/tutorial_joint_m6_screw.svg +++ b/docs/assets/tutorial_joint_m6_screw.svg @@ -361,6 +361,8 @@ + + \ No newline at end of file diff --git a/docs/assets/tutorial_joint_outer_leaf.svg b/docs/assets/tutorial_joint_outer_leaf.svg index eb175cc..a610245 100644 --- a/docs/assets/tutorial_joint_outer_leaf.svg +++ b/docs/assets/tutorial_joint_outer_leaf.svg @@ -207,6 +207,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/docs/direct_api_reference.rst b/docs/direct_api_reference.rst index ea5333e..e4eb28f 100644 --- a/docs/direct_api_reference.rst +++ b/docs/direct_api_reference.rst @@ -94,18 +94,14 @@ Methods and functions specific to exporting and importing build123d objects are :noindex: -************* -Joint Objects -************* -Joint classes which are used to position Solid and Compound objects relative to each -other are defined below. +************ +Joint Object +************ +Base Joint class which is used to position Solid and Compound objects relative to each +other are defined below. The :ref:`joints` section contains the class description of the +derived Joint classes. .. py:module:: topology :noindex: .. autoclass:: Joint -.. autoclass:: RigidJoint -.. autoclass:: RevoluteJoint -.. autoclass:: LinearJoint -.. autoclass:: CylindricalJoint -.. autoclass:: BallJoint diff --git a/docs/joints.rst b/docs/joints.rst index 7eb6863..d382106 100644 --- a/docs/joints.rst +++ b/docs/joints.rst @@ -1,3 +1,5 @@ +.. _joints: + ###### Joints ###### @@ -10,15 +12,15 @@ in pairs - a :class:`~topology.Joint` can only be connected to another :class:`~ +---------------------------------------+---------------------------------------------------------------------+--------------------+ | :class:`~topology.Joint` | connect_to | Example | +=======================================+=====================================================================+====================+ -| :class:`~topology.BallJoint` | :class:`~topology.RigidJoint` | Gimbal | +| :class:`~joints.BallJoint` | :class:`~joints.RigidJoint` | Gimbal | +---------------------------------------+---------------------------------------------------------------------+--------------------+ -| :class:`~topology.CylindricalJoint` | :class:`~topology.RigidJoint` | Screw | +| :class:`~joints.CylindricalJoint` | :class:`~joints.RigidJoint` | Screw | +---------------------------------------+---------------------------------------------------------------------+--------------------+ -| :class:`~topology.LinearJoint` | :class:`~topology.RigidJoint`, :class:`~topology.RevoluteJoint` | Slider or Pin Slot | +| :class:`~joints.LinearJoint` | :class:`~joints.RigidJoint`, :class:`~joints.RevoluteJoint` | Slider or Pin Slot | +---------------------------------------+---------------------------------------------------------------------+--------------------+ -| :class:`~topology.RevoluteJoint` | :class:`~topology.RigidJoint` | Hinge | +| :class:`~joints.RevoluteJoint` | :class:`~joints.RigidJoint` | Hinge | +---------------------------------------+---------------------------------------------------------------------+--------------------+ -| :class:`~topology.RigidJoint` | :class:`~topology.RigidJoint` | Fixed | +| :class:`~joints.RigidJoint` | :class:`~joints.RigidJoint` | Fixed | +---------------------------------------+---------------------------------------------------------------------+--------------------+ Objects may have many joints bound to them each with an identifying label. All :class:`~topology.Joint` @@ -27,17 +29,20 @@ their position and orientation (the `ocp-vscode = position <= 12 latch.part.joints["latch"].connect_to(slide.part.joints["slide"], position=12) diff --git a/docs/tutorial_joints.py b/docs/tutorial_joints.py index d5aa549..5e90027 100644 --- a/docs/tutorial_joints.py +++ b/docs/tutorial_joints.py @@ -133,7 +133,6 @@ class Hinge(Compound): # Leaf attachment RigidJoint( label="leaf", - to_part=leaf_builder.part, joint_location=Location( (width - barrel_diameter, 0, length / 2), (90, 0, 0) ), @@ -142,13 +141,13 @@ class Hinge(Compound): if inner: RigidJoint( "hinge_axis", - leaf_builder.part, - Location((width - barrel_diameter / 2, barrel_diameter / 2, 0)), + joint_location=Location( + (width - barrel_diameter / 2, barrel_diameter / 2, 0) + ), ) else: RevoluteJoint( "hinge_axis", - leaf_builder.part, axis=Axis( (width - barrel_diameter / 2, barrel_diameter / 2, 0), (0, 0, 1) ), @@ -159,13 +158,11 @@ class Hinge(Compound): for hole, hole_location in enumerate(hole_locations): CylindricalJoint( label="hole" + str(hole), - to_part=leaf_builder.part, axis=hole_location.to_axis(), linear_range=(-2 * CM, 2 * CM), angular_range=(0, 360), ) # [End Fastener holes] - super().__init__(leaf_builder.part.wrapped, joints=leaf_builder.part.joints) # [Hinge Class] @@ -202,10 +199,8 @@ with BuildPart() as box_builder: Hole(3 * MM, 1 * CM) RigidJoint( "hinge_attachment", - box_builder.part, - Location((-15 * CM, 0, 4 * CM), (180, 90, 0)), + joint_location=Location((-15 * CM, 0, 4 * CM), (180, 90, 0)), ) - # [Demonstrate that objects with Joints can be moved and the joints follow] box = box_builder.part.moved(Location((0, 0, 5 * CM))) @@ -217,8 +212,7 @@ with BuildPart() as lid_builder: Hole(3 * MM, 1 * CM) RigidJoint( "hinge_attachment", - lid_builder.part, - Location((0, 0, 0), (0, 0, 180)), + joint_location=Location((0, 0, 0), (0, 0, 180)), ) lid = lid_builder.part diff --git a/docs/tutorial_joints.rst b/docs/tutorial_joints.rst index d6de4f8..f3b90cd 100644 --- a/docs/tutorial_joints.rst +++ b/docs/tutorial_joints.rst @@ -80,7 +80,7 @@ The second joint to add is either a :class:`~topology.RigidJoint` (on the inner .. literalinclude:: tutorial_joints.py :start-after: [Create the Joints] :end-before: [Fastener holes] - :emphasize-lines: 10-25 + :emphasize-lines: 10-24 The inner leaf just pivots around the outer leaf and therefore the simple :class:`~topology.RigidJoint` is used to define the Location of this pivot. The outer leaf contains the more complex @@ -141,7 +141,7 @@ the joint used to attach the outer hinge leaf. .. literalinclude:: tutorial_joints.py :start-after: [Create the box with a RigidJoint to mount the hinge] :end-before: [Demonstrate that objects with Joints can be moved and the joints follow] - :emphasize-lines: 13-17 + :emphasize-lines: 13-16 Since the hinge will be fixed to the box another :class:`~topology.RigidJoint` is used mark where the hinge will go. Note that the orientation of this :class:`~topology.Joint` will control how the hinge leaf is @@ -172,7 +172,7 @@ Much like the box, the lid is created in a :class:`~build_part.BuildPart` contex .. literalinclude:: tutorial_joints.py :start-after: [The lid with a RigidJoint for the hinge] :end-before: [A screw to attach the hinge to the box] - :emphasize-lines: 6-10 + :emphasize-lines: 6-9 Again, the original orientation of the lid and hinge inner leaf are not important, when the joints are connected together the parts will move into the correct position. @@ -293,3 +293,10 @@ and ``other`` will move to the appropriate :class:`~geometry.Location`. show_object(m6_joint.symbol, name="m6 screw symbol") + or, with the ocp_vscode viewer + + .. code:: python + + show(box, render_joints=True) + + diff --git a/src/build123d/__init__.py b/src/build123d/__init__.py index 109057e..56f68d4 100644 --- a/src/build123d/__init__.py +++ b/src/build123d/__init__.py @@ -1,20 +1,22 @@ """build123d import definitions""" from build123d.build_common import * +from build123d.build_enums import * from build123d.build_line import * -from build123d.build_sketch import * from build123d.build_part import * +from build123d.build_sketch import * from build123d.exporters import * from build123d.geometry import * -from build123d.topology import * -from build123d.build_enums import * from build123d.importers import * +from build123d.joints import * +from build123d.mesher import * +from build123d.objects_curve import * +from build123d.objects_part import * +from build123d.objects_sketch import * from build123d.operations_generic import * from build123d.operations_part import * from build123d.operations_sketch import * -from build123d.objects_part import * -from build123d.objects_sketch import * -from build123d.objects_curve import * -from build123d.mesher import * +from build123d.topology import * + from .version import version as __version__ __all__ = [ diff --git a/src/build123d/build_common.py b/src/build123d/build_common.py index 80d978a..97499db 100644 --- a/src/build123d/build_common.py +++ b/src/build123d/build_common.py @@ -211,10 +211,16 @@ class Builder(ABC): return self + def _exit_extras(self): + """Any builder specific exit actions""" + pass + def __exit__(self, exception_type, exception_value, traceback): """Upon exiting restore context and send object to parent""" self._current.reset(self._reset_tok) + self._exit_extras() # custom builder exit code + if self.builder_parent is not None and self.mode != Mode.PRIVATE: logger.debug( "Transferring object(s) to %s", type(self.builder_parent).__name__ diff --git a/src/build123d/build_part.py b/src/build123d/build_part.py index 1dbac66..3fb9765 100644 --- a/src/build123d/build_part.py +++ b/src/build123d/build_part.py @@ -35,7 +35,7 @@ from typing import Union from build123d.build_common import Builder, logger from build123d.build_enums import Mode from build123d.geometry import Location, Plane -from build123d.topology import Edge, Face, Part, Solid, Wire +from build123d.topology import Edge, Face, Joint, Part, Solid, Wire class BuildPart(Builder): @@ -71,11 +71,17 @@ class BuildPart(Builder): """Return a wire representation of the pending edges""" return Wire.combine(self.pending_edges)[0] + @property + def location(self) -> Location: + """Builder's location""" + return self.part.location if self.part is not None else Location() + def __init__( self, *workplanes: Union[Face, Plane, Location], mode: Mode = Mode.ADD, ): + self.joints: dict[str, Joint] = {} self.part: Part = None self.pending_faces: list[Face] = [] self.pending_face_planes: list[Plane] = [] @@ -106,3 +112,10 @@ class BuildPart(Builder): edge.location, ) self.pending_edges.append(edge) + + def _exit_extras(self): + """Transfer joints on exit""" + if self.joints: + self.part.joints = self.joints + for joint in self.part.joints.values(): + joint.parent = self.part diff --git a/src/build123d/topology.py b/src/build123d/topology.py index e20bf01..01c3577 100644 --- a/src/build123d/topology.py +++ b/src/build123d/topology.py @@ -6810,509 +6810,6 @@ class Joint(ABC): return NotImplementedError -class RigidJoint(Joint): - """RigidJoint - - A rigid joint fixes two components to one another. - - Args: - label (str): joint label - to_part (Union[Solid, Compound]): object to attach joint to - joint_location (Location): global location of joint - - Attributes: - relative_location (Location): joint location relative to bound object - - """ - - @property - def symbol(self) -> Compound: - """A CAD symbol (XYZ indicator) as bound to part""" - size = self.parent.bounding_box().diagonal / 12 - return Compound.make_triad(axes_scale=size).locate( - self.parent.location * self.relative_location - ) - - def __init__( - self, - label: str, - to_part: Union[Solid, Compound], - joint_location: Location = Location(), - ): - self.relative_location = to_part.location.inverse() * joint_location - to_part.joints[label] = self - super().__init__(label, to_part) - - def relative_to(self, other: Joint, **kwargs) -> Location: - """relative_to - - Return the relative position to move the other. - - Args: - other (RigidJoint): joint to connect to - """ - if not isinstance(other, RigidJoint): - raise TypeError(f"other must of type RigidJoint not {type(other)}") - - return self.relative_location * other.relative_location.inverse() - - -class RevoluteJoint(Joint): - """RevoluteJoint - - Component rotates around axis like a hinge. - - Args: - label (str): joint label - to_part (Union[Solid, Compound]): object to attach joint to - axis (Axis): axis of rotation - angle_reference (VectorLike, optional): direction normal to axis defining where - angles will be measured from. Defaults to None. - range (tuple[float, float], optional): (min,max) angle of joint. Defaults to (0, 360). - - Attributes: - angle (float): angle of joint - angle_reference (Vector): reference for angular poitions - angular_range (tuple[float,float]): min and max angular position of joint - relative_axis (Axis): joint axis relative to bound part - - Raises: - ValueError: angle_reference must be normal to axis - """ - - @property - def symbol(self) -> Compound: - """A CAD symbol representing the axis of rotation as bound to part""" - radius = self.parent.bounding_box().diagonal / 30 - - return Compound.make_compound( - [ - Edge.make_line((0, 0, 0), (0, 0, radius * 10)), - Edge.make_circle(radius), - ] - ).move(self.parent.location * self.relative_axis.location) - - def __init__( - self, - label: str, - to_part: Union[Solid, Compound], - axis: Axis = Axis.Z, - angle_reference: VectorLike = None, - angular_range: tuple[float, float] = (0, 360), - ): - self.angular_range = angular_range - if angle_reference: - if not axis.is_normal(Axis((0, 0, 0), angle_reference)): - raise ValueError("angle_reference must be normal to axis") - self.angle_reference = Vector(angle_reference) - else: - self.angle_reference = Plane(origin=(0, 0, 0), z_dir=axis.direction).x_dir - self.angle = None - self.relative_axis = axis.located(to_part.location.inverse()) - to_part.joints[label] = self - super().__init__(label, to_part) - - def relative_to( - self, other: Joint, angle: float = None - ): # pylint: disable=arguments-differ - """relative_to - - Return the relative location from this joint to the RigidJoint of another object - - a hinge joint. - - Args: - other (RigidJoint): joint to connect to - angle (float, optional): angle within angular range. Defaults to minimum. - - Raises: - TypeError: other must of type RigidJoint - ValueError: angle out of range - """ - if not isinstance(other, RigidJoint): - raise TypeError(f"other must of type RigidJoint not {type(other)}") - - angle = self.angular_range[0] if angle is None else angle - if angle < self.angular_range[0] or angle > self.angular_range[1]: - raise ValueError(f"angle ({angle}) must in range of {self.angular_range}") - self.angle = angle - # Avoid strange rotations when angle is zero by using 360 instead - angle = 360.0 if angle == 0.0 else angle - rotation = Location( - Plane( - origin=(0, 0, 0), - x_dir=self.angle_reference.rotate(Axis.Z, angle), - z_dir=(0, 0, 1), - ) - ) - return ( - self.relative_axis.location * rotation * other.relative_location.inverse() - ) - - -class LinearJoint(Joint): - """LinearJoint - - Component moves along a single axis. - - Args: - label (str): joint label - to_part (Union[Solid, Compound]): object to attach joint to - axis (Axis): axis of linear motion - range (tuple[float, float], optional): (min,max) position of joint. - Defaults to (0, inf). - - Attributes: - axis (Axis): joint axis - angle (float): angle of joint - linear_range (tuple[float,float]): min and max positional values - position (float): joint position - relative_axis (Axis): joint axis relative to bound part - - """ - - @property - def symbol(self) -> Compound: - """A CAD symbol of the linear axis positioned relative to_part""" - radius = (self.linear_range[1] - self.linear_range[0]) / 15 - return Compound.make_compound( - [ - Edge.make_line( - (0, 0, self.linear_range[0]), (0, 0, self.linear_range[1]) - ), - Edge.make_circle(radius), - ] - ).move(self.parent.location * self.relative_axis.location) - - def __init__( - self, - label: str, - to_part: Union[Solid, Compound], - axis: Axis = Axis.Z, - linear_range: tuple[float, float] = (0, inf), - ): - self.axis = axis - self.linear_range = linear_range - self.position = None - self.relative_axis = axis.located(to_part.location.inverse()) - self.angle = None - to_part.joints[label]: dict[str, Joint] = self - super().__init__(label, to_part) - - @overload - def relative_to( - self, other: RigidJoint, position: float = None - ): # pylint: disable=arguments-differ - """relative_to - RigidJoint - - Return the relative location from this joint to the RigidJoint of another object - - a slider joint. - - Args: - other (RigidJoint): joint to connect to - position (float, optional): position within joint range. Defaults to middle. - """ - - @overload - def relative_to( - self, other: RevoluteJoint, position: float = None, angle: float = None - ): # pylint: disable=arguments-differ - """relative_to - RevoluteJoint - - Return the relative location from this joint to the RevoluteJoint of another object - - a pin slot joint. - - Args: - other (RigidJoint): joint to connect to - position (float, optional): position within joint range. Defaults to middle. - angle (float, optional): angle within angular range. Defaults to minimum. - """ - - def relative_to(self, *args, **kwargs): # pylint: disable=arguments-differ - """Return the relative position of other to linear joint defined by self""" - - # Parse the input parameters - other, position, angle = None, None, None - if args: - other = args[0] - position = args[1] if len(args) >= 2 else position - angle = args[2] if len(args) == 3 else angle - - if kwargs: - other = kwargs["other"] if "other" in kwargs else other - position = kwargs["position"] if "position" in kwargs else position - angle = kwargs["angle"] if "angle" in kwargs else angle - - if not isinstance(other, (RigidJoint, RevoluteJoint)): - raise TypeError( - f"other must of type RigidJoint or RevoluteJoint not {type(other)}" - ) - - position = sum(self.linear_range) / 2 if position is None else position - if not self.linear_range[0] <= position <= self.linear_range[1]: - raise ValueError( - f"position ({position}) must in range of {self.linear_range}" - ) - self.position = position - - if isinstance(other, RevoluteJoint): - angle = other.angular_range[0] if angle is None else angle - if not other.angular_range[0] <= angle <= other.angular_range[1]: - raise ValueError( - f"angle ({angle}) must in range of {other.angular_range}" - ) - rotation = Location( - Plane( - origin=(0, 0, 0), - x_dir=other.angle_reference.rotate(other.relative_axis, angle), - z_dir=other.relative_axis.direction, - ) - ) - else: - angle = 0.0 - rotation = Location() - self.angle = angle - joint_relative_position = ( - Location( - self.relative_axis.position + self.relative_axis.direction * position, - ) - * rotation - ) - - if isinstance(other, RevoluteJoint): - other_relative_location = Location(other.relative_axis.position) - else: - other_relative_location = other.relative_location - - return joint_relative_position * other_relative_location.inverse() - - -class CylindricalJoint(Joint): - """CylindricalJoint - - Component rotates around and moves along a single axis like a screw. - - Args: - label (str): joint label - to_part (Union[Solid, Compound]): object to attach joint to - axis (Axis): axis of rotation and linear motion - angle_reference (VectorLike, optional): direction normal to axis defining where - angles will be measured from. Defaults to None. - linear_range (tuple[float, float], optional): (min,max) position of joint. - Defaults to (0, inf). - angular_range (tuple[float, float], optional): (min,max) angle of joint. - Defaults to (0, 360). - - Attributes: - axis (Axis): joint axis - linear_position (float): linear joint position - rotational_position (float): revolute joint angle in degrees - angle_reference (Vector): reference for angular poitions - angular_range (tuple[float,float]): min and max angular position of joint - linear_range (tuple[float,float]): min and max positional values - relative_axis (Axis): joint axis relative to bound part - position (float): joint position - angle (float): angle of joint - - Raises: - ValueError: angle_reference must be normal to axis - """ - - @property - def symbol(self) -> Compound: - """A CAD symbol representing the cylindrical axis as bound to part""" - radius = (self.linear_range[1] - self.linear_range[0]) / 15 - return Compound.make_compound( - [ - Edge.make_line( - (0, 0, self.linear_range[0]), (0, 0, self.linear_range[1]) - ), - Edge.make_circle(radius), - ] - ).move(self.parent.location * self.relative_axis.location) - - # @property - # def axis_location(self) -> Location: - # """Current global location of joint axis""" - # return self.parent.location * self.relative_axis.location - - def __init__( - self, - label: str, - to_part: Union[Solid, Compound], - axis: Axis = Axis.Z, - angle_reference: VectorLike = None, - linear_range: tuple[float, float] = (0, inf), - angular_range: tuple[float, float] = (0, 360), - ): - self.axis = axis - self.linear_position = None - self.rotational_position = None - if angle_reference: - if not axis.is_normal(Axis((0, 0, 0), angle_reference)): - raise ValueError("angle_reference must be normal to axis") - self.angle_reference = Vector(angle_reference) - else: - self.angle_reference = Plane(origin=(0, 0, 0), z_dir=axis.direction).x_dir - self.angular_range = angular_range - self.linear_range = linear_range - self.relative_axis = axis.located(to_part.location.inverse()) - self.position = None - self.angle = None - to_part.joints[label]: dict[str, Joint] = self - super().__init__(label, to_part) - - def relative_to( - self, other: RigidJoint, position: float = None, angle: float = None - ): # pylint: disable=arguments-differ - """relative_to - CylindricalJoint - - Return the relative location from this joint to the RigidJoint of another object - - a sliding and rotating joint. - - Args: - other (RigidJoint): joint to connect to - position (float, optional): position within joint linear range. Defaults to middle. - angle (float, optional): angle within rotational range. - Defaults to angular_range minimum. - - Raises: - TypeError: other must be of type RigidJoint - ValueError: position out of range - ValueError: angle out of range - """ - if not isinstance(other, RigidJoint): - raise TypeError(f"other must of type RigidJoint not {type(other)}") - - position = sum(self.linear_range) / 2 if position is None else position - if not self.linear_range[0] <= position <= self.linear_range[1]: - raise ValueError( - f"position ({position}) must in range of {self.linear_range}" - ) - self.position = position - angle = sum(self.angular_range) / 2 if angle is None else angle - if not self.angular_range[0] <= angle <= self.angular_range[1]: - raise ValueError(f"angle ({angle}) must in range of {self.angular_range}") - self.angle = angle - - joint_relative_position = Location( - self.relative_axis.position + self.relative_axis.direction * position - ) - joint_rotation = Location( - Plane( - origin=(0, 0, 0), - x_dir=self.angle_reference.rotate(self.relative_axis, angle), - z_dir=self.relative_axis.direction, - ) - ) - - return ( - joint_relative_position * joint_rotation * other.relative_location.inverse() - ) - - -class BallJoint(Joint): - """BallJoint - - A component rotates around all 3 axes using a gimbal system (3 nested rotations). - - Args: - label (str): joint label - to_part (Union[Solid, Compound]): object to attach joint to - joint_location (Location): global location of joint - angular_range - (tuple[ tuple[float, float], tuple[float, float], tuple[float, float] ], optional): - X, Y, Z angle (min, max) pairs. Defaults to ((0, 360), (0, 360), (0, 360)). - angle_reference (Plane, optional): plane relative to part defining zero degrees of - rotation. Defaults to Plane.XY. - - Attributes: - relative_location (Location): joint location relative to bound part - angular_range - (tuple[ tuple[float, float], tuple[float, float], tuple[float, float] ]): - X, Y, Z angle (min, max) pairs. - angle_reference (Plane): plane relative to part defining zero degrees of - - """ - - @property - def symbol(self) -> Compound: - """A CAD symbol representing joint as bound to part""" - radius = self.parent.bounding_box().diagonal / 30 - circle_x = Edge.make_circle(radius, self.angle_reference) - circle_y = Edge.make_circle(radius, self.angle_reference.rotated((90, 0, 0))) - circle_z = Edge.make_circle(radius, self.angle_reference.rotated((0, 90, 0))) - - return Compound.make_compound( - [ - circle_x, - circle_y, - circle_z, - Compound.make_text( - "X", radius / 5, align=(Align.CENTER, Align.CENTER) - ).locate(circle_x.location_at(0.125) * Rotation(90, 0, 0)), - Compound.make_text( - "Y", radius / 5, align=(Align.CENTER, Align.CENTER) - ).locate(circle_y.location_at(0.625) * Rotation(90, 0, 0)), - Compound.make_text( - "Z", radius / 5, align=(Align.CENTER, Align.CENTER) - ).locate(circle_z.location_at(0.125) * Rotation(90, 0, 0)), - ] - ).move(self.parent.location * self.relative_location) - - def __init__( - self, - label: str, - to_part: Union[Solid, Compound], - joint_location: Location = Location(), - angular_range: tuple[ - tuple[float, float], tuple[float, float], tuple[float, float] - ] = ((0, 360), (0, 360), (0, 360)), - angle_reference: Plane = Plane.XY, - ): - self.relative_location = to_part.location.inverse() * joint_location - to_part.joints[label] = self - self.angular_range = angular_range - self.angle_reference = angle_reference - super().__init__(label, to_part) - - def relative_to( - self, other: RigidJoint, angles: RotationLike = None - ): # pylint: disable=arguments-differ - """relative_to - CylindricalJoint - - Return the relative location from this joint to the RigidJoint of another object - - Args: - other (RigidJoint): joint to connect to - angles (RotationLike, optional): orientation of other's parent relative to - self. Defaults to the minimums of the angle ranges. - - Raises: - TypeError: invalid other joint type - ValueError: angles out of range - """ - - if not isinstance(other, RigidJoint): - raise TypeError(f"other must of type RigidJoint not {type(other)}") - - rotation = ( - Rotation(*[self.angular_range[i][0] for i in [0, 1, 2]]) - if angles is None - else Rotation(*angles) - ) * self.angle_reference.location - - for i, rotations in zip( - [0, 1, 2], - [rotation.orientation.X, rotation.orientation.Y, rotation.orientation.Z], - ): - if not self.angular_range[i][0] <= rotations <= self.angular_range[i][1]: - raise ValueError( - f"angles ({angles}) must in range of {self.angular_range}" - ) - - return self.relative_location * rotation * other.relative_location.inverse() - - def downcast(obj: TopoDS_Shape) -> TopoDS_Shape: """Downcasts a TopoDS object to suitable specialized type diff --git a/tests/test_direct_api.py b/tests/test_direct_api.py index 977c9f3..9e62c96 100644 --- a/tests/test_direct_api.py +++ b/tests/test_direct_api.py @@ -59,18 +59,13 @@ from build123d.geometry import ( Vector, VectorLike, ) -from build123d.importers import import_brep, import_step, import_stl, import_svg +from build123d.importers import import_brep, import_step, import_stl from build123d.mesher import Mesher from build123d.topology import ( - BallJoint, Compound, - CylindricalJoint, Edge, Face, - LinearJoint, Plane, - RevoluteJoint, - RigidJoint, Shape, ShapeList, Shell, @@ -1245,234 +1240,6 @@ class TestImportExport(DirectApiTestCase): self.assertVectorAlmostEquals(stl_box.position, (0, 0, 0), 5) -class TestJoints(DirectApiTestCase): - def test_rigid_joint(self): - base = Solid.make_box(1, 1, 1) - j1 = RigidJoint("top", base, Location(Vector(0.5, 0.5, 1))) - fixed_top = Solid.make_box(1, 1, 1) - j2 = RigidJoint("bottom", fixed_top, Location((0.5, 0.5, 0))) - j1.connect_to(j2) - bbox = fixed_top.bounding_box() - self.assertVectorAlmostEquals(bbox.min, (0, 0, 1), 5) - self.assertVectorAlmostEquals(bbox.max, (1, 1, 2), 5) - - self.assertVectorAlmostEquals(j2.symbol.location.position, (0.5, 0.5, 1), 6) - self.assertVectorAlmostEquals(j2.symbol.location.orientation, (0, 0, 0), 6) - - def test_revolute_joint_with_angle_reference(self): - revolute_base = Solid.make_cylinder(1, 1) - j1 = RevoluteJoint( - label="top", - to_part=revolute_base, - axis=Axis((0, 0, 1), (0, 0, 1)), - angle_reference=(1, 0, 0), - angular_range=(0, 180), - ) - fixed_top = Solid.make_box(1, 0.5, 1) - j2 = RigidJoint("bottom", fixed_top, Location((0.5, 0.25, 0))) - - j1.connect_to(j2, 90) - bbox = fixed_top.bounding_box() - self.assertVectorAlmostEquals(bbox.min, (-0.25, -0.5, 1), 5) - self.assertVectorAlmostEquals(bbox.max, (0.25, 0.5, 2), 5) - - self.assertVectorAlmostEquals(j2.symbol.location.position, (0, 0, 1), 6) - self.assertVectorAlmostEquals(j2.symbol.location.orientation, (0, 0, 90), 6) - self.assertEqual(len(j1.symbol.edges()), 2) - - def test_revolute_joint_without_angle_reference(self): - revolute_base = Solid.make_cylinder(1, 1) - j1 = RevoluteJoint( - label="top", - to_part=revolute_base, - axis=Axis((0, 0, 1), (0, 0, 1)), - ) - self.assertVectorAlmostEquals(j1.angle_reference, (1, 0, 0), 5) - - def test_revolute_joint_error_bad_angle_reference(self): - """Test that the angle_reference must be normal to the axis""" - revolute_base = Solid.make_cylinder(1, 1) - with self.assertRaises(ValueError): - RevoluteJoint( - "top", - revolute_base, - axis=Axis((0, 0, 1), (0, 0, 1)), - angle_reference=(1, 0, 1), - ) - - def test_revolute_joint_error_bad_angle(self): - """Test that the joint angle is within bounds""" - revolute_base = Solid.make_cylinder(1, 1) - j1 = RevoluteJoint("top", revolute_base, Axis.Z, angular_range=(0, 180)) - fixed_top = Solid.make_box(1, 0.5, 1) - j2 = RigidJoint("bottom", fixed_top, Location((0.5, 0.25, 0))) - with self.assertRaises(ValueError): - j1.connect_to(j2, 270) - - def test_revolute_joint_error_bad_joint_type(self): - """Test that the joint angle is within bounds""" - revolute_base = Solid.make_cylinder(1, 1) - j1 = RevoluteJoint("top", revolute_base, Axis.Z, (0, 180)) - fixed_top = Solid.make_box(1, 0.5, 1) - j2 = RevoluteJoint("bottom", fixed_top, Axis.Z, (0, 180)) - with self.assertRaises(TypeError): - j1.connect_to(j2, 0) - - def test_linear_rigid_joint(self): - base = Solid.make_box(1, 1, 1) - j1 = LinearJoint( - "top", to_part=base, axis=Axis((0, 0.5, 1), (1, 0, 0)), linear_range=(0, 1) - ) - fixed_top = Solid.make_box(1, 1, 1) - j2 = RigidJoint("bottom", fixed_top, Location((0.5, 0.5, 0))) - j1.connect_to(j2, 0.25) - bbox = fixed_top.bounding_box() - self.assertVectorAlmostEquals(bbox.min, (-0.25, 0, 1), 5) - self.assertVectorAlmostEquals(bbox.max, (0.75, 1, 2), 5) - - self.assertVectorAlmostEquals(j2.symbol.location.position, (0.25, 0.5, 1), 6) - self.assertVectorAlmostEquals(j2.symbol.location.orientation, (0, 0, 0), 6) - - def test_linear_revolute_joint(self): - linear_base = Solid.make_box(1, 1, 1) - j1 = LinearJoint( - label="top", - to_part=linear_base, - axis=Axis((0, 0.5, 1), (1, 0, 0)), - linear_range=(0, 1), - ) - revolute_top = Solid.make_box(1, 0.5, 1).locate(Location((-0.5, -0.25, 0))) - j2 = RevoluteJoint( - label="top", - to_part=revolute_top, - axis=Axis((0, 0, 0), (0, 0, 1)), - angle_reference=(1, 0, 0), - angular_range=(0, 180), - ) - j1.connect_to(j2, position=0.25, angle=90) - - bbox = revolute_top.bounding_box() - self.assertVectorAlmostEquals(bbox.min, (0, 0, 1), 5) - self.assertVectorAlmostEquals(bbox.max, (0.5, 1, 2), 5) - - self.assertVectorAlmostEquals(j2.symbol.location.position, (0.25, 0.5, 1), 6) - self.assertVectorAlmostEquals(j2.symbol.location.orientation, (0, 0, 90), 6) - self.assertEqual(len(j1.symbol.edges()), 2) - - # Test invalid position - with self.assertRaises(ValueError): - j1.connect_to(j2, position=5, angle=90) - - # Test invalid angle - with self.assertRaises(ValueError): - j1.connect_to(j2, position=0.5, angle=270) - - # Test invalid joint - with self.assertRaises(TypeError): - j1.connect_to(Solid.make_box(1, 1, 1), position=0.5, angle=90) - - def test_cylindrical_joint(self): - cylindrical_base = ( - Solid.make_box(1, 1, 1) - .locate(Location((-0.5, -0.5, 0))) - .cut(Solid.make_cylinder(0.3, 1)) - ) - j1 = CylindricalJoint( - "base", - cylindrical_base, - Axis((0, 0, 1), (0, 0, -1)), - angle_reference=(1, 0, 0), - linear_range=(0, 1), - angular_range=(0, 90), - ) - dowel = Solid.make_cylinder(0.3, 1).cut( - Solid.make_box(1, 1, 1).locate(Location((-0.5, 0, 0))) - ) - j2 = RigidJoint("bottom", dowel, Location((0, 0, 0), (0, 0, 0))) - j1.connect_to(j2, 0.25, 90) - dowel_bbox = dowel.bounding_box() - self.assertVectorAlmostEquals(dowel_bbox.min, (0, -0.3, -0.25), 5) - self.assertVectorAlmostEquals(dowel_bbox.max, (0.3, 0.3, 0.75), 5) - - self.assertVectorAlmostEquals(j1.symbol.location.position, (0, 0, 1), 6) - self.assertVectorAlmostEquals( - j1.symbol.location.orientation, (-180, 0, -180), 6 - ) - self.assertEqual(len(j1.symbol.edges()), 2) - - # Test invalid position - with self.assertRaises(ValueError): - j1.connect_to(j2, position=5, angle=90) - - # Test invalid angle - with self.assertRaises(ValueError): - j1.connect_to(j2, position=0.5, angle=270) - - # Test invalid joint - with self.assertRaises(TypeError): - j1.connect_to(Solid.make_box(1, 1, 1), position=0.5, angle=90) - - def test_cylindrical_joint_error_bad_angle_reference(self): - """Test that the angle_reference must be normal to the axis""" - with self.assertRaises(ValueError): - CylindricalJoint( - "base", - Solid.make_box(1, 1, 1), - Axis((0, 0, 1), (0, 0, -1)), - angle_reference=(1, 0, 1), - linear_range=(0, 1), - angular_range=(0, 90), - ) - - def test_cylindrical_joint_error_bad_position_and_angle(self): - """Test that the joint angle is within bounds""" - - j1 = CylindricalJoint( - "base", - Solid.make_box(1, 1, 1), - Axis((0, 0, 1), (0, 0, -1)), - linear_range=(0, 1), - angular_range=(0, 90), - ) - j2 = RigidJoint("bottom", Solid.make_cylinder(1, 1), Location((0.5, 0.25, 0))) - with self.assertRaises(ValueError): - j1.connect_to(j2, position=0.5, angle=270) - - with self.assertRaises(ValueError): - j1.connect_to(j2, position=4, angle=30) - - def test_ball_joint(self): - socket_base = Solid.make_box(1, 1, 1).cut( - Solid.make_sphere(0.3, Plane((0.5, 0.5, 1))) - ) - j1 = BallJoint( - "socket", - socket_base, - Location((0.5, 0.5, 1)), - angular_range=((-45, 45), (-45, 45), (0, 360)), - ) - ball_rod = Solid.make_cylinder(0.15, 2).fuse( - Solid.make_sphere(0.3).locate(Location((0, 0, 2))) - ) - j2 = RigidJoint("ball", ball_rod, Location((0, 0, 2), (180, 0, 0))) - j1.connect_to(j2, (45, 45, 0)) - self.assertVectorAlmostEquals( - ball_rod.faces().filter_by(GeomType.PLANE)[0].center(CenterOf.GEOMETRY), - (1.914213562373095, -0.5, 2), - 5, - ) - - self.assertVectorAlmostEquals(j1.symbol.location.position, (0.5, 0.5, 1), 6) - self.assertVectorAlmostEquals(j1.symbol.location.orientation, (0, 0, 0), 6) - - with self.assertRaises(ValueError): - j1.connect_to(j2, (90, 45, 0)) - - # Test invalid joint - with self.assertRaises(TypeError): - j1.connect_to(Solid.make_box(1, 1, 1), (0, 0, 0)) - - class TestJupyter(DirectApiTestCase): def test_repr_javascript(self): shape = Solid.make_box(1, 1, 1)