Skip to content

Commit 481ef66

Browse files
jacobrkerstetterpyansys-ci-botpre-commit-ci[bot]RobPasMue
authored
feat: named selection functionality (#1768)
Co-authored-by: jkerstet <[email protected]> Co-authored-by: pyansys-ci-bot <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Roberto Pastor Muela <[email protected]>
1 parent 1cf0733 commit 481ef66

File tree

8 files changed

+343
-18
lines changed

8 files changed

+343
-18
lines changed

doc/changelog.d/1768.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
named selection functionality

src/ansys/geometry/core/designer/design.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
from ansys.geometry.core.connection.conversions import (
6060
grpc_frame_to_frame,
6161
grpc_matrix_to_matrix,
62+
grpc_point_to_point3d,
6263
plane_to_grpc_plane,
6364
point3d_to_grpc_point,
6465
)
@@ -78,6 +79,11 @@
7879
from ansys.geometry.core.math.plane import Plane
7980
from ansys.geometry.core.math.point import Point3D
8081
from ansys.geometry.core.math.vector import UnitVector3D, Vector3D
82+
from ansys.geometry.core.misc.auxiliary import (
83+
get_bodies_from_ids,
84+
get_edges_from_ids,
85+
get_faces_from_ids,
86+
)
8187
from ansys.geometry.core.misc.checks import ensure_design_is_active, min_backend_version
8288
from ansys.geometry.core.misc.measurements import DEFAULT_UNITS, Distance
8389
from ansys.geometry.core.misc.options import ImportOptions
@@ -677,8 +683,8 @@ def create_named_selection(
677683
beams=beams,
678684
design_points=design_points,
679685
)
680-
self._named_selections[named_selection.name] = named_selection
681686

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

11421148
# Create NamedSelections
11431149
for ns in response.named_selections:
1144-
new_ns = NamedSelection(ns.name, self._grpc_client, preexisting_id=ns.id)
1150+
result = self._named_selections_stub.Get(EntityIdentifier(id=ns.id))
1151+
1152+
# This works but is slow -- can use improvement for designs with many named selections
1153+
bodies = get_bodies_from_ids(self, [body.id for body in result.bodies])
1154+
faces = get_faces_from_ids(self, [face.id for face in result.faces])
1155+
edges = get_edges_from_ids(self, [edge.id for edge in result.edges])
1156+
1157+
design_points = []
1158+
for dp in result.design_points:
1159+
design_points.append(
1160+
DesignPoint(dp.id, "dp: " + dp.id, grpc_point_to_point3d(dp.points[0]), self)
1161+
)
1162+
1163+
new_ns = NamedSelection(
1164+
ns.name,
1165+
self._grpc_client,
1166+
preexisting_id=ns.id,
1167+
bodies=bodies,
1168+
faces=faces,
1169+
edges=edges,
1170+
beams=[], # BEAM IMPORT NOT SUPPORTED FOR NAMED SELECTIONS
1171+
design_points=design_points,
1172+
)
11451173
self._named_selections[new_ns.name] = new_ns
11461174

11471175
# Create CoordinateSystems

src/ansys/geometry/core/designer/geometry_commands.py

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
from enum import Enum, unique
2525
from typing import TYPE_CHECKING, Union
2626

27+
from beartype import beartype as check_input_types
28+
from pint import Quantity
29+
30+
from ansys.api.dbu.v0.dbumodels_pb2 import EntityIdentifier
2731
from ansys.api.geometry.v0.commands_pb2 import (
2832
ChamferRequest,
2933
CreateCircularPatternRequest,
@@ -37,6 +41,8 @@
3741
FullFilletRequest,
3842
ModifyCircularPatternRequest,
3943
ModifyLinearPatternRequest,
44+
MoveRotateRequest,
45+
MoveTranslateRequest,
4046
OffsetFacesSetRadiusRequest,
4147
PatternRequest,
4248
RenameObjectRequest,
@@ -55,6 +61,7 @@
5561
point3d_to_grpc_point,
5662
unit_vector_to_grpc_direction,
5763
)
64+
from ansys.geometry.core.designer.selection import NamedSelection
5865
from ansys.geometry.core.errors import protect_grpc
5966
from ansys.geometry.core.math.plane import Plane
6067
from ansys.geometry.core.math.point import Point3D
@@ -71,6 +78,7 @@
7178
check_type_all_elements_in_iterable,
7279
min_backend_version,
7380
)
81+
from ansys.geometry.core.misc.measurements import DEFAULT_UNITS, Angle, Distance
7482
from ansys.geometry.core.shapes.curves.line import Line
7583
from ansys.geometry.core.typing import Real
7684

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

