# Joints

**Joints** enable `Solid` and `Compound` objects to be arranged relative to each other in an intuitive manner – with the same degree of motion that is found with the equivalent physical joints.

Joints always work in pairs – a Joint can only be connected to another Joint as follows:

| Joint Type | Connects to | Example |
| :--- | :--- | :--- |
| **BallJoint** | `RigidJoint` | Gimbal |
| **CylindricalJoint** | `RigidJoint` | Screw |
| **LinearJoint** | `RigidJoint`, `RevoluteJoint` | Slider or Pin Slot |
| **RevoluteJoint** | `RigidJoint` | Hinge |
| **RigidJoint** | `RigidJoint` | Fixed |

Objects may have many joints bound to them, each with an identifying label. All Joint objects have a `symbol` property that can be displayed to help visualize their position and orientation (the [ocp-vscode](https://github.com/bernhard-42/vscode-ocp-cad-viewer) viewer has built-in support for displaying joints).

> **Note:**
> If joints are created within the scope of a `BuildPart` builder, the `to_part` parameter need not be specified. The builder will, on exit, automatically transfer the joints created in its scope to the part created.

The following sections provide more detail on the available joints and describe how they are used.

---

## Rigid Joint

A **rigid joint** positions two components relative to each other with no freedom of movement. When a `RigidJoint` is instantiated, it is assigned a label, a part to bind to (`to_part`), and a `joint_location` which defines both the position and orientation of the joint (see *Location*) – as follows:

```python
RigidJoint(label="outlet", to_part=pipe, joint_location=path.location_at(1))
```

Once a joint is bound to a part this way, the `connect_to()` method can be used to reposition another part relative to `self` (which stays fixed) – as follows:

```python
pipe.joints["outlet"].connect_to(flange_outlet.joints["pipe"])
```

> **Note:**
> Within a part, all of the joint labels must be unique.

The `connect_to()` method only does a **one-time re-position** of a part and does not bind them in any way; however, putting them into an `Assembly` will maintain their relative locations, as will combining parts with boolean operations or within a `BuildPart` context.

### Example: Connecting Flanges to a Pipe

Consider the following code where flanges are attached to the ends of a curved pipe:

```python
import copy
from build123d import *
from bd_warehouse.flange import WeldNeckFlange
from bd_warehouse.pipe import PipeSection
from ocp_vscode import *

flange_inlet = WeldNeckFlange(nps="10", flange_class=300)
flange_outlet = copy.copy(flange_inlet)

with BuildPart() as pipe_builder:
    # Create the pipe
    with BuildLine():
        path = TangentArc((0, 0, 0), (2 * FT, 0, 1 * FT), tangent=(1, 0, 0))
    with BuildSketch(Plane(origin=path @ 0, z_dir=path % 0)):
        PipeSection("10", material="stainless", identifier="40S")
    sweep()

    # Add the joints
    RigidJoint(label="inlet", joint_location=-path.location_at(0))
    RigidJoint(label="outlet", joint_location=path.location_at(1))

# Place the flanges at the ends of the pipe
pipe_builder.part.joints["inlet"].connect_to(flange_inlet.joints["pipe"])
pipe_builder.part.joints["outlet"].connect_to(flange_outlet.joints["pipe"])

show(pipe_builder, flange_inlet, flange_outlet, render_joints=True)
```

**Key Observations:**
* The locations of the joints are determined by the `location_at()` method.
* The `-` (negate) operator is used to reverse the direction of the location without changing its position.
* The `WeldNeckFlange` class predefines two joints (one at the pipe end and one at the face end), which are visualized when `render_joints=True` is set in the viewer.

---

## API Reference

### `class RigidJoint`

```python
class RigidJoint(label: str, to_part: Solid | Compound | None = None, joint_location: Location | None = None)
```
A rigid joint fixes two components to one another.

**Parameters:**
* `label` (str): Joint label.
* `to_part` (Union[Solid, Compound], optional): Object to attach joint to.
* `joint_location` (Location): Global location of joint.

**Variables:**
* `relative_location` (Location): Joint location relative to bound object.

#### Methods

**`connect_to`**
```python
connect_to(other: BallJoint, *, angles: Rotation | tuple[float, float, float] | None = None, **kwargs)
connect_to(other: CylindricalJoint, *, position: float | None = None, angle: float | None = None)
connect_to(other: LinearJoint, *, position: float | None = None)
connect_to(other: RevoluteJoint, *, angle: float | None = None)
connect_to(other: RigidJoint)
```
Connect the `RigidJoint` to another `Joint`.

* **Parameters:**
    * `other` (Joint): Joint to connect to.
    * `angle` (float, optional): Angle in degrees. Defaults to range min.
    * `angles` (RotationLike, optional): Angles about axes in degrees. Defaults to range minimums.
    * `position` (float, optional): Linear position. Defaults to linear range min.

**`relative_to`**
```python
relative_to(other: BallJoint, *, angles: Rotation | tuple[float, float, float] | None = None)
relative_to(other: CylindricalJoint, *, position: float | None = None, angle: float | None = None)
relative_to(other: LinearJoint, *, position: float | None = None)
relative_to(other: RevoluteJoint, *, angle: float | None = None)
relative_to(other: RigidJoint)
```
Relative location of `RigidJoint` to another `Joint`.

* **Parameters:**
    * `other` (RigidJoint): Relative to joint.
    * `angle` (float, optional): Angle in degrees. Defaults to range min.
    * `angles` (RotationLike, optional): Angles about axes in degrees. Defaults to range minimums.
    * `position` (float, optional): Linear position. Defaults to linear range min.
* **Raises:**
    * `TypeError`: `other` must be of a type in: `BallJoint`, `CylindricalJoint`, `LinearJoint`, `RevoluteJoint`, `RigidJoint`.

#### Properties

* **`location`**: `Location`
    * Location of joint.
* **`symbol`**: `Compound`
    * A CAD symbol (XYZ indicator) as bound to part.

## Revolute Joint

A **Revolute Joint** functions effectively as a hinge, allowing a component to rotate around a specific axis.

> **Reference:** The Joint Tutorial covers Revolute Joints in detail.

During the instantiation of a `RevoluteJoint`, there are three additional parameters compared to `RigidJoints` that allow the circular motion to be fully defined:
* `axis`: Defines the line around which rotation occurs.
* `angle_reference`: Defines the starting point (0 degrees) for rotation.
* `range`: Limits the rotation (min, max).

When using `connect_to()` with a Revolute Joint, an extra `angle` parameter is available. This allows you to change the relative position of the joined parts by modifying a single value.



### API Reference

#### `class RevoluteJoint`

```python
class RevoluteJoint(label: str, to_part: Solid | Compound | None = None, axis: Axis = Axis((0, 0, 0), (0, 0, 1)), angle_reference: Vector | tuple[float, float] | tuple[float, float, float] | Sequence[float] | None = None, angular_range: tuple[float, float] = (0, 360))
```

Component rotates around axis like a hinge.

**Parameters:**
* `label` (str): Joint label.
* `to_part` (Union[Solid, Compound], optional): 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).

**Variables:**
* `angle` (float): Current angle of the joint.
* `angle_reference` (Vector): Reference vector for angular positions.
* `angular_range` (tuple[float, float]): Min and max angular position of joint.
* `relative_axis` (Axis): Joint axis relative to the bound part.

**Raises:**
* `ValueError`: If `angle_reference` is not normal to `axis`.

---

#### Methods

**`connect_to`**

```python
connect_to(other: RigidJoint, *, angle: float | None = None)
```

Connect `RevoluteJoint` and `RigidJoint`.

* **Parameters:**
    * `other` (RigidJoint): The joint to connect to.
    * `angle` (float, optional): Angle in degrees. Defaults to range min.
* **Raises:**
    * `TypeError`: If `other` is not of type `RigidJoint`.
    * `ValueError`: If `angle` is out of the specified range.

**`relative_to`**

```python
relative_to(other: RigidJoint, *, angle: float | None = None)
```

Relative location of `RevoluteJoint` to `RigidJoint`.

* **Parameters:**
    * `other` (RigidJoint): The joint to connect to.
    * `angle` (float, optional): Angle in degrees. Defaults to range min.
* **Raises:**
    * `TypeError`: If `other` is not of type `RigidJoint`.
    * `ValueError`: If `angle` is out of the specified range.

---

#### Properties

* **`location`**: `Location`
    * Global location of the joint.
* **`symbol`**: `Compound`
    * A CAD symbol representing the axis of rotation as bound to the part.

## Linear Joint

A **Linear Joint** constrains a component to move along a single axis, similar to a sliding latch.



### Example: Sliding Latch

The code below demonstrates how to generate a latch and a slider, and then connect them using a `LinearJoint` for the latch and a `RigidJoint` for the slider.

```python
from build123d import *
from ocp_vscode import *

with BuildPart() as latch:
    # Basic box shape to start with filleted corners
    Box(70, 30, 14)
    end = latch.faces().sort_by(Axis.X)[-1]  # save the end with the hole
    fillet(latch.edges().filter_by(Axis.Z), 2)
    fillet(latch.edges().sort_by(Axis.Z)[-1], 1)
    # Make screw tabs
    with BuildSketch(latch.faces().sort_by(Axis.Z)[0]) as l4:
        with Locations((-30, 0), (30, 0)):
            SlotOverall(50, 10, rotation=90)
        Rectangle(50, 30)
        fillet(l4.vertices(Select.LAST), radius=2)
    extrude(amount=-2)
    with GridLocations(60, 40, 2, 2):
        Hole(2)
    # Create the hole from the end saved previously
    with BuildSketch(end) as slide_hole:
        add(end)
        offset(amount=-2)
        fillet(slide_hole.vertices(), 1)
    extrude(amount=-68, mode=Mode.SUBTRACT)
    # Slot for the handle to slide in
    with BuildSketch(latch.faces().sort_by(Axis.Z)[-1]):
        SlotOverall(32, 8)
    extrude(amount=-2, mode=Mode.SUBTRACT)
    
    # The slider will move align the x axis 12mm in each direction
    LinearJoint("latch", axis=Axis.X, linear_range=(-12, 12))

with BuildPart() as slide:
    # The slide will be a little smaller than the hole
    with BuildSketch() as s1:
        add(slide_hole.sketch)
        offset(amount=-0.25)
    # The extrusions aren't symmetric
    extrude(amount=46)
    extrude(slide.faces().sort_by(Axis.Z)[0], amount=20)
    # Round off the ends
    fillet(slide.edges().group_by(Axis.Z)[0], 1)
    fillet(slide.edges().group_by(Axis.Z)[-1], 1)
    # Create the knob
    with BuildSketch() as s2:
        with Locations((12, 0)):
            SlotOverall(15, 4, rotation=90)
        Rectangle(12, 7, align=(Align.MIN, Align.CENTER))
        fillet(s2.vertices(Select.LAST), 1)
        split(bisect_by=Plane.XZ)
    revolve(axis=Axis.X)
    
    # Align the joint to Plane.ZY flipped
    RigidJoint("slide", joint_location=Location(-Plane.ZY))

# Position the slide in the latch: -12 >= position <= 12
latch.part.joints["latch"].connect_to(slide.part.joints["slide"], position=12)

show(latch.part, slide.part, render_joints=True)
```

**Key Concepts:**

1.  **LinearJoint Definition:** The latch defines a `LinearJoint` with an axis (`Axis.X`) and movement limits (`linear_range=(-12, 12)`).
2.  **RigidJoint Definition:** The slider uses a `RigidJoint` with a specific location/orientation (`Location(-Plane.ZY)`) so the knob points "up".
3.  **Connection:** The `connect_to` method links the joints and specifies a `position` (12mm) which must be within the defined range.
4.  **Movement:** The slider can be moved by changing the `position` value. Values outside the `linear_range` limits will raise an exception.
5.  **Orientation:** Note that the slide is constructed in a different orientation than the direction of motion, which is handled by the joint definitions.

---

### API Reference

#### `class LinearJoint`

```python
class LinearJoint(label: str, to_part: Solid | Compound | None = None, axis: Axis = Axis((0, 0, 0), (0, 0, 1)), linear_range: tuple[float, float] = (0, inf))
```

Component moves along a single axis.

**Parameters:**
* `label` (str): Joint label.
* `to_part` (Union[Solid, Compound], optional): 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).

