Skip to content

feat: named selection functionality #1768

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
bd9e051
added properties
Feb 18, 2025
da25a31
tested named selection import
Feb 18, 2025
ef8b880
able to query named_selections for underlying members
Feb 19, 2025
2b9b695
implemented named selection from existing design
Feb 21, 2025
302cda6
Merge branch 'main' into feat/named_selection_functionality
jacobrkerstetter Feb 21, 2025
9848969
chore: adding changelog file 1768.added.md [dependabot-skip]
pyansys-ci-bot Feb 21, 2025
e285344
chore: auto fixes from pre-commit hooks
pre-commit-ci[bot] Feb 21, 2025
ba51faa
change repeated selection field to single named selection
Feb 24, 2025
2224804
Merge branch 'main' into feat/named_selection_functionality
jacobrkerstetter Feb 24, 2025
a8ff440
- fixed move_rotate and move_translate proto naming
Feb 25, 2025
a2cc30f
chore: auto fixes from pre-commit hooks
pre-commit-ci[bot] Feb 25, 2025
04af7d0
Merge branch 'main' into feat/named_selection_functionality
jacobrkerstetter Feb 25, 2025
6d9b804
edited test because order of id's is not constant
Feb 25, 2025
3768b9c
Merge branch 'feat/named_selection_functionality' of https://github.c…
Feb 25, 2025
18b358a
- removed a few NS from the test file for speed of testing
Feb 25, 2025
ff262a9
Merge branch 'main' into feat/named_selection_functionality
jacobrkerstetter Feb 25, 2025
e9647ff
chore: auto fixes from pre-commit hooks
pre-commit-ci[bot] Feb 25, 2025
cb3ecc7
precommit fix
Feb 25, 2025
999cc3f
merge error fix
Feb 25, 2025
00e433d
accepting distance object for offset_faces_set_radius
Feb 25, 2025
ebbe4cb
updated docstring
Feb 25, 2025
ed0383a
chore: auto fixes from pre-commit hooks
pre-commit-ci[bot] Feb 25, 2025
f59a2c7
accept Beam from NamedSelection constructor
Feb 26, 2025
a0a985f
Merge branch 'feat/named_selection_functionality' of https://github.c…
Feb 26, 2025
c0216a1
enhanced result from move rotate to include modified bodies, faces, e…
Feb 26, 2025
afd1e8a
Merge branch 'main' into feat/named_selection_functionality
RobPasMue Feb 27, 2025
1558093
- edited model
Feb 27, 2025
adf96d6
Merge branch 'feat/named_selection_functionality' of https://github.c…
Feb 27, 2025
fc53f96
chore: auto fixes from pre-commit hooks
pre-commit-ci[bot] Feb 27, 2025
b8e838f
Merge branch 'main' into feat/named_selection_functionality
jacobrkerstetter Feb 27, 2025
6f787c9
removed beams from reading existing design (not supported yet)
Feb 28, 2025
d899975
chore: auto fixes from pre-commit hooks
pre-commit-ci[bot] Feb 28, 2025
41ad431
Merge branch 'main' into feat/named_selection_functionality
jacobrkerstetter Feb 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/1768.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
named selection functionality
32 changes: 30 additions & 2 deletions src/ansys/geometry/core/designer/design.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
from ansys.geometry.core.connection.conversions import (
grpc_frame_to_frame,
grpc_matrix_to_matrix,
grpc_point_to_point3d,
plane_to_grpc_plane,
point3d_to_grpc_point,
)
Expand All @@ -78,6 +79,11 @@
from ansys.geometry.core.math.plane import Plane
from ansys.geometry.core.math.point import Point3D
from ansys.geometry.core.math.vector import UnitVector3D, Vector3D
from ansys.geometry.core.misc.auxiliary import (
get_bodies_from_ids,
get_edges_from_ids,
get_faces_from_ids,
)
from ansys.geometry.core.misc.checks import ensure_design_is_active, min_backend_version
from ansys.geometry.core.misc.measurements import DEFAULT_UNITS, Distance
from ansys.geometry.core.misc.options import ImportOptions
Expand Down Expand Up @@ -677,8 +683,8 @@ def create_named_selection(
beams=beams,
design_points=design_points,
)
self._named_selections[named_selection.name] = named_selection

self._named_selections[named_selection.name] = named_selection
self._grpc_client.log.debug(
f"Named selection {named_selection.name} is successfully created."
)
Expand Down Expand Up @@ -1141,7 +1147,29 @@ def __read_existing_design(self) -> None:

# Create NamedSelections
for ns in response.named_selections:
new_ns = NamedSelection(ns.name, self._grpc_client, preexisting_id=ns.id)
result = self._named_selections_stub.Get(EntityIdentifier(id=ns.id))

# This works but is slow -- can use improvement for designs with many named selections
bodies = get_bodies_from_ids(self, [body.id for body in result.bodies])
faces = get_faces_from_ids(self, [face.id for face in result.faces])
edges = get_edges_from_ids(self, [edge.id for edge in result.edges])

