Repository: ansys/pyansys-geometry (python)

Issue:
Arc using a point, a center and an angle
Same way we expose an Arc constructor using 3 points, we should also allow for the following:

* Create an arc based on 1 starting point, a center and an angle (probably clockwise)
* With the starting point and the center you have your radius. Now it's just a matter of moving it a certain angle.
* Input should be either:
  *  an Angle object - see https://geometry.docs.pyansys.com/version/0.5/api/ansys/geometry/core/misc/measurements/Angle.html#angle
  * a number (assume it is a radian) and an optional bool to use deg
  * If Angle object is provided, this one will take precedence and a warning will be raised
* Boolean for going counterclockwise should should also be allowed.

Pull request description:
feat: add new arc constructors
## Description
Addressing issues #1207 and #1206

## Issue linked
Closes #1207 and #1206

## Checklist
- [x] I have tested my changes locally.
- [x] I have added necessary documentation or updated existing documentation.
- [X] I have followed the coding style guidelines of this project.
- [x] I have added appropriate unit tests.
- [x] I have reviewed my changes before submitting this pull request.
- [X] I have linked the issue or issues that are solved to the PR if any.
- [X] I have assigned this PR to myself.
- [X] I have added the minimum version decorator to any new backend method implemented.
- [X] I have made sure that the title of my PR follows [Conventional commits style](https://www.conventionalcommits.org/en/v1.0.0/#summary) (e.g. ``feat: extrude circle to cylinder``)


Patch:
diff --git a/doc/changelog.d/1208.added.md b/doc/changelog.d/1208.added.md
new file mode 100644
index 00000000..4ea1d315
--- /dev/null
+++ b/doc/changelog.d/1208.added.md
@@ -0,0 +1,1 @@
+feat: add new arc constructors
\ No newline at end of file
diff --git a/doc/source/examples/02_sketching/basic_usage.mystnb b/doc/source/examples/02_sketching/basic_usage.mystnb
index aa400aef..a7e72fe0 100644
--- a/doc/source/examples/02_sketching/basic_usage.mystnb
+++ b/doc/source/examples/02_sketching/basic_usage.mystnb
@@ -105,6 +105,31 @@ sketch.select("Arc")
 sketch.plot_selection()
 ```
 
+There are also additional ways to draw arcs, such as by specifying the start, center point, and angle.
+
+```{code-cell} ipython3
+start_point = Point2D([2, 1], unit=u.m)
+center_point = Point2D([1, 1], unit=u.m)
+angle = 90
+sketch.arc_from_start_center_and_angle(
+    start_point, center_point, angle=90, tag="Arc_from_start_center_angle"
+)
+sketch.select("Arc_from_start_center_angle")
+sketch.plot_selection()
+```
+
+Or by specifying the start, end point, and radius.
+
+```{code-cell} ipython3
+start_point, end_point = Point2D([2, 1], unit=u.m), Point2D([0, 1], unit=u.meter)
+radius = 1 * u.m
+sketch.arc_from_start_end_and_radius(
+    start_point, end_point, radius, tag="Arc_from_start_end_radius"
+)
+sketch.select("Arc_from_start_end_radius")
+sketch.plot_selection()
+```
+
 ### Draw a slot
 
 You draw a slot by specifying the center, width, and height.
diff --git a/doc/source/examples/04_applied/01_naca_airfoils.mystnb b/doc/source/examples/04_applied/01_naca_airfoils.mystnb
index 55d09193..55878f48 100644
--- a/doc/source/examples/04_applied/01_naca_airfoils.mystnb
+++ b/doc/source/examples/04_applied/01_naca_airfoils.mystnb
@@ -78,7 +78,7 @@ def naca_airfoil_4digits(number: Union[int, str], n_points: int = 200) -> List[P
         # near the leading edge
         x = (1 - np.cos(i / (n_points - 1) * np.pi)) / 2
 
-        # Check if it is a symmetric airfoil or not
+        # Check if it is a symmetric airfoil
         if p == 0 and m == 0:
             # Camber line is zero in this case
             yc = 0
diff --git a/src/ansys/geometry/core/connection/product_instance.py b/src/ansys/geometry/core/connection/product_instance.py
index 1f01735d..64cf1db3 100644
--- a/src/ansys/geometry/core/connection/product_instance.py
+++ b/src/ansys/geometry/core/connection/product_instance.py
@@ -363,7 +363,7 @@ def _wait_for_backend(host: str, port: int, timeout: int):
 
 def _is_port_available(port: int, host: str = "localhost") -> bool:
     """
-    Check whether the argument port is available or not.
+    Check whether the argument port is available.
 
     The optional argument is the ip address where to check port availability.
     Its default is ``localhost``.
diff --git a/src/ansys/geometry/core/math/__init__.py b/src/ansys/geometry/core/math/__init__.py
index dd3493d5..7e767014 100644
--- a/src/ansys/geometry/core/math/__init__.py
+++ b/src/ansys/geometry/core/math/__init__.py
@@ -39,6 +39,7 @@ from ansys.geometry.core.math.constants import (
 )
 from ansys.geometry.core.math.frame import Frame
 from ansys.geometry.core.math.matrix import Matrix, Matrix33, Matrix44
+from ansys.geometry.core.math.misc import get_two_circle_intersections
 from ansys.geometry.core.math.plane import Plane
 from ansys.geometry.core.math.point import Point2D, Point3D
 from ansys.geometry.core.math.vector import UnitVector2D, UnitVector3D, Vector2D, Vector3D
diff --git a/src/ansys/geometry/core/math/misc.py b/src/ansys/geometry/core/math/misc.py
new file mode 100644
index 00000000..67753583
--- /dev/null
+++ b/src/ansys/geometry/core/math/misc.py
@@ -0,0 +1,97 @@
+# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates.
+# SPDX-License-Identifier: MIT
+#
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+"""Provides auxiliary math functions for PyAnsys Geometry."""
+
+
+from beartype import beartype as check_input_types
+from beartype.typing import Tuple, Union
+import numpy as np
+
+from ansys.geometry.core.typing import Real
+
+
+@check_input_types
+def get_two_circle_intersections(
+    x0: Real, y0: Real, r0: Real, x1: Real, y1: Real, r1: Real
+) -> Union[Tuple[Tuple[Real, Real], Tuple[Real, Real]], None]:
+    """
+    Get the intersection points of two circles.
+
+    Parameters
+    ----------
+    x0 : Real
+        x coordinate of the first circle.
+    y0 : Real
+        y coordinate of the first circle.
+    r0 : Real
+        Radius of the first circle.
+    x1 : Real
+        x coordinate of the second circle.
+    y1 : Real
+        y coordinate of the second circle.
+    r1 : Real
+        Radius of the second circle.
+
+    Notes
+    -----
+    This function is based on the following StackOverflow post:
+    https://stackoverflow.com/questions/55816902/finding-the-intersection-of-two-circles
+
+    That post is based on the following implementation:
+    https://paulbourke.net/geometry/circlesphere/
+
+    Returns
+    -------
+    Union[Tuple[Tuple[Real, Real], Tuple[Real, Real]], None]
+        Intersection points of the two circles if they intersect.
+        The points are returned as ``((x3, y3), (x4, y4))``, where ``(x3, y3)`` and ``(x4, y4)``
+        are the intersection points of the two circles. If the circles do not
+        intersect, then ``None`` is returned.
+    """
+    # circle 1: (x0, y0), radius r0
+    # circle 2: (x1, y1), radius r1
+    d = np.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
+
+    # non-intersecting
+    if d > r0 + r1:
+        return None
+    # one circle within other
+    if d < abs(r0 - r1):
+        return None
+    # coincident circles
+    if d == 0 and r0 == r1:
+        return None
+    else:
+        a = (r0**2 - r1**2 + d**2) / (2 * d)
+        h = np.sqrt(r0**2 - a**2)
+        x2 = x0 + a * (x1 - x0) / d
+        y2 = y0 + a * (y1 - y0) / d
+
+        dx = h * (y1 - y0) / d
+        dy = h * (x1 - x0) / d
+        x3 = x2 + dx
+        y3 = y2 - dy
+
+        x4 = x2 - dx
+        y4 = y2 + dy
+
+        return ((x3, y3), (x4, y4))
diff --git a/src/ansys/geometry/core/shapes/surfaces/torus.py b/src/ansys/geometry/core/shapes/surfaces/torus.py
index 7ef5096c..0664cbb7 100644
--- a/src/ansys/geometry/core/shapes/surfaces/torus.py
+++ b/src/ansys/geometry/core/shapes/surfaces/torus.py
@@ -22,7 +22,6 @@
 """Provides for creating and managing a torus."""
 
 from functools import cached_property
-from typing import Tuple
 
 from beartype import beartype as check_input_types
 from beartype.typing import Tuple, Union
diff --git a/src/ansys/geometry/core/sketch/arc.py b/src/ansys/geometry/core/sketch/arc.py
index bcc11802..b0fc9d49 100644
--- a/src/ansys/geometry/core/sketch/arc.py
+++ b/src/ansys/geometry/core/sketch/arc.py
@@ -22,16 +22,18 @@
 """Provides for creating and managing an arc."""
 
 from beartype import beartype as check_input_types
-from beartype.typing import Optional
+from beartype.typing import Optional, Union
 import numpy as np
 from pint import Quantity
 import pyvista as pv
 
+from ansys.geometry.core.math.matrix import Matrix
 from ansys.geometry.core.math.point import Point2D
 from ansys.geometry.core.math.vector import Vector2D
-from ansys.geometry.core.misc.measurements import DEFAULT_UNITS
+from ansys.geometry.core.misc.measurements import DEFAULT_UNITS, Angle, Distance
 from ansys.geometry.core.misc.units import UNITS
 from ansys.geometry.core.sketch.edge import SketchEdge
+from ansys.geometry.core.typing import Real
 
 
 class Arc(SketchEdge):
@@ -270,6 +272,7 @@ class Arc(SketchEdge):
         return arc_sub1 + arc_sub2
 
     @classmethod
+    @check_input_types
     def from_three_points(cls, start: Point2D, inter: Point2D, end: Point2D):
         """
         Create an arc from three given points.
@@ -342,3 +345,122 @@ class Arc(SketchEdge):
 
         # Finally... you can create the arc
         return Arc(start=start, end=end, center=center, clockwise=is_clockwise)
+
+    @classmethod
+    @check_input_types
+    def from_start_end_and_radius(
+        cls,
+        start: Point2D,
+        end: Point2D,
+        radius: Union[Quantity, Distance, Real],
+        convex_arc: Optional[bool] = False,
+        clockwise: Optional[bool] = False,
+    ):
+        """
+        Create an arc from a starting point, an ending point, and a radius.
+
+        Parameters
+        ----------
+        start : Point2D
+            Starting point of the arc.
+        end : Point2D
+            Ending point of the arc.
+        radius : Union[Quantity, Distance, Real]
+            Radius of the arc.
+        convex_arc : bool, default: False
+            Whether the arc is convex. The default is ``False``.
+            When ``False``, the arc is concave. When ``True``, the arc is convex.
+        clockwise : bool, default: False
+            Whether the arc spans the clockwise angle between the start and end points.
+            When ``False``, the arc spans the counter-clockwise angle.
+            When ``True``, the arc spands the clockwise angle.
+
+        Returns
+        -------
+        Arc
+            Arc generated from the three points.
+        """
+        # Compute the potential centers of the circle
+        # that could generate the arc
+        from ansys.geometry.core.math.misc import get_two_circle_intersections
+
+        # Sanitize the radius
+        radius = radius if isinstance(radius, Distance) else Distance(radius)
+        if radius.value <= 0:
+            raise ValueError("Radius must be a real positive value.")
+
+        # Unpack the points into its coordinates (in DEFAULT_UNITS.LENGTH)
+        x_s, y_s = start.tolist()
+        x_e, y_e = end.tolist()
+        r0 = r1 = radius.value.m_as(DEFAULT_UNITS.LENGTH)
+
+        # Compute the potential centers of the circle
+        centers = get_two_circle_intersections(x0=x_s, y0=y_s, r0=r0, x1=x_e, y1=y_e, r1=r1)
+        if centers is None:
+            raise ValueError("The provided points and radius do not yield a valid arc.")
+
+        # Choose the center depending on if the arc is convex
+        center = Point2D(centers[1] if convex_arc else centers[0])
+
+        # Create the arc
+        return Arc(start=start, end=end, center=center, clockwise=clockwise)
+
+    @classmethod
+    @check_input_types
+    def from_start_center_and_angle(
+        cls,
+        start: Point2D,
+        center: Point2D,
+        angle: Union[Angle, Quantity, Real],
+        clockwise: Optional[bool] = False,
+    ):
+        """
+        Create an arc from a starting point, a center point, and an angle.
+
+        Parameters
+        ----------
+        start : Point2D
+            Starting point of the arc.
+        center : Point2D
+            Center point of the arc.
+        angle : Union[Angle, Quantity, Real]
+            Angle of the arc.
+        clockwise : bool, default: False
+            Whether the provided angle should be considered clockwise.
+            When ``False``, the angle is considered counter-clockwise.
+            When ``True``, the angle is considered clockwise.
+
+        Returns
+        -------
+        Arc
+            Arc generated from the three points.
+        """
+        # Define a 2D vector from the center to the start point
+        to_start_vector = Vector2D.from_points(center, start)
+
+        # Perform sanity check for the angle
+        angle = angle if isinstance(angle, Angle) else Angle(angle)
+        rad_angle = angle.value.m_as(UNITS.radian)
+        cang = np.cos(rad_angle)
+        sang = np.sin(rad_angle)
+
+        # Rotate the vector by the angle
+        if clockwise:
+            rot_matrix = Matrix([[cang, sang], [-sang, cang]])
+        else:
+            rot_matrix = Matrix([[cang, -sang], [sang, cang]])
+
+        # Compute the end vector
+        to_end_vector = rot_matrix @ to_start_vector
+
+        # Define the end point
+        end = Point2D(
+            [
+                to_end_vector[0] + center.x.to_base_units().m,
+                to_end_vector[1] + center.y.to_base_units().m,
+            ],
+            center.base_unit,
+        )
+
+        # Create the arc
+        return Arc(start=start, end=end, center=center, clockwise=clockwise)
diff --git a/src/ansys/geometry/core/sketch/sketch.py b/src/ansys/geometry/core/sketch/sketch.py
index 13a5a071..571f1bc9 100644
--- a/src/ansys/geometry/core/sketch/sketch.py
+++ b/src/ansys/geometry/core/sketch/sketch.py
@@ -485,6 +485,89 @@ class Sketch:
         arc = Arc.from_three_points(start, inter, end)
         return self.edge(arc, tag)
 
+    def arc_from_start_end_and_radius(
+        self,
+        start: Point2D,
+        end: Point2D,
+        radius: Union[Quantity, Distance, Real],
+        convex_arc: Optional[bool] = False,
+        clockwise: Optional[bool] = False,
+        tag: Optional[str] = None,
+    ) -> "Sketch":
+        """
+        Add an arc to the sketch plane from the start and end points and a given radius.
+
+        Parameters
+        ----------
+        start : Point2D
+            Starting point of the arc.
+        end : Point2D
+            Ending point of the arc.
+        radius : Union[~pint.Quantity, Distance, Real]
+            Radius of the arc.
+        convex_arc : bool, default: False
+            Whether the arc is convex. The default is ``False``.
+            When ``False`` , the arc spans the concave version of the arc.
+            When ``True``, the arc spans the convex version of the arc.
+        clockwise : bool, default: False
+            Whether the arc spans the angle clockwise between the start
+            and end points. When ``False``, the arc spans the angle
+            counter-clockwise. When ``True``, the arc spans the angle
+            clockwise.
+        tag : str, default: None
+            User-defined label for identifying the edge.
+
+        Returns
+        -------
+        Sketch
+            Revised sketch state ready for further sketch actions.
+        """
+        arc = Arc.from_start_end_and_radius(
+            start,
+            end,
+            radius,
+            convex_arc=convex_arc,
+            clockwise=clockwise,
+        )
+        return self.edge(arc, tag)
+
+    def arc_from_start_center_and_angle(
+        self,
+        start: Point2D,
+        center: Point2D,
+        angle: Union[Quantity, Angle, Real],
+        clockwise: Optional[bool] = False,
+        tag: Optional[str] = None,
+    ) -> "Sketch":
+        """
+        Add an arc to the sketch plane from the start, center point, and angle.
+
+        Parameters
+        ----------
+        start : Point2D
+            Starting point of the arc.
+        center : Point2D
+            Center point of the arc.
+        angle : Union[~pint.Quantity, Angle, Real]
+            Angle of the arc.
+        clockwise : bool, default: False
+            Whether the arc spans the angle clockwise. The default is ``False``.
+            When ``False`` , the arc spans the angle counter-clockwise.
+            When ``True``, the arc spans the angle clockwise.
+
+        Returns
+        -------
+        Sketch
+            Revised sketch state ready for further sketch actions.
+        """
+        arc = Arc.from_start_center_and_angle(
+            start,
+            center,
+            angle,
+            clockwise=clockwise,
+        )
+        return self.edge(arc, tag)
+
     def triangle(
         self,
         point1: Point2D,


Test patch:
diff --git a/tests/test_math.py b/tests/test_math.py
index f387f396..a56489b0 100644
--- a/tests/test_math.py
+++ b/tests/test_math.py
@@ -46,6 +46,7 @@ from ansys.geometry.core.math import (
     UnitVector3D,
     Vector2D,
     Vector3D,
+    get_two_circle_intersections,
 )
 from ansys.geometry.core.misc import UNITS
 
@@ -1095,3 +1096,74 @@ def test_div_operator_point(a, b_ref):
     assert b.unit
     assert a.unit.is_compatible_with(b.unit)
     assert a.unit.dimensionality == b.unit.dimensionality
+
+
+def test_circle_intersections():
+    """Test circle intersections using the get_two_circle_intersections method."""
+
+    # Circle 1 - a point and its radius
+    x0, y0, r0 = 5, 0, 5
+    # Circle 2 - a point and its radius
+    x1, y1, r1 = 0, 0, 5
+
+    # Check the intersection points
+    intersections = get_two_circle_intersections(x0, y0, r0, x1, y1, r1)
+
+    assert intersections is not None
+    assert len(intersections) == 2
+    assert np.allclose(intersections[0], [2.5, +4.3301270189])
+    assert np.allclose(intersections[1], [2.5, -4.3301270189])
+
+
+def test_circle_intersections_non_intersecting():
+    """
+    Test circle intersections using the get_two_circle_intersections method.
+
+    This test checks the case where the circles do not intersect.
+    """
+
+    # Circle 1 - a point and its radius
+    x0, y0, r0 = 5, 0, 2
+    # Circle 2 - a point and its radius
+    x1, y1, r1 = 0, 0, 2
+
+    # Check the intersection points
+    intersections = get_two_circle_intersections(x0, y0, r0, x1, y1, r1)
+
+    assert intersections is None
+
+
+def test_circle_intersections_non_intersecting_inside():
+    """
+    Test circle intersections using the get_two_circle_intersections method.
+
+    This test checks the case where the circles are one inside the other.
+    """
+
+    # Circle 1 - a point and its radius
+    x0, y0, r0 = 6, 0, 1
+    # Circle 2 - a point and its radius
+    x1, y1, r1 = 10, 0, 8
+
+    # Check the intersection points
+    intersections = get_two_circle_intersections(x0, y0, r0, x1, y1, r1)
+
+    assert intersections is None
+
+
+def test_circle_intersections_coincident():
+    """
+    Test circle intersections using the get_two_circle_intersections method.
+
+    This test checks the case where the circles are coincident.
+    """
+
+    # Circle 1 - a point and its radius
+    x0, y0, r0 = 5, 0, 5
+    # Circle 2 - a point and its radius
+    x1, y1, r1 = 5, 0, 5
+
+    # Check the intersection points
+    intersections = get_two_circle_intersections(x0, y0, r0, x1, y1, r1)
+
+    assert intersections is None
diff --git a/tests/test_sketch.py b/tests/test_sketch.py
index 90ee1a88..6d1fde2e 100644
--- a/tests/test_sketch.py
+++ b/tests/test_sketch.py
@@ -805,3 +805,274 @@ def test_sketch_pyconus2024_voglster_issue1195():
     assert sketch.edges[0].start == p_start
     assert sketch.edges[0].end == p_end
     assert sketch.edges[0].center == p_center
+
+
+def test_arc_start_end_radius_default():
+    """
+    Test arc generation from a start and an end point with a certain radius.
+
+    The arc is created with the default values for the convex_arc and clockwise
+    parameters.
+    """
+    start = Point2D([0, 5])
+    end = Point2D([5, 0])
+    radius = 5
+
+    # Verify the arc is created correctly
+    arc_ctor = Arc.from_start_end_and_radius(start, end, radius)
+    assert arc_ctor.start == start
+    assert arc_ctor.end == end
+    assert arc_ctor.radius.m_as(DEFAULT_UNITS.LENGTH) == radius
+
+    # Center of the arc should be at (0, 0)... but it won't
+    # be exactly (0, 0) due to floating point errors
+    assert np.allclose(arc_ctor.center, Point2D([0, 0]))
+
+    # Verify the arc is a 270 degree arc --> COUNTERCLOCKWISE IS DEFAULT
+    assert np.isclose(arc_ctor.angle.m_as(UNITS.radian), 2 * np.pi * (3 / 4))
+
+    # Verify the arc length --> COUNTERCLOCKWISE IS DEFAULT
+    assert np.isclose(arc_ctor.length.m_as(DEFAULT_UNITS.LENGTH), 2 * np.pi * radius * (3 / 4))
+
+
+def test_arc_start_end_radius_clockwise():
+    """
+    Test arc generation from a start and an end point with a certain radius.
+
+    The arc is requested to be clockwise.
+    """
+    start = Point2D([0, 5])
+    end = Point2D([5, 0])
+    radius = 5
+
+    # Verify the arc is created correctly
+    arc_ctor = Arc.from_start_end_and_radius(start, end, radius, clockwise=True)
+    assert arc_ctor.start == start
+    assert arc_ctor.end == end
+    assert arc_ctor.radius.m_as(DEFAULT_UNITS.LENGTH) == radius
+
+    # Center of the arc should be at (0, 0)... but it won't
+    # be exactly (0, 0) due to floating point errors
+    assert np.allclose(arc_ctor.center, Point2D([0, 0]))
+
+    # Verify the arc is a 90 degree arc --> CLOCKWISE requested
+    assert np.isclose(arc_ctor.angle.m_as(UNITS.radian), 2 * np.pi * (1 / 4))
+
+    # Verify the arc length --> CLOCKWISE requested
+    assert np.isclose(arc_ctor.length.m_as(DEFAULT_UNITS.LENGTH), 2 * np.pi * radius * (1 / 4))
+
+
+def test_arc_start_end_radius_convex():
+    """
+    Test arc generation from a start and an end point with a certain radius.
+
+    This test is for a convex arc.
+    """
+    start = Point2D([0, 5])
+    end = Point2D([5, 0])
+    radius = 5
+
+    # Verify the arc is created correctly
+    arc_ctor = Arc.from_start_end_and_radius(start, end, radius, convex_arc=True)
+    assert arc_ctor.start == start
+    assert arc_ctor.end == end
+    assert arc_ctor.radius.m_as(DEFAULT_UNITS.LENGTH) == radius
+
+    # Center of the arc should be at (5, 5)... but it won't
+    # be exactly (5, 5) due to floating point errors
+    assert np.allclose(arc_ctor.center, Point2D([5, 5]))
+
+    # Verify the arc is a 90 degree arc --> COUNTERCLOCKWISE IS DEFAULT
+    # For the convex arc, the angle is 90 degrees
+    assert np.isclose(arc_ctor.angle.m_as(UNITS.radian), 2 * np.pi * (1 / 4))
+
+    # Verify the arc length --> COUNTERCLOCKWISE IS DEFAULT
+    assert np.isclose(arc_ctor.length.m_as(DEFAULT_UNITS.LENGTH), 2 * np.pi * radius * (1 / 4))
+
+
+def test_arc_start_end_radius_convex_and_clockwise():
+    """
+    Test arc generation from a start and an end point with a certain radius.
+
+    This test is for a convex arc that is requested to be clockwise.
+    """
+    start = Point2D([0, 5])
+    end = Point2D([5, 0])
+    radius = 5
+
+    # Verify the arc is created correctly
+    arc_ctor = Arc.from_start_end_and_radius(start, end, radius, convex_arc=True, clockwise=True)
+    assert arc_ctor.start == start
+    assert arc_ctor.end == end
+    assert arc_ctor.radius.m_as(DEFAULT_UNITS.LENGTH) == radius
+
+    # Center of the arc should be at (5, 5)... but it won't
+    # be exactly (5, 5) due to floating point errors
+    assert np.allclose(arc_ctor.center, Point2D([5, 5]))
+
+    # Verify the arc is a 90 degree arc --> CLOCKWISE requested
+    # For the convex arc, the angle is 90 degrees
+    assert np.isclose(arc_ctor.angle.m_as(UNITS.radian), 2 * np.pi * (3 / 4))
+
+    # Verify the arc length --> CLOCKWISE requested
+    assert np.isclose(arc_ctor.length.m_as(DEFAULT_UNITS.LENGTH), 2 * np.pi * radius * (3 / 4))
+
+
+def test_arc_start_end_radius_through_sketch_ctor():
+    """
+    Test arc generation from a start and an end point with a certain radius.
+
+    This test is for the Sketch class convenience constructor.
+    """
+    start = Point2D([0, 5])
+    end = Point2D([5, 0])
+    radius = 5
+
+    # Create the arc through the Sketch convenience constructor
+    sketch = Sketch()
+    sketch.arc_from_start_end_and_radius(start, end, radius, convex_arc=True, clockwise=True)
+
+    # Verify the arc is created correctly
+    arc = sketch.edges[0]
+    assert arc.start == start
+    assert arc.end == end
+    assert arc.radius.m_as(DEFAULT_UNITS.LENGTH) == radius
+
+    # Center of the arc should be at (5, 5)... but it won't
+    # be exactly (5, 5) due to floating point errors
+    assert np.allclose(arc.center, Point2D([5, 5]))
+
+    # Verify the arc is a 90-degree arc --> CLOCKWISE requested
+    # For the convex arc, the angle is 90 degrees
+    assert np.isclose(arc.angle.m_as(UNITS.radian), 2 * np.pi * (3 / 4))
+
+    # Verify the arc length --> CLOCKWISE requested
+    assert np.isclose(arc.length.m_as(DEFAULT_UNITS.LENGTH), 2 * np.pi * radius * (3 / 4))
+
+
+def test_start_end_radius_error_cases():
+    """
+    Test arc generation from a start and an end point with a certain radius.
+
+    This test is for cases where the inputs are invalid.
+    """
+    # Invalid start and end points - same point
+    with pytest.raises(
+        ValueError, match="The provided points and radius do not yield a valid arc."
+    ):
+        Arc.from_start_end_and_radius(Point2D([0, 5]), Point2D([0, 5]), 5)
+
+    # Invalid radius - negative radius
+    with pytest.raises(ValueError, match="Radius must be a real positive value."):
+        Arc.from_start_end_and_radius(Point2D([0, 5]), Point2D([5, 0]), -5)
+
+    # Invalid radius - zero radius
+    with pytest.raises(ValueError, match="Radius must be a real positive value."):
+        Arc.from_start_end_and_radius(Point2D([0, 5]), Point2D([5, 0]), 0)
+
+    # Invalid radius - radius too small
+    with pytest.raises(
+        ValueError, match="The provided points and radius do not yield a valid arc."
+    ):
+        Arc.from_start_end_and_radius(Point2D([0, 5]), Point2D([5, 0]), 1)
+
+
+def test_start_center_and_angle_default():
+    """
+    Test arc generation from a start point, center point, and angle.
+
+    The arc is created with the default values for the clockwise parameter.
+    """
+    # Inputs
+    start = Point2D([0, 5])
+    center = Point2D([0, 0])
+    angle = 3 * np.pi / 2
+
+    # Expected radius
+    radius = 5
+
+    # Expected end point
+    end = Point2D([5, 0])
+
+    # Create the arc
+    arc_ctor = Arc.from_start_center_and_angle(start, center, angle)
+
+    # Verify the arc is created correctly
+    assert arc_ctor.start == start
+    assert arc_ctor.center == center
+    assert np.isclose(arc_ctor.angle.m_as(UNITS.radian), angle)
+    assert arc_ctor.radius.m_as(DEFAULT_UNITS.LENGTH) == radius
+
+    # Verify the arc is a 270 degree arc
+    assert np.isclose(arc_ctor.length.m_as(DEFAULT_UNITS.LENGTH), radius * angle)
+
+    # Verify the final point of the arc assuming a counterclockwise arc
+    assert np.allclose(arc_ctor.end, end)
+
+
+def test_start_center_and_angle_clockwise():
+    """
+    Test arc generation from a start point, center point, and angle.
+
+    The arc is requested to be clockwise.
+    """
+    # Inputs
+    start = Point2D([0, 5])
+    center = Point2D([0, 0])
+    angle = 3 * np.pi / 2
+
+    # Expected radius
+    radius = 5
+
+    # Expected end point
+    end = Point2D([-5, 0])
+
+    # Create the arc
+    arc_ctor = Arc.from_start_center_and_angle(start, center, angle, clockwise=True)
+
+    # Verify the arc is created correctly
+    assert arc_ctor.start == start
+    assert arc_ctor.center == center
+    assert np.isclose(arc_ctor.angle.m_as(UNITS.radian), angle)
+    assert arc_ctor.radius.m_as(DEFAULT_UNITS.LENGTH) == radius
+
+    # Verify the arc is a 270-degree arc
+    assert np.isclose(arc_ctor.length.m_as(DEFAULT_UNITS.LENGTH), radius * angle)
+
+    # Verify the final point of the arc assuming a clockwise arc
+    assert np.allclose(arc_ctor.end, end)
+
+
+def test_start_center_and_angle_clockwise_from_sketch():
+    """
+    Test arc generation from a start point, center point, and angle.
+
+    This test is for the Sketch class convenience constructor.
+    """
+    # Inputs
+    start = Point2D([0, 5])
+    center = Point2D([0, 0])
+    angle = 3 * np.pi / 2
+
+    # Expected radius
+    radius = 5
+
+    # Expected end point
+    end = Point2D([-5, 0])
+
+    # Create the arc
+    sketch = Sketch()
+    sketch.arc_from_start_center_and_angle(start, center, angle, clockwise=True)
+
+    # Verify the arc is created correctly
+    arc = sketch.edges[0]
+    assert arc.start == start
+    assert arc.center == center
+    assert np.isclose(arc.angle.m_as(UNITS.radian), angle)
+    assert arc.radius.m_as(DEFAULT_UNITS.LENGTH) == radius
+
+    # Verify the arc is a 270 degree arc
+    assert np.isclose(arc.length.m_as(DEFAULT_UNITS.LENGTH), radius * angle)
+
+    # Verify the final point of the arc assuming a clockwise arc
+    assert np.allclose(arc.end, end)