**Variables:**
* `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.

---

#### Methods

**`connect_to`**

```python
connect_to(other: RevoluteJoint, *, position: float | None = None, angle: float | None = None)
connect_to(other: RigidJoint, *, position: float | None = None)
```

Connect `LinearJoint` to another `Joint`.

* **Parameters:**
    * `other` (Joint): Joint to connect to.
    * `angle` (float, optional): Angle in degrees. Defaults to range min.
    * `position` (float, optional): Linear position. Defaults to linear range min.
* **Raises:**
    * `TypeError`: If `other` is not of type `RevoluteJoint` or `RigidJoint`.
    * `ValueError`: If `position` is out of range.
    * `ValueError`: If `angle` is out of range.

**`relative_to`**

```python
relative_to(other: RigidJoint, *, position: float | None = None)
relative_to(other: RevoluteJoint, *, position: float | None = None, angle: float | None = None)
```

Relative location of `LinearJoint` to `RevoluteJoint` or `RigidJoint`.

* **Parameters:**
    * `other` (Joint): Joint to connect to.
    * `angle` (float, optional): Angle in degrees. Defaults to range min.
    * `position` (float, optional): Linear position. Defaults to linear range min.
* **Raises:**
    * `TypeError`: If `other` is not of type `RevoluteJoint` or `RigidJoint`.
    * `ValueError`: If `position` is out of range.
    * `ValueError`: If `angle` is out of range.

---

#### Properties

* **`location`**: `Location`
    * Location of the joint.
* **`symbol`**: `Compound`
    * A CAD symbol of the linear axis positioned relative to `to_part`.

## Cylindrical Joint

A **CylindricalJoint** allows a component to rotate around and move along a single axis, effectively acting like a screw. It combines the functionality of a `LinearJoint` and a `RevoluteJoint`.



The `connect_to` method for these joints accepts both `position` and `angle` parameters.

### API Reference

#### `class CylindricalJoint`

```python
class CylindricalJoint(label: str, to_part: Solid | Compound | None = None, axis: Axis = Axis((0, 0, 0), (0, 0, 1)), angle_reference: Vector | tuple[float, float] | tuple[float, float, float] | Sequence[float] | None = None, linear_range: tuple[float, float] = (0, inf), angular_range: tuple[float, float] = (0, 360))
```

Component rotates around and moves along a single axis like a screw.

**Parameters:**
* `label` (str): Joint label.
* `to_part` (Union[Solid, Compound], optional): 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).

**Variables:**
* `axis` (Axis): Joint axis.
* `linear_position` (float): Linear joint position.
* `rotational_position` (float): Revolute joint angle in degrees.
* `angle_reference` (Vector): Reference for angular positions.
* `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`: If `angle_reference` must be normal to axis.

---

#### Methods

**`connect_to`**

```python
connect_to(other: RigidJoint, *, position: float | None = None, angle: float | None = None)
```

Connect `CylindricalJoint` and `RigidJoint`.

* **Parameters:**
    * `other` (Joint): Joint to connect to.
    * `position` (float, optional): Linear position. Defaults to linear range min.
    * `angle` (float, optional): Angle in degrees. Defaults to range min.
* **Raises:**
    * `TypeError`: If `other` is not of type `RigidJoint`.
    * `ValueError`: If `position` is out of range.
    * `ValueError`: If `angle` is out of range.

**`relative_to`**

```python
relative_to(other: RigidJoint, *, position: float | None = None, angle: float | None = None)
```

Relative location of `CylindricalJoint` to `RigidJoint`.

* **Parameters:**
    * `other` (Joint): Joint to connect to.
    * `position` (float, optional): Linear position. Defaults to linear range min.
    * `angle` (float, optional): Angle in degrees. Defaults to range min.
* **Raises:**
    * `TypeError`: If `other` is not of type `RigidJoint`.
    * `ValueError`: If `position` is out of range.
    * `ValueError`: If `angle` is out of range.

---

#### Properties

* **`location`**: `Location`
    * Location of joint.
* **`symbol`**: `Compound`
    * A CAD symbol representing the cylindrical axis as bound to part.

---

## Ball Joint

A **Ball Joint** allows a component to rotate around all 3 axes using a gimbal system (3 nested rotations). A common example is a rod end.



### Example: Rod End

The following code demonstrates creating a rod end with a threaded shaft and a ball, connecting them via a `BallJoint`.

```python
from build123d import *
from bd_warehouse.thread import IsoThread
from ocp_vscode import *

