From 4f79ce745c6e2d0c5a768dabb2ab55e19020f531 Mon Sep 17 00:00:00 2001 From: gumyr Date: Fri, 8 Dec 2023 16:06:45 -0500 Subject: [PATCH] Added the Triangle Sketch object --- docs/assets/triangle_example.svg | 25 ++++++++++++ docs/installation.rst | 2 +- docs/objects.rst | 7 ++++ docs/objects_2d.py | 26 ++++++++++++ pyproject.toml | 1 + src/build123d/__init__.py | 1 + src/build123d/objects_sketch.py | 69 +++++++++++++++++++++++++++++++- tests/test_build_sketch.py | 11 +++++ 8 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 docs/assets/triangle_example.svg 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: