11from archicad import Types as act
22from .enums import ElType , Filter
3- from .utils import getPropValues , getDetails , rtc , acu , _pprint # noqa: F401
3+ from .utils import getPropValues , getDetails , getGeometry , rtc , acu , _pprint # noqa: F401
44
55
66class ElementCollection :
@@ -35,6 +35,8 @@ def __init__(self, elements, *, _field=None, _propGUID: str = None):
3535 Filter .PROPERTY : lambda elements : getPropValues (
3636 propGUID = self ._propGUID , elements = elements
3737 ),
38+ # geometry
39+ Filter .HEIGHT : lambda elements : getGeometry (Filter .HEIGHT , elements ),
3840 }
3941
4042 def filterBy (self , field : Filter ):
@@ -46,7 +48,13 @@ def filterBy(self, field: Filter):
4648 return self
4749
4850 def property (self , group : str , name : str ):
49- """When filtering to a property all elements that do not have the property available are discarded."""
51+ """Must follow on a `.filterBy(Filter.PROPERTY)` to specify which Property should be read.
52+ Please note: When filtering to a property all elements that do not have the property available are discarded.
53+
54+ Args:
55+ group (`str`): Property group name.
56+ name (`str`): Property name.
57+ """
5058 if not self ._field == Filter .PROPERTY :
5159 raise ValueError ("`filterBy()` must be set to `Filter.PROPERTY'" )
5260
@@ -68,6 +76,41 @@ def property(self, group: str, name: str):
6876 filtered , _field = Filter .PROPERTY , _propGUID = self ._propGUID
6977 )
7078
79+ def and_ (self , other_collection_or_callable ):
80+ """Combine with another ElementCollection using AND logic (intersection).
81+
82+ Args:
83+ other_collection_or_callable: Either an ElementCollection or a callable
84+ that takes this collection and returns an ElementCollection
85+
86+ Returns:
87+ ElementCollection: New collection containing only elements present in both collections
88+ """
89+ if callable (other_collection_or_callable ):
90+ other_collection = other_collection_or_callable (
91+ ElementCollection (self .elements )
92+ )
93+ elif isinstance (other_collection_or_callable , ElementCollection ):
94+ other_collection = other_collection_or_callable
95+ else :
96+ raise TypeError (
97+ "Argument must be an ElementCollection or callable returning one"
98+ )
99+
100+ # Get GUIDs from the other collection
101+ other_guids = {
102+ element ["elementId" ]["guid" ] for element in other_collection .elements
103+ }
104+
105+ # Keep only elements that exist in both collections
106+ intersection_elements = [
107+ element
108+ for element in self .elements
109+ if element ["elementId" ]["guid" ] in other_guids
110+ ]
111+
112+ return ElementCollection (intersection_elements )
113+
71114 # region // string comparisons
72115 def startsWith (self , value : str | ElType , casesensitive : bool = True ):
73116 if not self ._field :
@@ -151,31 +194,150 @@ def contains(self, value: str | ElType, casesensitive: bool = True):
151194
152195 return ElementCollection (filtered )
153196
154- def equals (self , value : str | ElType , casesensitive : bool = True ):
197+ def equals (self , value : str | int | float | ElType , casesensitive : bool = True ):
198+ """Keep only the elements whose value match the input.
199+ Args:
200+ value (`str` | `int`| `float`| `ElType`): Value to check against.
201+ casesensitive (`bool`): Determines if the check should be perforemed case-sensitive.
202+
203+ Returns:
204+ `ElementCollection`: Returns a new ElementCollection.
205+ """
155206 if not self ._field :
156207 raise ValueError ("Must call filterBy() first" )
157208
158209 if isinstance (value , ElType ):
159210 # get 'value' from enum
160211 value = value .value
161212
162- if not casesensitive :
163- value = value .casefold ()
213+ handler = self ._pattern_handlers .get (self ._field )
214+ if handler :
215+ ret_values = handler (self .elements )
216+ filtered = []
217+ for i , item in enumerate (ret_values ):
218+ if item ["ok" ]: # exclude errors
219+ if isinstance (value , (int , float )):
220+ try :
221+ item_value = float (item ["value" ])
222+ if item_value == value :
223+ filtered .append (self .elements [i ])
224+ except (ValueError , TypeError ):
225+ # Skip non-numeric values
226+ continue
227+ else :
228+ # String comparison
229+ if not casesensitive :
230+ value = value .casefold ()
231+
232+ item_str = (
233+ str (item ["value" ]).casefold ()
234+ if not casesensitive
235+ else str (item ["value" ])
236+ )
237+ if value == item_str :
238+ filtered .append (self .elements [i ])
239+
240+ return ElementCollection (filtered )
241+
242+ # endregion //
243+
244+ # region // numeric comparisons
245+ def lessThan (self , value : int | float , inclusive : bool = False ):
246+ """Keep only elements whose numeric value is less than the input.
247+ Args:
248+ value (`int` | `float`): Numeric value to compare against.
249+ inclusive (`bool`): If True, uses <= instead of <.
250+
251+ Returns:
252+ `ElementCollection`: Returns a new ElementCollection.
253+ """
254+ if not self ._field :
255+ raise ValueError ("Must call filterBy() first" )
164256
165257 handler = self ._pattern_handlers .get (self ._field )
166258 if handler :
167259 ret_values = handler (self .elements )
168- filtered = [
169- self .elements [i ]
170- for i , item in enumerate (ret_values )
171- if item ["ok" ] # exclude errors
172- and value
173- == (
174- str (item ["value" ]).casefold ()
175- if not casesensitive
176- else str (item ["value" ])
177- )
178- ]
260+ filtered = []
261+ for i , item in enumerate (ret_values ):
262+ if item ["ok" ]: # exclude errors
263+ try :
264+ item_value = float (item ["value" ])
265+ if inclusive :
266+ if item_value <= value :
267+ filtered .append (self .elements [i ])
268+ else :
269+ if item_value < value :
270+ filtered .append (self .elements [i ])
271+ except (ValueError , TypeError ):
272+ # Skip non-numeric values
273+ continue
274+
275+ return ElementCollection (filtered )
276+
277+ def greaterThan (self , value : int | float , inclusive : bool = False ):
278+ """Keep only elements whose numeric value is greater than the input.
279+ Args:
280+ value (`int` | `float`): Numeric value to compare against.
281+ inclusive (`bool`): If True, uses >= instead of >.
282+
283+ Returns:
284+ `ElementCollection`: Returns a new ElementCollection.
285+ """
286+ if not self ._field :
287+ raise ValueError ("Must call filterBy() first" )
288+
289+ handler = self ._pattern_handlers .get (self ._field )
290+ if handler :
291+ ret_values = handler (self .elements )
292+ filtered = []
293+ for i , item in enumerate (ret_values ):
294+ if item ["ok" ]: # exclude errors
295+ try :
296+ item_value = float (item ["value" ])
297+ if inclusive :
298+ if item_value >= value :
299+ filtered .append (self .elements [i ])
300+ else :
301+ if item_value > value :
302+ filtered .append (self .elements [i ])
303+ except (ValueError , TypeError ):
304+ # Skip non-numeric values
305+ continue
306+
307+ return ElementCollection (filtered )
308+
309+ def between (
310+ self , min_value : int | float , max_value : int | float , inclusive : bool = True
311+ ):
312+ """Keep only elements whose numeric value is between min and max values.
313+ Args:
314+ min_value (`int` | `float`): Minimum value (lower bound).
315+ max_value (`int` | `float`): Maximum value (upper bound).
316+ inclusive (`bool`): If True, includes boundary values.
317+
318+ Returns:
319+ `ElementCollection`: Returns a new ElementCollection.
320+ """
321+ if not self ._field :
322+ raise ValueError ("Must call filterBy() first" )
323+
324+ handler = self ._pattern_handlers .get (self ._field )
325+ if handler :
326+ ret_values = handler (self .elements )
327+ filtered = []
328+ for i , item in enumerate (ret_values ):
329+ if item ["ok" ]: # exclude errors
330+ try :
331+ item_value = float (item ["value" ])
332+ if inclusive :
333+ if min_value <= item_value <= max_value :
334+ filtered .append (self .elements [i ])
335+ else :
336+ if min_value < item_value < max_value :
337+ filtered .append (self .elements [i ])
338+ except (ValueError , TypeError ):
339+ # Skip non-numeric values
340+ continue
179341
180342 return ElementCollection (filtered )
181343
@@ -208,6 +370,23 @@ def highlight(
208370 noncolor : list [int ] = [164 , 166 , 165 , 128 ],
209371 wireframe = True ,
210372 ):
373+ """Highlight elements in the collection with specified colors.
374+ Args:
375+ color (`list[int]`, optional): RGBA color values for highlighted elements.
376+ Must be a list of exactly 4 integers.
377+ noncolor (`list[int]`, optional): RGBA color values for non-highlighted elements.
378+ Must be a list of exactly 4 integers.
379+ wireframe (`bool`, optional): Whether to display elements in wireframe mode in 3D.
380+ Defaults to True.
381+ Returns:
382+ self: Returns the collection instance for method chaining.
383+ Raises:
384+ ValueError: If color or noncolor parameters are not lists of exactly 4 integers,
385+ or if any color values are not integers.
386+ Example:
387+ >>> collection.highlight(color=[255, 0, 0, 255], wireframe=False)
388+ >>> collection.highlight(noncolor=[128, 128, 128, 100])
389+ """
211390 # Validate color parameter
212391 if not isinstance (color , list ) or len (color ) != 4 :
213392 raise ValueError ("Color must be a list of exactly 4 integers" )
0 commit comments