Skip to content

Commit c296a13

Browse files
add search for NodeCollection class
1 parent fa98b9e commit c296a13

File tree

3 files changed

+165
-8
lines changed

3 files changed

+165
-8
lines changed

flow360/component/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Flow360 Component Module"""
22

33
from flow360.component.geometry_tree import (
4+
CollectionTreeSearch,
45
GeometryTree,
56
NodeCollection,
67
NodeType,
@@ -9,6 +10,7 @@
910
)
1011

1112
__all__ = [
13+
"CollectionTreeSearch",
1214
"GeometryTree",
1315
"NodeCollection",
1416
"NodeType",

flow360/component/geometry.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from flow360.cloud.heartbeat import post_upload_heartbeat
2020
from flow360.cloud.rest_api import RestApi
2121
from flow360.component.geometry_tree import (
22+
CollectionTreeSearch,
2223
GeometryTree,
2324
NodeCollection,
2425
NodeType,
@@ -272,7 +273,7 @@ def face_count(self) -> int:
272273
return len(self._faces)
273274

274275
def add(
275-
self, selection: Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch]
276+
self, selection: Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch]
276277
) -> "FaceGroup":
277278
"""
278279
Add more nodes to this face group.
@@ -281,9 +282,10 @@ def add(
281282
282283
Parameters
283284
----------
284-
selection : Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch]
285+
selection : Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch]
285286
Nodes to add to this group. Can be:
286287
- TreeSearch instance - will be executed internally
288+
- CollectionTreeSearch instance - will be executed internally
287289
- NodeCollection - nodes will be extracted
288290
- Single TreeNode - will be wrapped in a list
289291
- List of TreeNode instances
@@ -714,7 +716,7 @@ def load_geometry_tree(self, tree_json_path: str) -> None:
714716
self.print_face_grouping_stats()
715717