# Create the thread so the min radius is available below
thread = IsoThread(major_diameter=6, pitch=1, length=20, end_finishes=("fade", "raw"))
inner_radius = 15.89 / 2
inner_gap = 0.2

with BuildPart() as rod_end:
    # Create the outer shape
    with BuildSketch():
        Circle(22.25 / 2)
        with Locations((0, -12)):
            Rectangle(8, 1)
        make_hull()
        split(bisect_by=Plane.YZ)
    revolve(axis=Axis.Y)
    # Refine the shape
    with BuildSketch(Plane.YZ) as s2:
        Rectangle(25, 8, align=(Align.MIN, Align.CENTER))
        Rectangle(9, 10, align=(Align.MIN, Align.CENTER))
        chamfer(s2.vertices(), 0.5)
    revolve(axis=Axis.Z, mode=Mode.INTERSECT)
    # Add the screw shaft
    Cylinder(
        thread.min_radius,
        30,
        rotation=(90, 0, 0),
        align=(Align.CENTER, Align.CENTER, Align.MIN),
    )
    # Cutout the ball socket
    Sphere(inner_radius, mode=Mode.SUBTRACT)
    # Add thread
    with Locations((0, -30, 0)):
        add(thread, rotation=(-90, 0, 0))
    # Create the ball joint
    BallJoint(
        "socket",
        joint_location=Location(),
        angular_range=((-14, 14), (-14, 14), (0, 360)),
    )