12171225
@protect_grpc
1226+
@check_input_types
1227+
@min_backend_version(25, 2, 0)
1228+
def move_translate(
1229+
self,
1230+
selection: NamedSelection,
1231+
direction: UnitVector3D,
1232+
distance: Distance | Quantity | Real,
1233+
) -> bool:
1234+
"""Move a selection by a distance in a direction.
1235+
1236+
Parameters
1237+
----------
1238+
selection : NamedSelection
1239+
Named selection to move.
1240+
direction : UnitVector3D
1241+
Direction to move in.
1242+
distance : Distance | Quantity | Real
1243+
Distance to move. Default units are meters.
1244+
1245+
Returns
1246+
-------
1247+
bool
1248+
``True`` when successful, ``False`` when failed.
1249+
"""
1250+
distance = distance if isinstance(distance, Distance) else Distance(distance)
1251+
translation_magnitude = distance.value.m_as(DEFAULT_UNITS.SERVER_LENGTH)
1252+
1253+
result = self._commands_stub.MoveTranslate(
1254+
MoveTranslateRequest(
1255+
selection=[EntityIdentifier(id=selection.id)],
1256+
direction=unit_vector_to_grpc_direction(direction),
1257+
distance=translation_magnitude,
1258+
)
1259+
)
1260+
1261+
return result.success
1262+
1263+
@protect_grpc
1264+
@check_input_types
1265+
@min_backend_version(25, 2, 0)
1266+
def move_rotate(
1267+
self,
1268+
selection: NamedSelection,
1269+
axis: Line,
1270+
angle: Angle | Quantity | Real,
1271+
) -> dict[str, Union[bool, Real]]:
1272+
"""Rotate a selection by an angle about a given axis.
1273+
1274+
Parameters
1275+
----------
1276+
selection : NamedSelection
1277+
Named selection to move.
1278+
axis : Line
1279+
Direction to move in.
1280+
Angle : Angle | Quantity | Real
1281+
Angle to rotate by. Default units are radians.
1282+
1283+
Returns
1284+
-------
1285+
dict[str, Union[bool, Real]]
1286+
Dictionary containing the useful output from the command result.
1287+
Keys are success, modified_bodies, modified_faces, modified_edges.
1288+
"""
1289+
angle = angle if isinstance(angle, Angle) else Angle(angle)
1290+
rotation_angle = angle.value.m_as(DEFAULT_UNITS.SERVER_ANGLE)
1291+
1292+
response = self._commands_stub.MoveRotate(
1293+
MoveRotateRequest(
1294+
selection=[EntityIdentifier(id=selection.id)],
1295+
axis=line_to_grpc_line(axis),
1296+
angle=rotation_angle,
1297+
)
1298+
)
1299+
1300+
result = {}
1301+
result["success"] = response.success
1302+
result["modified_bodies"] = response.modified_bodies
1303+
result["modified_faces"] = response.modified_faces
1304+
result["modified_edges"] = response.modified_edges
1305+
1306+
return result
1307+
1308+
@protect_grpc
1309+
@check_input_types
12181310
@min_backend_version(25, 2, 0)
12191311
def offset_faces_set_radius(
12201312
self,
12211313
faces: Union["Face", list["Face"]],
1222-
radius: Real,
1314+
radius: Distance | Quantity | Real,
12231315
copy: bool = False,
12241316
offset_mode: OffsetMode = OffsetMode.IGNORE_RELATIONSHIPS,
12251317
extrude_type: ExtrudeType = ExtrudeType.FORCE_INDEPENDENT,
@@ -1230,7 +1322,7 @@ def offset_faces_set_radius(
12301322
----------
12311323
faces : Face | list[Face]
12321324
Faces to offset.
1233-
radius : Real
1325+
radius : Distance | Quantity | Real
12341326
Radius of the offset.
12351327
copy : bool, default: False
12361328
Copy the face and move it instead of offsetting the original face if ``True``.
@@ -1247,17 +1339,18 @@ def offset_faces_set_radius(
12471339
from ansys.geometry.core.designer.face import Face
12481340

12491341
faces: list[Face] = faces if isinstance(faces, list) else [faces]
1250-
12511342
check_type_all_elements_in_iterable(faces, Face)
1252-
check_is_float_int(radius, "radius")
12531343

12541344
for face in faces:
12551345
face.body._reset_tessellation_cache()
12561346

1347+
radius = radius if isinstance(radius, Distance) else Distance(radius)
1348+
radius_magnitude = radius.value.m_as(DEFAULT_UNITS.SERVER_LENGTH)
1349+
12571350
result = self._commands_stub.OffsetFacesSetRadius(
12581351
OffsetFacesSetRadiusRequest(
12591352
faces=[face._grpc_id for face in faces],
1260-
radius=radius,
1353+
radius=radius_magnitude,
12611354
copy=copy,
12621355
offset_mode=offset_mode.value,
12631356
extrude_type=extrude_type.value,

src/ansys/geometry/core/designer/selection.py

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,11 @@ def __init__(
7474
preexisting_id: str | None = None,
7575
):
7676
"""Initialize the ``NamedSelection`` class."""
77+
self._name = name
7778
self._grpc_client = grpc_client
78-
self._named_selections_stub = NamedSelectionsStub(grpc_client.channel)
79-
80-
if preexisting_id:
81-
self._id = preexisting_id
82-
self._name = name
83-
return
84-
85-
# All ids should be unique - no duplicated values
86-
ids = set()
79+
self._named_selections_stub = NamedSelectionsStub(self._grpc_client.channel)
8780

81+
# Create empty arrays if there are none of a type
8882
if bodies is None:
8983
bodies = []
9084
if faces is None:
@@ -96,6 +90,20 @@ def __init__(
9690
if design_points is None:
9791
design_points = []
9892

93+
# Instantiate
94+
self._bodies = bodies
95+
self._faces = faces
96+
self._edges = edges
97+
self._beams = beams
98+
self._design_points = design_points
99+
100+
if preexisting_id:
101+
self._id = preexisting_id
102+
return
103+
104+
# All ids should be unique - no duplicated values
105+
ids = set()
106+
99107
# Loop over bodies, faces and edges
100108
[ids.add(body.id) for body in bodies]
101109
[ids.add(face.id) for face in faces]
@@ -107,7 +115,6 @@ def __init__(
107115
self._grpc_client.log.debug("Requesting creation of named selection.")
108116
new_named_selection = self._named_selections_stub.Create(named_selection_request)
109117
self._id = new_named_selection.id
110-
self._name = new_named_selection.name
111118

112119
@property
113120
def id(self) -> str:
@@ -118,3 +125,40 @@ def id(self) -> str:
118125
def name(self) -> str:
119126
"""Name of the named selection."""
120127
return self._name
128+
129+
@property
130+
def bodies(self) -> list[Body]:
131+
"""All bodies in the named selection."""
132+
return self._bodies
133+
134+
@property
135+
def faces(self) -> list[Face]:
136+
"""All faces in the named selection."""
137+
return self._faces
138+
139+
@property
140+
def edges(self) -> list[Edge]:
141+
"""All edges in the named selection."""
142+
return self._edges
143+
144+
@property
145+
def beams(self) -> list[Beam]:
146+
"""All beams in the named selection."""
147+
return self._beams
148+
149+
@property
150+
def design_points(self) -> list[DesignPoint]:
151+
"""All design points in the named selection."""
152+
return self._design_points
153+
154+
def __repr__(self) -> str:
155+
"""Represent the ``NamedSelection`` as a string."""
156+
lines = [f"ansys.geometry.core.designer.selection.NamedSelection {hex(id(self))}"]
157+
lines.append(f" Name : {self._name}")
158+
lines.append(f" Id : {self._id}")
159+
lines.append(f" N Bodies : {len(self.bodies)}")
160+
lines.append(f" N Faces : {len(self.faces)}")
161+
lines.append(f" N Edges : {len(self.edges)}")
162+
lines.append(f" N Beams : {len(self.beams)}")
163+
lines.append(f" N Design Points : {len(self.design_points)}")
164+
return "\n".join(lines)
5.26 MB
Binary file not shown.

tests/integration/test_design.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,37 @@ def test_named_selections(modeler: Modeler):
455455
assert len(design.named_selections) == 3
456456

457457

458+
def test_named_selection_contents(modeler: Modeler):
459+
"""Test for verifying the correct contents of a ``NamedSelection``."""
460+
# Create your design on the server side
461+
design = modeler.create_design("NamedSelection_Test")
462+
463+
# Create objects to add to the named selection
464+
box = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1)
465+
box_2 = design.extrude_sketch("box_2", Sketch().box(Point2D([0, 0]), 5, 5), 5)
466+
face = box_2.faces[2]
467+
edge = box_2.edges[0]
468+
469+
# Create the NamedSelection
470+
ns = design.create_named_selection(
471+
"MyNamedSelection", bodies=[box, box_2], faces=[face], edges=[edge]
472+
)
473+
474+
print(ns.bodies)
475+
# Check that the named selection has everything
476+
assert len(ns.bodies) == 2
477+
assert np.isin([box.id, box_2.id], [body.id for body in ns.bodies]).all()
478+
479+
assert len(ns.faces) == 1
480+
assert ns.faces[0].id == face.id
481+
482+
assert len(ns.edges) == 1
483+
assert ns.edges[0].id == edge.id
484+
485+
assert len(ns.beams) == 0
486+
assert len(ns.design_points) == 0
487+
488+
458489
def test_add_component_with_instance_name(modeler: Modeler):
459490
design = modeler.create_design("DesignHierarchyExample")
460491
circle_sketch = Sketch()
@@ -1519,7 +1550,7 @@ def test_named_selections_design_points(modeler: Modeler):
15191550
design points.
15201551
"""
15211552
# Create your design on the server side
1522-
design = modeler.create_design("NamedSelectionBeams_Test")
1553+
design = modeler.create_design("NamedSelectionDesignPoints_Test")
15231554

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

0 commit comments

Comments
 (0)