@@ -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+
235331class 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