with BuildPart() as ball:
    Sphere(inner_radius - inner_gap)
    Box(50, 50, 13, mode=Mode.INTERSECT)
    Hole(4)
    ball.part.color = Color("aliceblue")
    RigidJoint("ball", joint_location=Location())

rod_end.part.joints["socket"].connect_to(ball.part.joints["ball"], angles=(5, 10, 0))

show(rod_end.part, ball.part, s2)
```

**Key Observations:**
* **Limits:** Limits are defined during the instantiation of the `BallJoint` to ensure that the pin or bolt within the rod end does not interfere with the rod end itself.
* **Connection:** The `connect_to` method sets the three angles (`angles=(5, 10, 0)`), though in this specific example only two might be significant depending on the constraints.

### API Reference

#### `class BallJoint`

```python
class BallJoint(label: str, to_part: Solid | Compound | None = None, joint_location: Location | None = None, angular_range: tuple[tuple[float, float], tuple[float, float], tuple[float, float]] = ((0, 360), (0, 360), (0, 360)), angle_reference: Plane = Plane((0, 0, 0), (1, 0, 0), (0, 0, 1)))
```

A component rotates around all 3 axes using a gimbal system (3 nested rotations).

**Parameters:**
* `label` (str): Joint label.
* `to_part` (Union[Solid, Compound], optional): Object to attach joint to.
* `joint_location` (Location): Global location of joint.
* `angular_range` (tuple[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`.