716718
def create_face_group(
717-
self, name: str, selection: Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch]
719+
self, name: str, selection: Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch]
718720
) -> FaceGroup:
719721
"""
720722
Create a face group based on explicit selection of tree nodes
@@ -728,9 +730,10 @@ def create_face_group(
728730
----------
729731
name : str
730732
Name of the face group
731-
selection : Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch]
733+
selection : Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch]
732734
Can be one of:
733735
- TreeSearch instance (returned from tree_root.search()) - will be executed internally
736+
- CollectionTreeSearch instance (returned from NodeCollection.search()) - will be executed internally
734737
- NodeCollection (returned from tree_root.children()) - nodes will be extracted
735738
- Single TreeNode - all faces under this node will be included
736739
- List of TreeNode instances - all faces under these nodes will be included
@@ -787,7 +790,7 @@ def create_face_group(
787790
return face_group
788791

789792
def _add_to_face_group(
790-
self, face_group: FaceGroup, selection: Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch]
793+
self, face_group: FaceGroup, selection: Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch]
791794
) -> None:
792795
"""
793796
Internal method to add faces to a face group.
@@ -804,16 +807,16 @@ def _add_to_face_group(
804807
----------
805808
face_group : FaceGroup
806809
The face group to add faces to
807-
selection : Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch]
808-
The selection to add (TreeSearch, NodeCollection, TreeNode, or list of TreeNodes)
810+
selection : Union[TreeNode, List[TreeNode], NodeCollection, TreeSearch, CollectionTreeSearch]
811+
The selection to add (TreeSearch, CollectionTreeSearch, NodeCollection, TreeNode, or list of TreeNodes)
809812
810813
Notes
811814
-----
812815
If moving faces causes any group to become empty (0 faces), that group will be
813816
automatically removed from the Geometry's face group registry.
814817
"""
815818
# Handle different selection types
816-
if isinstance(selection, TreeSearch):
819+
if isinstance(selection, (TreeSearch, CollectionTreeSearch)):
817820
selected_nodes = selection.execute()
818821
elif isinstance(selection, NodeCollection):
819822
selected_nodes = selection.nodes

flow360/component/geometry_tree.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,63 @@ def children(
331331

332332
return NodeCollection(all_children)
333333

334+
def search(
335+
self,
336+
type: Optional[NodeType] = None,
337+
name: Optional[str] = None,
338+
colorRGB: Optional[str] = None,
339+
material: Optional[str] = None,
340+
attributes: Optional[Dict[str, str]] = None,
341+
) -> "CollectionTreeSearch":
342+
"""
343+
Create a deferred search operation across all nodes in the collection.
344+
345+
This method searches the subtrees of all nodes in this collection for nodes
346+
matching the given criteria. It returns a CollectionTreeSearch instance that
347+
captures the search criteria but does not execute until needed.
348+
349+
Supports wildcard matching for name and material using '*' character.
350+
All criteria are ANDed together.
351+
352+
Parameters
353+
----------
354+
type : Optional[NodeType]
355+
Node type to match (e.g., NodeType.FRMFeature)
356+
name : Optional[str]
357+
Name pattern to match. Supports wildcards:
358+
- "*wing*" matches any name containing "wing"
359+
- "wing*" matches any name starting with "wing"
360+
- "*wing" matches any name ending with "wing"
361+
- "wing" matches exact name "wing"
362+
colorRGB : Optional[str]
363+
RGB color string to match (e.g., "255,0,0" for red)
364+
material : Optional[str]
365+
Material name to match. Supports wildcard matching like name parameter.
366+
attributes : Optional[Dict[str, str]]
367+
Dictionary of attribute key-value pairs to match
368+
369+
Returns
370+
-------
371+
CollectionTreeSearch
372+
A search instance that can be executed later to get matching nodes
373+
374+
Examples
375+
--------
376+
>>> # Search for FRMFeature nodes across multiple nodes
377+
>>> results = collection.search(type=NodeType.FRMFeature, name="Boss-Extrude3")
378+
>>>
379+
>>> # Pass to create_face_group or add methods
380+
>>> wing.add(results)
381+
"""
382+
return CollectionTreeSearch(
383+
nodes=self._nodes,
384+
type=type,
385+
name=name,
386+
colorRGB=colorRGB,
387+
material=material,
388+
attributes=attributes,
389+
)
390+
334391
def __len__(self) -> int:
335392
"""Return the number of nodes in this collection"""
336393
return len(self._nodes)
@@ -457,6 +514,101 @@ def __repr__(self):
457514
return f"TreeSearch({criteria_str})"
458515

459516

517+
class CollectionTreeSearch:
518+
"""
519+
Represents a deferred tree search operation across multiple nodes.
520+
521+
This class is similar to TreeSearch but operates on a collection of nodes
522+
instead of a single node. It captures search criteria and executes the search
523+
across all nodes when requested.
524+
"""
525+
526+
def __init__(
527+
self,
528+
nodes: List[TreeNode],
529+
type: Optional[NodeType] = None,
530+
name: Optional[str] = None,
531+
colorRGB: Optional[str] = None,
532+
material: Optional[str] = None,
533+
attributes: Optional[Dict[str, str]] = None,
534+
):
535+
"""
536+
Initialize a CollectionTreeSearch with search criteria.
537+
538+
Parameters
539+
----------
540+
nodes : List[TreeNode]
541+
The nodes from which to start the search (searches their subtrees)
542+
type : Optional[NodeType]
543+
Node type to match (e.g., NodeType.FRMFeature)
544+
name : Optional[str]
545+
Name pattern to match. Supports wildcards (e.g., "*wing*")
546+
colorRGB : Optional[str]
547+
RGB color string to match (e.g., "255,0,0")
548+
material : Optional[str]
549+
Material name to match. Supports wildcards.
550+
attributes : Optional[Dict[str, str]]
551+
Dictionary of attribute key-value pairs to match
552+
"""
553+
self.nodes = nodes
554+
self.type = type
555+
self.name = name
556+
self.colorRGB = colorRGB
557+
self.material = material
558+
self.attributes = attributes
559+
560+
def execute(self) -> List[TreeNode]:
561+
"""
562+
Execute the search across all nodes and return matching nodes.
563+
564+
Searches the subtree of each node in the collection and combines
565+
all matching results, avoiding duplicates.
566+
567+
Returns
568+
-------
569+
List[TreeNode]
570+
List of unique nodes matching the search criteria across all subtrees
571+
"""
572+
all_matches = []
573+
seen_ids = set()
574+
575+
for node in self.nodes:
576+
# Create a TreeSearch for this node
577+
tree_search = TreeSearch(
578+
node=node,
579+
type=self.type,
580+
name=self.name,
581+
colorRGB=self.colorRGB,
582+
material=self.material,
583+
attributes=self.attributes,
584+
)
585+
586+
# Execute and collect matches, avoiding duplicates
587+
matches = tree_search.execute()
588+
for match in matches:
589+
node_id = id(match)
590+
if node_id not in seen_ids:
591+
seen_ids.add(node_id)
592+
all_matches.append(match)
593+
594+
return all_matches
595+
596+
def __repr__(self):
597+
criteria = []
598+
if self.type is not None:
599+
criteria.append(f"type={self.type.value}")
600+
if self.name is not None:
601+
criteria.append(f"name='{self.name}'")
602+
if self.colorRGB is not None:
603+
criteria.append(f"colorRGB='{self.colorRGB}'")
604+
if self.material is not None:
605+
criteria.append(f"material='{self.material}'")
606+
if self.attributes is not None:
607+
criteria.append(f"attributes={self.attributes}")
608+
criteria_str = ", ".join(criteria)
609+
return f"CollectionTreeSearch({len(self.nodes)} nodes, {criteria_str})"
610+
611+
460612
class GeometryTree:
461613
"""Pure tree structure representing Geometry hierarchy"""
462614

0 commit comments

Comments
 (0)