Skip to content

Commit

Permalink
feat: implement cut operation in extrude sketch (#1510)
Browse files Browse the repository at this point in the history
Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com>
Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
4 people authored Oct 30, 2024
1 parent 8b919dd commit d25808d
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 5 deletions.
1 change: 1 addition & 0 deletions doc/changelog.d/1510.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
implement cut operation in extrude sketch
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ def intersphinx_pyansys_geometry(switcher_version: str):
"examples/02_sketching/advanced_sketching_gears": "_static/thumbnails/advanced_sketching_gears.png", # noqa: E501
"examples/03_modeling/add_design_material": "_static/thumbnails/add_design_material.png",
"examples/03_modeling/plate_with_hole": "_static/thumbnails/plate_with_hole.png",
"examples/03_modeling/cut_operation_on_extrude": "_static/thumbnails/cut_operation_on_extrude.png", # noqa: E501
"examples/03_modeling/tessellation_usage": "_static/thumbnails/tessellation_usage.png",
"examples/03_modeling/design_organization": "_static/thumbnails/design_organization.png",
"examples/03_modeling/boolean_operations": "_static/thumbnails/boolean_operations.png",
Expand Down
1 change: 1 addition & 0 deletions doc/source/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ These examples demonstrate service-based modeling operations.

examples/03_modeling/add_design_material.mystnb
examples/03_modeling/plate_with_hole.mystnb
examples/03_modeling/cut_operation_on_extrude.mystnb
examples/03_modeling/tessellation_usage.mystnb
examples/03_modeling/design_organization.mystnb
examples/03_modeling/boolean_operations.mystnb
Expand Down
100 changes: 100 additions & 0 deletions doc/source/examples/03_modeling/cut_operation_on_extrude.mystnb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
jupytext:
text_representation:
extension: .mystnb
format_name: myst
format_version: 0.13
jupytext_version: 1.14.1
kernelspec:
display_name: Python 3 (ipykernel)
language: python
name: python3
---

# Modeling: Extruded plate with cut operations

As seen in the [Rectangular plate with multiple bodies](./plate_with_hole.mystnb) example,
you can create a complex sketch with holes and extrude it to create a body. However, you can also
perform cut operations on the extruded body to achieve similar results.

+++

## Perform required imports

Perform the required imports.

```{code-cell} ipython3
from pint import Quantity

from ansys.geometry.core import launch_modeler
from ansys.geometry.core.math import Plane, Point3D, Point2D
from ansys.geometry.core.misc import UNITS
from ansys.geometry.core.sketch import Sketch
```

## Define sketch profile without holes

Create a sketch profile for the proposed design. The sketch is the same as the
[Rectangular plate with multiple bodies](./plate_with_hole.mystnb) example, but without the holes.

These holes are created by performing cut operations on the extruded body in the next steps.


```{code-cell} ipython3
sketch = Sketch()
(sketch.segment(Point2D([-4, 5], unit=UNITS.m), Point2D([4, 5], unit=UNITS.m))
.segment_to_point(Point2D([4, -5], unit=UNITS.m))
.segment_to_point(Point2D([-4, -5], unit=UNITS.m))
.segment_to_point(Point2D([-4, 5], unit=UNITS.m))
.box(Point2D([0,0], unit=UNITS.m), Quantity(3, UNITS.m), Quantity(3, UNITS.m))
)

modeler = launch_modeler()
design = modeler.create_design("ExtrudedPlateNoHoles")
body = design.extrude_sketch(f"PlateLayer", sketch, Quantity(2, UNITS.m))

design.plot()
```

## Define sketch profile for holes

Create a sketch profile for the holes in the proposed design. The holes are created by
sketching circles at the four corners of the plate. First create a reference sketch
for all the circles. This sketch is translated to the four corners of the plate.

```{code-cell} ipython3
sketch_hole = Sketch()
sketch_hole.circle(Point2D([0, 0], unit=UNITS.m), Quantity(0.5, UNITS.m))

hole_centers = [
Plane(Point3D([3, 4, 0], unit=UNITS.m)),
Plane(Point3D([-3, 4, 0], unit=UNITS.m)),
Plane(Point3D([-3, -4, 0], unit=UNITS.m)),
Plane(Point3D([3, -4, 0], unit=UNITS.m)),
]
```

## Perform cut operations on the extruded body

Perform cut operations on the extruded body to create holes at the four corners of the plate.

```{code-cell} ipython3
for center in hole_centers:
sketch_hole.plane = center
design.extrude_sketch(
name= f"H_{center.origin.x}_{center.origin.y}",
sketch=sketch_hole,
distance=Quantity(2, UNITS.m),
cut=True,
)

design.plot()
```

## Close the modeler

Close the modeler to free up resources and release the connection.

```{code-cell} ipython3
modeler.close()
```
2 changes: 1 addition & 1 deletion doc/source/examples/03_modeling/plate_with_hole.mystnb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ kernelspec:
You can create multiple bodies from a single sketch by extruding the same sketch in different planes.

The sketch is designed as an effective *functional-style* API with all operations receiving 2D configurations.
For more information, see the :ref:`Sketch <ref_sketch>` subpackage.
For more information, see the [Sketch](../../user_guide/shapes.rst) user guide.

In this example, a box is located in the center of the plate, with the default origin of a sketch plane
(origin at ``(0, 0, 0)``). Four holes of equal radius are sketched at the corners of the plate.
Expand Down
45 changes: 42 additions & 3 deletions src/ansys/geometry/core/designer/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
unit_vector_to_grpc_direction,
)
from ansys.geometry.core.designer.beam import Beam, BeamProfile
from ansys.geometry.core.designer.body import Body, MasterBody
from ansys.geometry.core.designer.body import Body, CollisionType, MasterBody
from ansys.geometry.core.designer.coordinate_system import CoordinateSystem
from ansys.geometry.core.designer.designpoint import DesignPoint
from ansys.geometry.core.designer.face import Face
Expand Down Expand Up @@ -324,6 +324,20 @@ def __create_children(self, template: "Component") -> None:
)
self.components.append(new)

