diff --git a/doc/changelog.d/1495.added.md b/doc/changelog.d/1495.added.md new file mode 100644 index 0000000000..0266dfa43d --- /dev/null +++ b/doc/changelog.d/1495.added.md @@ -0,0 +1 @@ +add chamfer tool \ No newline at end of file diff --git a/doc/source/_static/thumbnails/chamfer.png b/doc/source/_static/thumbnails/chamfer.png new file mode 100644 index 0000000000..89a78bc9b4 Binary files /dev/null and b/doc/source/_static/thumbnails/chamfer.png differ diff --git a/doc/source/conf.py b/doc/source/conf.py index 7758b3cc06..1e77550c11 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -303,6 +303,7 @@ def intersphinx_pyansys_geometry(switcher_version: str): "examples/03_modeling/design_tree": "_static/thumbnails/design_tree.png", "examples/03_modeling/service_colors": "_static/thumbnails/service_colors.png", "examples/03_modeling/surface_bodies": "_static/thumbnails/quarter_sphere.png", + "examples/03_modeling/chamfer": "_static/thumbnails/chamfer.png", "examples/04_applied/01_naca_airfoils": "_static/thumbnails/naca_airfoils.png", "examples/04_applied/02_naca_fluent": "_static/thumbnails/naca_fluent.png", } diff --git a/doc/source/examples.rst b/doc/source/examples.rst index fc4c5abb1d..3ee9cc6498 100644 --- a/doc/source/examples.rst +++ b/doc/source/examples.rst @@ -49,6 +49,7 @@ These examples demonstrate service-based modeling operations. examples/03_modeling/design_tree.mystnb examples/03_modeling/service_colors.mystnb examples/03_modeling/surface_bodies.mystnb + examples/03_modeling/chamfer.mystnb Applied examples ---------------- diff --git a/doc/source/examples/03_modeling/boolean_operations.mystnb b/doc/source/examples/03_modeling/boolean_operations.mystnb index 26727f9005..6f17137450 100644 --- a/doc/source/examples/03_modeling/boolean_operations.mystnb +++ b/doc/source/examples/03_modeling/boolean_operations.mystnb @@ -91,7 +91,7 @@ output list is sorted according to the picking order. pl = GeometryPlotter(allow_picking=True) pl.plot(design.bodies) pl.show() -bodies: List[Body] = GeometryPlotter(allow_picking=True).show(design.bodies) +bodies: list[Body] = GeometryPlotter(allow_picking=True).show(design.bodies) ``` Otherwise, you can select bodies from the design directly. diff --git a/doc/source/examples/03_modeling/chamfer.mystnb b/doc/source/examples/03_modeling/chamfer.mystnb new file mode 100644 index 0000000000..eeddcfb27f --- /dev/null +++ b/doc/source/examples/03_modeling/chamfer.mystnb @@ -0,0 +1,63 @@ +--- +jupytext: + text_representation: + extension: .mystnb + format_name: myst + format_version: 0.13 + jupytext_version: 1.16.4 +kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +# Modeling: Chamfer edges and faces +A chamfer is an angled cut on an edge. Chamfers can be created using the ``Modeler.pull_tools`` module. + ++++ + +## Create a block +Launch the modeler and create a block. + +```{code-cell} ipython3 +from ansys.geometry.core import launch_modeler, Modeler + +modeler = Modeler() +print(modeler) +``` + +```{code-cell} ipython3 +from ansys.geometry.core.sketch import Sketch +from ansys.geometry.core.math import Point2D + +design = modeler.create_design("chamfer_block") +body = design.extrude_sketch("block", Sketch().box(Point2D([0, 0]), 1, 1), 1) + +body.plot() +``` + +## Chamfer edges +Create a uniform chamfer on all edges of the block. + +```{code-cell} ipython3 +modeler.pull_tools.chamfer(body.edges, distance=0.1) + +body.plot() +``` + +## Chamfer faces +The chamfer of a face can also be modified. Create a chamfer on a single edge and then modify the chamfer distance value by providing the newly created face that represents the chamfer. + +```{code-cell} ipython3 +body = design.extrude_sketch("box", Sketch().box(Point2D([0,0]), 1, 1), 1) + +modeler.pull_tools.chamfer(body.edges[0], distance=0.1) + +body.plot() +``` + +```{code-cell} ipython3 +modeler.pull_tools.chamfer(body.faces[-1], distance=0.3) + +body.plot() +``` diff --git a/doc/source/examples/04_applied/01_naca_airfoils.mystnb b/doc/source/examples/04_applied/01_naca_airfoils.mystnb index 87d74e195a..df34995454 100644 --- a/doc/source/examples/04_applied/01_naca_airfoils.mystnb +++ b/doc/source/examples/04_applied/01_naca_airfoils.mystnb @@ -43,7 +43,7 @@ import numpy as np from ansys.geometry.core.math import Point2D -def naca_airfoil_4digits(number: Union[int, str], n_points: int = 200) -> List[Point2D]: +def naca_airfoil_4digits(number: Union[int, str], n_points: int = 200) -> list[Point2D]: """ Generate a NACA 4-digits airfoil. @@ -58,7 +58,7 @@ def naca_airfoil_4digits(number: Union[int, str], n_points: int = 200) -> List[P Returns ------- - List[Point2D] + list[Point2D] List of points that define the airfoil. """ # Check if the number is a string diff --git a/src/ansys/geometry/core/designer/body.py b/src/ansys/geometry/core/designer/body.py index 4f5414812b..6b57beb9cf 100644 --- a/src/ansys/geometry/core/designer/body.py +++ b/src/ansys/geometry/core/designer/body.py @@ -1207,11 +1207,15 @@ def reset_tessellation_cache(func): # noqa: N805 @wraps(func) def wrapper(self: "Body", *args, **kwargs): - self._template._tessellation = None + self._reset_tessellation_cache() return func(self, *args, **kwargs) return wrapper + def _reset_tessellation_cache(self): # noqa: N805 + """Reset the cached tessellation for a body.""" + self._template._tessellation = None + @property def id(self) -> str: # noqa: D102 return self._id diff --git a/src/ansys/geometry/core/designer/edge.py b/src/ansys/geometry/core/designer/edge.py index d4cd78f4e4..d43ac73235 100644 --- a/src/ansys/geometry/core/designer/edge.py +++ b/src/ansys/geometry/core/designer/edge.py @@ -100,6 +100,11 @@ def _grpc_id(self) -> EntityIdentifier: """Entity ID of this edge on the server side.""" return EntityIdentifier(id=self._id) + @property + def body(self) -> "Body": + """Body of the edge.""" + return self._body + @property def is_reversed(self) -> bool: """Flag indicating if the edge is reversed.""" diff --git a/src/ansys/geometry/core/modeler.py b/src/ansys/geometry/core/modeler.py index e7f98b1648..f0caeb6cf0 100644 --- a/src/ansys/geometry/core/modeler.py +++ b/src/ansys/geometry/core/modeler.py @@ -42,6 +42,7 @@ from ansys.geometry.core.misc.options import ImportOptions from ansys.geometry.core.tools.measurement_tools import MeasurementTools from ansys.geometry.core.tools.prepare_tools import PrepareTools +from ansys.geometry.core.tools.pull_tools import PullTools from ansys.geometry.core.tools.repair_tools import RepairTools from ansys.geometry.core.typing import Real @@ -59,7 +60,7 @@ class Modeler: ---------- host : str, default: DEFAULT_HOST Host where the server is running. - port : Union[str, int], default: DEFAULT_PORT + port : str | int, default: DEFAULT_PORT Port number where the server is running. channel : ~grpc.Channel, default: None gRPC channel for server communication. @@ -128,6 +129,7 @@ def __init__( self._repair_tools = RepairTools(self._grpc_client) self._prepare_tools = PrepareTools(self._grpc_client) self._measurement_tools = MeasurementTools(self._grpc_client) + self._pull_tools = PullTools(self._grpc_client) # Maintaining references to all designs within the modeler workspace self._designs: dict[str, "Design"] = {} @@ -498,6 +500,11 @@ def measurement_tools(self) -> MeasurementTools: """Access to measurement tools.""" return self._measurement_tools + @property + def pull_tools(self) -> PullTools: + """Access to pull tools.""" + return self._pull_tools + @min_backend_version(25, 1, 0) def get_service_logs( self, diff --git a/src/ansys/geometry/core/tools/__init__.py b/src/ansys/geometry/core/tools/__init__.py index a5da5a3644..7c3e5a5ea8 100644 --- a/src/ansys/geometry/core/tools/__init__.py +++ b/src/ansys/geometry/core/tools/__init__.py @@ -28,5 +28,6 @@ ExtraEdgeProblemAreas, InexactEdgeProblemAreas, ) +from ansys.geometry.core.tools.pull_tools import PullTools from ansys.geometry.core.tools.repair_tool_message import RepairToolMessage from ansys.geometry.core.tools.repair_tools import RepairTools diff --git a/src/ansys/geometry/core/tools/pull_tools.py b/src/ansys/geometry/core/tools/pull_tools.py new file mode 100644 index 0000000000..58dd105d05 --- /dev/null +++ b/src/ansys/geometry/core/tools/pull_tools.py @@ -0,0 +1,81 @@ +# 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 tools for pulling geometry.""" + +from typing import TYPE_CHECKING, List, Union + +from ansys.api.geometry.v0.commands_pb2 import ChamferRequest +from ansys.api.geometry.v0.commands_pb2_grpc import CommandsStub +from ansys.geometry.core.connection import GrpcClient +from ansys.geometry.core.errors import protect_grpc +from ansys.geometry.core.misc.checks import min_backend_version +from ansys.geometry.core.typing import Real + +if TYPE_CHECKING: # pragma: no cover + from ansys.geometry.core.designer.edge import Edge + from ansys.geometry.core.designer.face import Face + + +class PullTools: + """Provides pull tools for PyAnsys Geometry. + + Parameters + ---------- + grpc_client : GrpcClient + gRPC client to use for the measurement tools. + """ + + @protect_grpc + def __init__(self, grpc_client: GrpcClient): + """Initialize an instance of the ``PullTools`` class.""" + self._grpc_client = grpc_client + self._commands_stub = CommandsStub(self._grpc_client.channel) + + @protect_grpc + @min_backend_version(25, 2, 0) + def chamfer( + self, selection: Union["Edge", List["Edge"], "Face", List["Face"]], distance: Real + ) -> bool: + """Create a chamfer on an edge, or adjust the chamfer of a face. + + Parameters + ---------- + edges_or_faces : Edge | list[Edge] | Face | list[Face] + One or more edges or faces to act on. + distance : Real + Chamfer distance. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + selection: list[Edge | Face] = selection if isinstance(selection, list) else [selection] + + for ef in selection: + ef.body._reset_tessellation_cache() + + result = self._commands_stub.Chamfer( + ChamferRequest(ids=[ef._grpc_id for ef in selection], distance=distance) + ) + + return result.success diff --git a/tests/integration/test_pull_tools.py b/tests/integration/test_pull_tools.py new file mode 100644 index 0000000000..a08c55ea60 --- /dev/null +++ b/tests/integration/test_pull_tools.py @@ -0,0 +1,63 @@ +# 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. +""" "Testing of repair tools.""" + +from pint import Quantity +import pytest + +from ansys.geometry.core.math.point import Point2D +from ansys.geometry.core.misc import UNITS +from ansys.geometry.core.modeler import Modeler +from ansys.geometry.core.sketch.sketch import Sketch + + +def test_chamfer(modeler: Modeler): + """Test chamfer on edge and face.""" + design = modeler.create_design("chamfer") + + body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + assert len(body.faces) == 6 + assert len(body.edges) == 12 + assert body.volume.m == pytest.approx(Quantity(1, UNITS.m**3).m, rel=1e-6, abs=1e-8) + + modeler.pull_tools.chamfer(body.edges[0], 0.1) + assert len(body.faces) == 7 + assert len(body.edges) == 15 + assert body.volume.m == pytest.approx(Quantity(0.995, UNITS.m**3).m, rel=1e-6, abs=1e-8) + + modeler.pull_tools.chamfer(body.faces[-1], 0.5) + assert len(body.faces) == 7 + assert len(body.edges) == 15 + assert body.volume.m == pytest.approx(Quantity(0.875, UNITS.m**3).m, rel=1e-6, abs=1e-8) + + # multiple edges + body2 = design.extrude_sketch("box2", Sketch().box(Point2D([0, 0]), 1, 1), 1) + assert len(body2.faces) == 6 + assert len(body2.edges) == 12 + assert body2.volume.m == pytest.approx(Quantity(1, UNITS.m**3).m, rel=1e-6, abs=1e-8) + + modeler.pull_tools.chamfer(body2.edges, 0.1) + assert len(body2.faces) == 26 + assert len(body2.edges) == 48 + assert body2.volume.m == pytest.approx( + Quantity(0.945333333333333333, UNITS.m**3).m, rel=1e-6, abs=1e-8 + )