design_points = []
for dp in result.design_points:
design_points.append(
DesignPoint(dp.id, "dp: " + dp.id, grpc_point_to_point3d(dp.points[0]), self)
)

new_ns = NamedSelection(
ns.name,
self._grpc_client,
preexisting_id=ns.id,
bodies=bodies,
faces=faces,
edges=edges,
beams=[], # BEAM IMPORT NOT SUPPORTED FOR NAMED SELECTIONS
design_points=design_points,
)
self._named_selections[new_ns.name] = new_ns

# Create CoordinateSystems
Expand Down
103 changes: 98 additions & 5 deletions src/ansys/geometry/core/designer/geometry_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
from enum import Enum, unique
from typing import TYPE_CHECKING, Union

from beartype import beartype as check_input_types
from pint import Quantity

from ansys.api.dbu.v0.dbumodels_pb2 import EntityIdentifier
from ansys.api.geometry.v0.commands_pb2 import (
ChamferRequest,
CreateCircularPatternRequest,
Expand All @@ -37,6 +41,8 @@
FullFilletRequest,
ModifyCircularPatternRequest,
ModifyLinearPatternRequest,
MoveRotateRequest,
MoveTranslateRequest,
OffsetFacesSetRadiusRequest,
PatternRequest,
RenameObjectRequest,
Expand All @@ -55,6 +61,7 @@
point3d_to_grpc_point,
unit_vector_to_grpc_direction,
)
from ansys.geometry.core.designer.selection import NamedSelection
from ansys.geometry.core.errors import protect_grpc
from ansys.geometry.core.math.plane import Plane
from ansys.geometry.core.math.point import Point3D
Expand All @@ -71,6 +78,7 @@
check_type_all_elements_in_iterable,
min_backend_version,
)
from ansys.geometry.core.misc.measurements import DEFAULT_UNITS, Angle, Distance
from ansys.geometry.core.shapes.curves.line import Line
from ansys.geometry.core.typing import Real

Expand Down Expand Up @@ -1215,11 +1223,95 @@ def get_round_info(self, face: "Face") -> tuple[bool, Real]:
return (result.along_u, result.radius)

@protect_grpc
@check_input_types
@min_backend_version(25, 2, 0)
def move_translate(
self,
selection: NamedSelection,
direction: UnitVector3D,
distance: Distance | Quantity | Real,
) -> bool:
"""Move a selection by a distance in a direction.

Parameters
----------
selection : NamedSelection
Named selection to move.
direction : UnitVector3D
Direction to move in.
distance : Distance | Quantity | Real
Distance to move. Default units are meters.

Returns
-------
bool
``True`` when successful, ``False`` when failed.
"""
distance = distance if isinstance(distance, Distance) else Distance(distance)
translation_magnitude = distance.value.m_as(DEFAULT_UNITS.SERVER_LENGTH)

result = self._commands_stub.MoveTranslate(
MoveTranslateRequest(
selection=[EntityIdentifier(id=selection.id)],
direction=unit_vector_to_grpc_direction(direction),
distance=translation_magnitude,
)
)

return result.success

@protect_grpc
@check_input_types
@min_backend_version(25, 2, 0)
def move_rotate(
self,
selection: NamedSelection,
axis: Line,
angle: Angle | Quantity | Real,
) -> dict[str, Union[bool, Real]]:
"""Rotate a selection by an angle about a given axis.

Parameters
----------
selection : NamedSelection
Named selection to move.
axis : Line
Direction to move in.
Angle : Angle | Quantity | Real
Angle to rotate by. Default units are radians.

Returns
-------
dict[str, Union[bool, Real]]
Dictionary containing the useful output from the command result.
Keys are success, modified_bodies, modified_faces, modified_edges.
"""
angle = angle if isinstance(angle, Angle) else Angle(angle)
rotation_angle = angle.value.m_as(DEFAULT_UNITS.SERVER_ANGLE)

response = self._commands_stub.MoveRotate(
MoveRotateRequest(
selection=[EntityIdentifier(id=selection.id)],
axis=line_to_grpc_line(axis),
angle=rotation_angle,
)
)

result = {}
result["success"] = response.success
result["modified_bodies"] = response.modified_bodies
result["modified_faces"] = response.modified_faces
result["modified_edges"] = response.modified_edges

return result

@protect_grpc
@check_input_types
@min_backend_version(25, 2, 0)
def offset_faces_set_radius(
self,
faces: Union["Face", list["Face"]],
radius: Real,
radius: Distance | Quantity | Real,
copy: bool = False,
offset_mode: OffsetMode = OffsetMode.IGNORE_RELATIONSHIPS,
extrude_type: ExtrudeType = ExtrudeType.FORCE_INDEPENDENT,
Expand All @@ -1230,7 +1322,7 @@ def offset_faces_set_radius(
----------
faces : Face | list[Face]
Faces to offset.
radius : Real
radius : Distance | Quantity | Real
Radius of the offset.
copy : bool, default: False
Copy the face and move it instead of offsetting the original face if ``True``.
Expand All @@ -1247,17 +1339,18 @@ def offset_faces_set_radius(
from ansys.geometry.core.designer.face import Face

faces: list[Face] = faces if isinstance(faces, list) else [faces]

check_type_all_elements_in_iterable(faces, Face)
check_is_float_int(radius, "radius")

for face in faces:
face.body._reset_tessellation_cache()

radius = radius if isinstance(radius, Distance) else Distance(radius)
radius_magnitude = radius.value.m_as(DEFAULT_UNITS.SERVER_LENGTH)

result = self._commands_stub.OffsetFacesSetRadius(
OffsetFacesSetRadiusRequest(
faces=[face._grpc_id for face in faces],
radius=radius,
radius=radius_magnitude,
copy=copy,
offset_mode=offset_mode.value,
extrude_type=extrude_type.value,
Expand Down
64 changes: 54 additions & 10 deletions src/ansys/geometry/core/designer/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,11 @@ def __init__(
preexisting_id: str | None = None,
):
"""Initialize the ``NamedSelection`` class."""
self._name = name
self._grpc_client = grpc_client
self._named_selections_stub = NamedSelectionsStub(grpc_client.channel)

if preexisting_id:
self._id = preexisting_id
self._name = name
return

# All ids should be unique - no duplicated values
ids = set()
self._named_selections_stub = NamedSelectionsStub(self._grpc_client.channel)

# Create empty arrays if there are none of a type
if bodies is None:
bodies = []
if faces is None:
Expand All @@ -96,6 +90,20 @@ def __init__(
if design_points is None:
design_points = []

# Instantiate
self._bodies = bodies
self._faces = faces
self._edges = edges
self._beams = beams
self._design_points = design_points

if preexisting_id:
self._id = preexisting_id
return

# All ids should be unique - no duplicated values
ids = set()

# Loop over bodies, faces and edges
[ids.add(body.id) for body in bodies]
[ids.add(face.id) for face in faces]
Expand All @@ -107,7 +115,6 @@ def __init__(
self._grpc_client.log.debug("Requesting creation of named selection.")
new_named_selection = self._named_selections_stub.Create(named_selection_request)
self._id = new_named_selection.id
self._name = new_named_selection.name

@property
def id(self) -> str:
Expand All @@ -118,3 +125,40 @@ def id(self) -> str:
def name(self) -> str:
"""Name of the named selection."""
return self._name

@property
def bodies(self) -> list[Body]:
"""All bodies in the named selection."""
return self._bodies

@property
def faces(self) -> list[Face]:
"""All faces in the named selection."""
return self._faces

@property
def edges(self) -> list[Edge]:
"""All edges in the named selection."""
return self._edges

@property
def beams(self) -> list[Beam]:
"""All beams in the named selection."""
return self._beams

@property
def design_points(self) -> list[DesignPoint]:
"""All design points in the named selection."""
return self._design_points

def __repr__(self) -> str:
"""Represent the ``NamedSelection`` as a string."""
lines = [f"ansys.geometry.core.designer.selection.NamedSelection {hex(id(self))}"]
lines.append(f" Name : {self._name}")
lines.append(f" Id : {self._id}")
lines.append(f" N Bodies : {len(self.bodies)}")
lines.append(f" N Faces : {len(self.faces)}")
lines.append(f" N Edges : {len(self.edges)}")
lines.append(f" N Beams : {len(self.beams)}")
lines.append(f" N Design Points : {len(self.design_points)}")
return "\n".join(lines)
Binary file not shown.
33 changes: 32 additions & 1 deletion tests/integration/test_design.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,37 @@ def test_named_selections(modeler: Modeler):
assert len(design.named_selections) == 3


def test_named_selection_contents(modeler: Modeler):
"""Test for verifying the correct contents of a ``NamedSelection``."""
# Create your design on the server side
design = modeler.create_design("NamedSelection_Test")

# Create objects to add to the named selection
box = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1)
box_2 = design.extrude_sketch("box_2", Sketch().box(Point2D([0, 0]), 5, 5), 5)
face = box_2.faces[2]
edge = box_2.edges[0]

# Create the NamedSelection
ns = design.create_named_selection(
"MyNamedSelection", bodies=[box, box_2], faces=[face], edges=[edge]
)

print(ns.bodies)
# Check that the named selection has everything
assert len(ns.bodies) == 2
assert np.isin([box.id, box_2.id], [body.id for body in ns.bodies]).all()

assert len(ns.faces) == 1
assert ns.faces[0].id == face.id

assert len(ns.edges) == 1
assert ns.edges[0].id == edge.id

assert len(ns.beams) == 0
assert len(ns.design_points) == 0


def test_add_component_with_instance_name(modeler: Modeler):
design = modeler.create_design("DesignHierarchyExample")
circle_sketch = Sketch()
Expand Down Expand Up @@ -1519,7 +1550,7 @@ def test_named_selections_design_points(modeler: Modeler):
design points.
"""
# Create your design on the server side
design = modeler.create_design("NamedSelectionBeams_Test")
design = modeler.create_design("NamedSelectionDesignPoints_Test")

# Test creating a named selection out of design_points
point_set_1 = Point3D([10, 10, 0], UNITS.m)
Expand Down
Loading
Loading