def get_all_bodies(self) -> list[Body]:
"""Get all bodies in the component hierarchy.
Returns
-------
list[Body]
List of all bodies in the component hierarchy.
"""
bodies = []
for comp in self.components:
bodies.extend(comp.get_all_bodies())
bodies.extend(self.bodies)
return bodies

def get_world_transform(self) -> Matrix44:
"""Get the full transformation matrix of the component in world space.
Expand Down Expand Up @@ -468,7 +482,8 @@ def extrude_sketch(
sketch: Sketch,
distance: Quantity | Distance | Real,
direction: ExtrusionDirection | str = ExtrusionDirection.POSITIVE,
) -> Body:
cut: bool = False,
) -> Body | None:
"""Create a solid body by extruding the sketch profile a distance.
Notes
Expand All @@ -487,11 +502,16 @@ def extrude_sketch(
Direction for extruding the solid body.
The default is to extrude in the positive normal direction of the sketch.
Options are "+" and "-" as a string, or the enum values.
cut : bool, default: False
Whether to cut the extrusion from the existing component.
By default, the extrusion is added to the existing component.
Returns
-------
Body
Extruded body from the given sketch.
None
If the cut parameter is ``True``, the function returns ``None``.
"""
# Sanity checks on inputs
distance = distance if isinstance(distance, Distance) else Distance(distance)
Expand All @@ -516,7 +536,26 @@ def extrude_sketch(
tb = MasterBody(response.master_id, name, self._grpc_client, is_surface=False)
self._master_component.part.bodies.append(tb)
self._clear_cached_bodies()
return Body(response.id, response.name, self, tb)

created_body = Body(response.id, response.name, self, tb)
if not cut:
return created_body
else:
# If cut is True, subtract the created body from all existing bodies
# in the component...
for existing_body in self.get_all_bodies():
# Skip the created body
if existing_body.id == created_body.id:
continue
# Check for collision
if existing_body.get_collision(created_body) != CollisionType.NONE:
existing_body.subtract(created_body, keep_other=True)

# Finally, delete the created body
self.delete_body(created_body)

# And obviously return None... since no body is created
return None

@min_backend_version(24, 2, 0)
@protect_grpc
Expand Down
4 changes: 4 additions & 0 deletions src/ansys/geometry/core/misc/measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ def __init__(self, value: Real | Quantity, unit: Unit, dimensions: Unit):
# Store the value
self._value = self._base_units_magnitude(value)

def __repr__(self):
"""Representation of the ``Measurement`` class."""
return f"{self.value}"

@property
def value(self) -> Quantity:
"""Value of the measurement."""
Expand Down
77 changes: 76 additions & 1 deletion tests/integration/test_design.py
Original file line number Diff line number Diff line change
Expand Up @@ -1991,7 +1991,6 @@ def test_get_active_design(modeler: Modeler):

def test_get_collision(modeler: Modeler):
"""Test the collision state between two bodies."""
skip_if_linux(modeler, test_get_collision.__name__, "get_collision") # Skip test on Linux
design = modeler.open_file(FILES_DIR / "MixingTank.scdocx")
body1 = design.bodies[0]
body2 = design.bodies[1]
Expand Down Expand Up @@ -2784,3 +2783,79 @@ def test_cached_bodies(modeler: Modeler):
for body1, body3 in zip(my_bodies, my_bodies_3):
assert body1 is not body3
assert id(body1) != id(body3)


def test_extrude_sketch_with_cut_request(modeler: Modeler):
"""Test the cut argument when performing a sketch extrusion.
This method mimics a cut operation.
Behind the scenes, a subtraction operation is performed on the bodies. After extruding the
sketch, the resulting body should be a cut body.
"""
# Define a sketch
origin = Point3D([0, 0, 10])
plane = Plane(origin, direction_x=[1, 0, 0], direction_y=[0, 1, 0])

# Create a sketch
sketch_box = Sketch(plane)
sketch_box.box(Point2D([20, 20]), 30 * UNITS.m, 30 * UNITS.m)

sketch_cylinder = Sketch(plane)
sketch_cylinder.circle(Point2D([20, 20]), 5 * UNITS.m)

# Create a design
design = modeler.create_design("ExtrudeSketchWithCut")

box_body = design.extrude_sketch(
name="BoxBody", sketch=sketch_box, distance=Distance(30, unit=UNITS.m)
)
volume_box = box_body.volume

design.extrude_sketch(
name="CylinderBody", sketch=sketch_cylinder, distance=Distance(60, unit=UNITS.m), cut=True
)

# Verify there is only one body
assert len(design.bodies) == 1

# Verify the volume of the resulting body is less than the volume of the box
assert design.bodies[0].volume < volume_box


def test_extrude_sketch_with_cut_request_no_collision(modeler: Modeler):
"""Test the cut argument when performing a sketch extrusion (with no collision).
This method mimics an unsuccessful cut operation.
The sketch extrusion should not result in a cut body since there is no collision between the
original body and the extruded body.
"""
# Define a sketch
origin = Point3D([0, 0, 10])
plane = Plane(origin, direction_x=[1, 0, 0], direction_y=[0, 1, 0])

# Create a sketch
sketch_box = Sketch(plane)
sketch_box.box(Point2D([20, 20]), 30 * UNITS.m, 30 * UNITS.m)

sketch_cylinder = Sketch(plane)
sketch_cylinder.circle(Point2D([100, 100]), 5 * UNITS.m)

# Create a design
design = modeler.create_design("ExtrudeSketchWithCutNoCollision")

box_body = design.extrude_sketch(
name="BoxBody", sketch=sketch_box, distance=Distance(30, unit=UNITS.m)
)
volume_box = box_body.volume

design.extrude_sketch(
name="CylinderBody", sketch=sketch_cylinder, distance=Distance(60, unit=UNITS.m), cut=True
)

# Verify there is only one body... the cut operation should delete it
assert len(design.bodies) == 1

# Verify the volume of the resulting body is exactly the same
assert design.bodies[0].volume == volume_box

0 comments on commit d25808d

Please sign in to comment.