diff --git a/docs/assets/triangle_example.svg b/docs/assets/triangle_example.svg
new file mode 100644
index 0000000..b22b6a8
--- /dev/null
+++ b/docs/assets/triangle_example.svg
@@ -0,0 +1,25 @@
+
+
\ No newline at end of file
diff --git a/docs/installation.rst b/docs/installation.rst
index ec30cda..92b449f 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -131,7 +131,7 @@ A procedure for avoiding this issue is to install in a conda environment, which
conda create -n python=3.10
conda activate
conda install -c cadquery -c conda-forge cadquery=master
- pip install svgwrite svgpathtools anytree scipy ipython \
+ pip install svgwrite svgpathtools anytree scipy ipython trianglesolver \
ocp_tessellate webcolors==1.12 numpy numpy-quaternion cachetools==5.2.0 \
ocp_vscode requests orjson urllib3 certifi numpy-stl git+https://github.com/jdegenstein/py-lib3mf \
"svgpathtools>=1.5.1,<2" "svgelements>=1.9.1,<2"
diff --git a/docs/objects.rst b/docs/objects.rst
index 9e95cef..7308928 100644
--- a/docs/objects.rst
+++ b/docs/objects.rst
@@ -329,6 +329,12 @@ Reference
+++
Trapezoid defined by width, height and interior angles
+ .. grid-item-card:: :class:`~objects_sketch.Triangle`
+
+ .. image:: assets/triangle_example.svg
+
+ +++
+ Triangle defined by one side & two other sides or interior angles
Reference
@@ -353,6 +359,7 @@ Reference
.. autoclass:: drafting.TechnicalDrawing
.. autoclass:: Text
.. autoclass:: Trapezoid
+.. autoclass:: Triangle
3D Objects
----------
diff --git a/docs/objects_2d.py b/docs/objects_2d.py
index 8901e0c..9122fad 100644
--- a/docs/objects_2d.py
+++ b/docs/objects_2d.py
@@ -193,6 +193,32 @@ exporter.add_shape(visible, layer="Visible")
exporter.add_shape(hidden, layer="Hidden")
exporter.write(f"assets/controller.svg")
+d = Draft(line_width=0.1)
+# [Ex. 15]
+with BuildSketch() as isosceles_triangle:
+ t = Triangle(a=30, b=40, c=40)
+ # [Ex. 15]
+ ExtensionLine(t.edges().sort_by(Axis.Y)[0], 6, d, label="a")
+ ExtensionLine(t.edges().sort_by(Axis.X)[-1], 6, d, label="b")
+ ExtensionLine(t.edges().sort_by(SortBy.LENGTH)[-1], 6, d, label="c")
+a1 = CenterArc(t.vertices().group_by(Axis.Y)[0].sort_by(Axis.X)[0], 5, 0, t.B)
+a2 = CenterArc(t.vertices().group_by(Axis.Y)[0].sort_by(Axis.X)[-1], 5, 180 - t.C, t.C)
+a3 = CenterArc(t.vertices().sort_by(Axis.Y)[-1], 5, 270 - t.A / 2, t.A)
+p1 = CenterArc(t.vertices().group_by(Axis.Y)[0].sort_by(Axis.X)[0], 8, 0, t.B)
+p2 = CenterArc(t.vertices().group_by(Axis.Y)[0].sort_by(Axis.X)[-1], 8, 180 - t.C, t.C)
+p3 = CenterArc(t.vertices().sort_by(Axis.Y)[-1], 8, 270 - t.A / 2, t.A)
+t1 = Text("B", font_size=d.font_size).moved(Pos(p1 @ 0.5))
+t2 = Text("C", font_size=d.font_size).moved(Pos(p2 @ 0.5))
+t3 = Text("A", font_size=d.font_size).moved(Pos(p3 @ 0.5))
+
+s = 100 / max(*isosceles_triangle.sketch.bounding_box().size)
+svg = ExportSVG(scale=s)
+svg.add_layer("dashed", line_type=LineType.DASHED)
+svg.add_shape([a1, a2, a3], "dashed")
+svg.add_shape(isosceles_triangle.sketch)
+svg.add_shape([t1, t2, t3])
+svg.write("assets/triangle_example.svg")
+
# [Align]
with BuildSketch() as align:
diff --git a/pyproject.toml b/pyproject.toml
index dee382a..c77ce0e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -45,6 +45,7 @@ dependencies = [
"ipython >= 8.0.0, <9",
"py-lib3mf",
"ocpsvg",
+ "trianglesolver"
]
[project.urls]
diff --git a/src/build123d/__init__.py b/src/build123d/__init__.py
index eb0fdfa..219ced9 100644
--- a/src/build123d/__init__.py
+++ b/src/build123d/__init__.py
@@ -106,6 +106,7 @@ __all__ = [
"Text",
"TechnicalDrawing",
"Trapezoid",
+ "Triangle",
# 3D Part Objects
"BasePartObject",
"CounterBoreHole",
diff --git a/src/build123d/objects_sketch.py b/src/build123d/objects_sketch.py
index 4d5d977..ff870ab 100644
--- a/src/build123d/objects_sketch.py
+++ b/src/build123d/objects_sketch.py
@@ -27,7 +27,9 @@ license:
"""
from __future__ import annotations
-from math import cos, pi, radians, sin, tan
+import trianglesolver
+
+from math import cos, degrees, pi, radians, sin, tan
from typing import Iterable, Union
from build123d.build_common import LocationList, flatten_sequence, validate_inputs
@@ -629,3 +631,68 @@ class Trapezoid(BaseSketchObject):
pts.append(pts[0])
face = Face.make_from_wires(Wire.make_polygon(pts))
super().__init__(face, rotation, self.align, mode)
+
+
+class Triangle(BaseSketchObject):
+ """Sketch Object: Triangle
+
+ Add any triangle to the sketch by specifying the length of any side and any
+ two other side lengths or interior angles. Note that the interior angles are
+ opposite the side with the same designation (i.e. side 'a' is opposite angle 'A').
+
+ Args:
+ a (float, optional): side 'a' length. Defaults to None.
+ b (float, optional): side 'b' length. Defaults to None.
+ c (float, optional): side 'c' length. Defaults to None.
+ A (float, optional): interior angle 'A' in degrees. Defaults to None.
+ B (float, optional): interior angle 'B' in degrees. Defaults to None.
+ C (float, optional): interior angle 'C' in degrees. Defaults to None.
+ rotation (float, optional): angles to rotate objects. Defaults to 0.
+ align (Union[Align, tuple[Align, Align]], optional): align min, center, or max of object.
+ Defaults to None.
+ mode (Mode, optional): combination mode. Defaults to Mode.ADD.
+
+ Raises:
+ ValueError: One length and two other values were not provided
+ """
+
+ _applies_to = [BuildSketch._tag]
+
+ def __init__(
+ self,
+ *,
+ a: float = None,
+ b: float = None,
+ c: float = None,
+ A: float = None,
+ B: float = None,
+ C: float = None,
+ align: Union[None, Align, tuple[Align, Align]] = None,
+ rotation: float = 0,
+ mode: Mode = Mode.ADD,
+ ):
+ context = BuildSketch._get_context(self)
+ validate_inputs(context, self)
+
+ if [v is None for v in [a, b, c]].count(True) == 3 or [
+ v is None for v in [a, b, c, A, B, C]
+ ].count(True) != 3:
+ raise ValueError("One length and two other values must be provided")
+
+ A, B, C = (radians(angle) if angle is not None else None for angle in [A, B, C])
+ a, b, c, A, B, C = trianglesolver.solve(a, b, c, A, B, C)
+ self.a = a #: length of side 'a'
+ self.b = b #: length of side 'b'
+ self.c = c #: length of side 'c'
+ self.A = degrees(A) #: interior angle 'A' in degrees
+ self.B = degrees(B) #: interior angle 'B' in degrees
+ self.C = degrees(C) #: interior angle 'C' in degrees
+ triangle = Face.make_from_wires(
+ Wire.make_polygon(
+ [Vector(0, 0), Vector(a, 0), Vector(c, 0).rotate(Axis.Z, self.B)]
+ )
+ )
+ center_of_geometry = sum(Vector(v) for v in triangle.vertices()) / 3
+ triangle.move(Location(-center_of_geometry))
+ alignment = None if align is None else tuplify(align, 2)
+ super().__init__(obj=triangle, rotation=rotation, align=alignment, mode=mode)
diff --git a/tests/test_build_sketch.py b/tests/test_build_sketch.py
index 4346c57..dcf487b 100644
--- a/tests/test_build_sketch.py
+++ b/tests/test_build_sketch.py
@@ -382,6 +382,17 @@ class TestBuildSketchObjects(unittest.TestCase):
with BuildSketch() as test:
Trapezoid(6, 2, 30)
+ def test_triangle(self):
+ tri = Triangle(a=3, b=4, c=5)
+ self.assertAlmostEqual(tri.area, (3 * 4) / 2, 5)
+ tri = Triangle(c=5, C=90, a=3)
+ self.assertAlmostEqual(tri.area, (3 * 4) / 2, 5)
+
+ with self.assertRaises(ValueError):
+ Triangle(A=90, B=45, C=45)
+ with self.assertRaises(AssertionError):
+ Triangle(a=10, b=4, c=4)
+
def test_offset(self):
"""Test normal and error cases"""
with BuildSketch() as test: