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)