Skip to content

Commit ca0f79a

Browse files
allow subtraction between geometry and face group
1 parent c296a13 commit ca0f79a

File tree

1 file changed

+187
-19
lines changed

1 file changed

+187
-19
lines changed

flow360/component/geometry.py

Lines changed: 187 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,102 @@ def submit(self, description="", progress_callback=None, run_async=False) -> Geo
232232
return Geometry.from_cloud(info.id)
233233

234234

235+
class FaceSelection:
236+
"""
237+
Represents a selection of faces that can be used in boolean operations.
238+
239+
This class supports subtraction operations to create complex face selections,
240+
such as "all geometry faces minus wing faces minus tail faces".
241+
"""
242+
243+
def __init__(self, geometry: "Geometry", include_all: bool = False,
244+
face_groups_to_subtract: Optional[List["FaceGroup"]] = None):
245+
"""
246+
Initialize a FaceSelection.
247+
248+
Parameters
249+
----------
250+
geometry : Geometry
251+
The parent Geometry object
252+
include_all : bool
253+
If True, starts with all faces in the geometry
254+
face_groups_to_subtract : Optional[List[FaceGroup]]
255+
List of face groups whose faces should be subtracted
256+
"""
257+
self._geometry = geometry
258+
self._include_all = include_all
259+
self._face_groups_to_subtract = face_groups_to_subtract or []
260+
261+
def __sub__(self, other: "FaceGroup") -> "FaceSelection":
262+
"""
263+
Subtract a face group from this selection.
264+
265+
Parameters
266+
----------
267+
other : FaceGroup
268+
The face group to subtract
269+
270+
Returns
271+
-------
272+
FaceSelection
273+
A new FaceSelection with the subtraction applied
274+
"""
275+
if not isinstance(other, FaceGroup):
276+
raise Flow360ValueError(
277+
f"Can only subtract FaceGroup from FaceSelection, got {type(other)}"
278+
)
279+
280+
# Create a new FaceSelection with the additional subtraction
281+
new_subtractions = self._face_groups_to_subtract + [other]
282+
return FaceSelection(
283+
self._geometry,
284+
include_all=self._include_all,
285+
face_groups_to_subtract=new_subtractions
286+
)
287+
288+
def get_selected_faces(self) -> List[TreeNode]:
289+
"""
290+
Execute the selection and return the list of face nodes.
291+
292+
Returns
293+
-------
294+
List[TreeNode]
295+
List of face nodes after applying all operations
296+
"""
297+
if self._geometry._tree is None:
298+
raise Flow360ValueError(
299+
"Geometry tree not loaded. Call load_geometry_tree() first"
300+
)
301+
302+
if self._include_all:
303+
# Start with all faces
304+
all_faces = self._geometry._tree.all_faces
305+
selected_face_uuids = {face.uuid for face in all_faces if face.uuid}
306+
else:
307+
# Start with empty set
308+
selected_face_uuids = set()
309+
310+
# Subtract face groups
311+
for face_group in self._face_groups_to_subtract:
312+
face_uuids_to_remove = {face.uuid for face in face_group.faces if face.uuid}
313+
selected_face_uuids -= face_uuids_to_remove
314+
315+
# Convert UUIDs back to face nodes
316+
uuid_to_face = self._geometry._tree.uuid_to_face
317+
result_faces = [uuid_to_face[uuid] for uuid in selected_face_uuids if uuid in uuid_to_face]
318+
319+
return result_faces
320+
321+
def __repr__(self):
322+
parts = []
323+
if self._include_all:
324+
parts.append("All geometry")
325+
if self._face_groups_to_subtract:
326+
subtracted = ", ".join([fg.name for fg in self._face_groups_to_subtract])
327+
parts.append(f"minus [{subtracted}]")
328+
return f"FaceSelection({' '.join(parts)})"
329+
330+
235331
class FaceGroup:
236332
"""
237333
Represents a face group that can be incrementally built by adding nodes.
@@ -273,7 +369,7 @@ def face_count(self) -> int:
273369
return len(self._faces)
274370

275371
def add(
276-
self, selection: Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch]
372+
self, selection: Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch, FaceSelection]
277373
) -> "FaceGroup":
278374
"""
279375
Add more nodes to this face group.
@@ -282,8 +378,9 @@ def add(
282378
283379
Parameters
284380
----------
285-
selection : Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch]
381+
selection : Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch, FaceSelection]
286382
Nodes to add to this group. Can be:
383+
- FaceSelection instance - computed faces will be added
287384
- TreeSearch instance - will be executed internally
288385
- CollectionTreeSearch instance - will be executed internally
289386
- NodeCollection - nodes will be extracted
@@ -661,6 +758,37 @@ def __getitem__(self, key: str):
661758
def __setitem__(self, key: str, value: Any):
662759
raise NotImplementedError("Assigning/setting entities is not supported.")
663760

761+
def __sub__(self, other: FaceGroup) -> FaceSelection:
762+
"""
763+
Subtract a face group from the geometry to create a face selection.
764+
765+
This allows intuitive syntax like:
766+
geometry - wing_group - tail_group
767+
768+
Parameters
769+
----------
770+
other : FaceGroup
771+
The face group to subtract from all geometry faces
772+
773+
Returns
774+
-------
775+
FaceSelection
776+
A FaceSelection representing all faces minus the specified group
777+
778+
Examples
779+
--------
780+
>>> fuselage = geometry.create_face_group(
781+
... name="fuselage",
782+
... selection=geometry - wing - tail
783+
... )
784+
"""
785+
if not isinstance(other, FaceGroup):
786+
raise Flow360ValueError(
787+
f"Can only subtract FaceGroup from Geometry, got {type(other)}"
788+
)
789+
790+
return FaceSelection(self, include_all=True, face_groups_to_subtract=[other])
791+
664792
# ========== Tree-based face grouping methods ==========
665793