**Variables:**
* `relative_location` (Location): Joint location relative to bound part.
* `angular_range` (tuple): X, Y, Z angle (min, max) pairs.
* `angle_reference` (Plane): Plane relative to part defining zero degrees.

---

#### Methods

**`connect_to`**

```python
connect_to(other: RigidJoint, *, angles: Rotation | tuple[float, float, float] | None = None)
```

Connect `BallJoint` and `RigidJoint`.

* **Parameters:**
    * `other` (RigidJoint): Joint to connect to.
    * `angles` (RotationLike, optional): Angles about axes in degrees. Defaults to range minimums.
* **Raises:**
    * `TypeError`: Invalid other joint type.
    * `ValueError`: If angles are out of range.

**`relative_to`**

```python
relative_to(other: RigidJoint, *, angles: Rotation | tuple[float, float, float] | None = None)
```

Return the relative location from this joint to the `RigidJoint` of another object.

* **Parameters:**
    * `other` (RigidJoint): Joint to connect to.
    * `angles` (RotationLike, optional): Angles about axes in degrees. Defaults to range minimums.
* **Raises:**
    * `TypeError`: Invalid other joint type.
    * `ValueError`: If angles are out of range.

---

#### Properties

* **`location`**: `Location`
    * Location of joint.
* **`symbol`**: `Compound`
    * A CAD symbol representing joint as bound to part.