666794
@property
@@ -716,7 +844,7 @@ def load_geometry_tree(self, tree_json_path: str) -> None:
716844
self.print_face_grouping_stats()
717845

718846
def create_face_group(
719-
self, name: str, selection: Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch]
847+
self, name: str, selection: Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch, FaceSelection]
720848
) -> FaceGroup:
721849
"""
722850
Create a face group based on explicit selection of tree nodes
@@ -730,8 +858,9 @@ def create_face_group(
730858
----------
731859
name : str
732860
Name of the face group
733-
selection : Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch]
861+
selection : Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch, FaceSelection]
734862
Can be one of:
863+
- FaceSelection instance (returned from geometry - face_group operations)
735864
- TreeSearch instance (returned from tree_root.search()) - will be executed internally
736865
- CollectionTreeSearch instance (returned from NodeCollection.search()) - will be executed internally
737866
- NodeCollection (returned from tree_root.children()) - nodes will be extracted
@@ -755,6 +884,12 @@ def create_face_group(
755884
... selection=geometry.search(type=NodeType.FRMFeature, name="*wing*")
756885
... )
757886
>>>
887+
>>> # Using subtraction (boolean operations)
888+
>>> fuselage_group = geometry.create_face_group(
889+
... name="fuselage",
890+
... selection=geometry - wing_group - tail_group
891+
... )
892+
>>>
758893
>>> # Using children() chaining (fluent navigation with exact matching)
759894
>>> body_group = geometry.create_face_group(
760895
... name="body",
@@ -790,7 +925,7 @@ def create_face_group(
790925
return face_group
791926

792927
def _add_to_face_group(
793-
self, face_group: FaceGroup, selection: Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch]
928+
self, face_group: FaceGroup, selection: Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch, FaceSelection]
794929
) -> None:
795930
"""
796931
Internal method to add faces to a face group.
@@ -807,34 +942,67 @@ def _add_to_face_group(
807942
----------
808943
face_group : FaceGroup
809944
The face group to add faces to
810-
selection : Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch]
811-
The selection to add (TreeSearch, CollectionTreeSearch, NodeCollection, TreeNode, or list of TreeNodes)
945+
selection : Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch, FaceSelection]
946+
The selection to add (TreeSearch, CollectionTreeSearch, NodeCollection, TreeNode, list of TreeNodes, or FaceSelection)
812947
813948
Notes
814949
-----
815950
If moving faces causes any group to become empty (0 faces), that group will be
816951
automatically removed from the Geometry's face group registry.
817952
"""
818953
# Handle different selection types
819-
if isinstance(selection, (TreeSearch, CollectionTreeSearch)):
954+
if isinstance(selection, FaceSelection):
955+
# FaceSelection: get the computed face list
956+
new_faces = selection.get_selected_faces()
957+
new_face_uuids = {face.uuid for face in new_faces if face.uuid}
958+
elif isinstance(selection, (TreeSearch, CollectionTreeSearch)):
820959
selected_nodes = selection.execute()
960+
# Collect faces from selected nodes
961+
new_faces = []
962+
new_face_uuids = set()
963+
964+
for node in selected_nodes:
965+
faces = node.get_all_faces()
966+
for face in faces:
967+
if face.uuid:
968+
new_faces.append(face)
969+
new_face_uuids.add(face.uuid)
821970
elif isinstance(selection, NodeCollection):
822971
selected_nodes = selection.nodes
972+
# Collect faces from selected nodes
973+
new_faces = []
974+
new_face_uuids = set()
975+
976+
for node in selected_nodes:
977+
faces = node.get_all_faces()
978+
for face in faces:
979+
if face.uuid:
980+
new_faces.append(face)
981+
new_face_uuids.add(face.uuid)
823982
elif isinstance(selection, TreeNode):
824983
selected_nodes = [selection]
984+
# Collect faces from selected nodes
985+
new_faces = []
986+
new_face_uuids = set()
987+
988+
for node in selected_nodes:
989+
faces = node.get_all_faces()
990+
for face in faces:
991+
if face.uuid:
992+
new_faces.append(face)
993+
new_face_uuids.add(face.uuid)
825994
else:
826995
selected_nodes = selection
827-
828-
# Collect faces from selected nodes
829-
new_faces = []
830-
new_face_uuids = set()
831-
832-
for node in selected_nodes:
833-
faces = node.get_all_faces()
834-
for face in faces:
835-
if face.uuid:
836-
new_faces.append(face)
837-
new_face_uuids.add(face.uuid)
996+
# Collect faces from selected nodes
997+
new_faces = []
998+
new_face_uuids = set()
999+
1000+
for node in selected_nodes:
1001+
faces = node.get_all_faces()
1002+
for face in faces:
1003+
if face.uuid:
1004+
new_faces.append(face)
1005+
new_face_uuids.add(face.uuid)
8381006

8391007
# Remove these faces from their previous groups
8401008
groups_to_check = set()

0 commit comments

Comments
 (0)