Skip to content

Commit d666a68

Browse files
committed
Add filtering and geometry functionalities to ElementCollection
- Introduced `and_` method for combining collections with AND logic. - Added `between` method for numeric comparisons. - Enhanced `equals` method to support also numbers. - Implemented `getGeometry` function for height filtering. - Updated documentation for clarity on method usage and parameters. - Added example usage in examples.py.
1 parent 77f0bf1 commit d666a68

File tree

4 files changed

+245
-22
lines changed

4 files changed

+245
-22
lines changed

examples/examples.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@
2727
)
2828
print(col_elements)
2929

30+
wanted = (
31+
elements.filterBy(Filter.PROPERTY)
32+
.property("Prop Group Name", "Prop Name")
33+
.between(1.0, 2.5)
34+
)
35+
print(wanted)
36+
3037
# perisso supports slicing, too
3138
print(col_elements[0])
3239

@@ -37,6 +44,8 @@
3744
print(structural)
3845

3946
# additional functionalities:
40-
perisso().filterBy(Filter.PROPERTY).property("Prop Group Name", "Prop Name").equals("test").highlight()
47+
perisso().filterBy(Filter.PROPERTY).property("Prop Group Name", "Prop Name").equals(
48+
"test"
49+
).highlight()
4150
sleep(5)
4251
clearhighlight()

src/perisso/collection.py

Lines changed: 195 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from archicad import Types as act
22
from .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

66
class 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")

src/perisso/enums.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,4 @@ class Filter(Enum):
9595

9696
# geometry
9797
# LENGTH = "length"
98-
# HEIGHT = "heigth"
98+
HEIGHT = "height"

src/perisso/utils.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
from archicad import ACConnection
33
from archicad.releases import Commands, Types, Utilities
4-
from .enums import Filter
4+
from .enums import Filter, ElType
55

66
# Connection setup
77
conn = ACConnection.connect()
@@ -15,7 +15,12 @@
1515

1616
def rtc(command: str, *args):
1717
"""Run a Tapir Command \n
18-
Uses the official `archicad` package.
18+
Uses the official `archicad` package for that.
19+
20+
Args:
21+
command (`str`): The name of the tapir command to be run.
22+
*args (`dict`): Usually a dict with the additional parameters.
23+
Could look like this: `{"elements": perisso().get()}`
1924
"""
2025
if args:
2126
return acc.ExecuteAddOnCommand(
@@ -83,10 +88,40 @@ def _getPropValues(*, builtin: str = None, propGUID: str = None, elements: dict
8388
return ret_values
8489

8590

86-
def getDetails(type_: Filter, elements: dict) -> list:
91+
def getDetails(filter: Filter, elements: dict) -> list:
8792
json_ = rtc("GetDetailsOfElements", {"elements": elements})["detailsOfElements"]
8893
ret_values = []
89-
if type_ == Filter.ELEMENT_TYPE:
94+
if filter == Filter.ELEMENT_TYPE:
9095
# ElementType can never be error
9196
ret_values = [{"ok": True, "value": i["type"]} for i in json_]
97+
else:
98+
raise NotImplementedError
99+
return ret_values
100+
101+
102+
def getGeometry(filter: Filter, elements: dict) -> list:
103+
ret_values = []
104+
elem_types = getDetails(filter=Filter.ELEMENT_TYPE, elements=elements)
105+
if filter == Filter.HEIGHT:
106+
for i, item in enumerate(elements):
107+
if elem_types[i]["value"] == ElType.MORPH.value:
108+
ret_values.append({"ok": True, "value": _getBBoxSize([item])[0]["z"]})
109+
else:
110+
ret_values.append({"ok": False, "error": "not implemented"})
111+
else:
112+
raise NotImplementedError
113+
return ret_values
114+
115+
116+
def _getBBoxSize(elements: dict) -> list:
117+
"""Gets the size of the fitted Bounding Box of the elements."""
118+
ret_values = []
119+
bboxes_raw = rtc("Get3DBoundingBoxes", {"elements": elements})
120+
for i, item in enumerate(bboxes_raw["boundingBoxes3D"]):
121+
dimensions = {
122+
"x": item["boundingBox3D"]["xMax"] - item["boundingBox3D"]["xMin"],
123+
"y": item["boundingBox3D"]["yMax"] - item["boundingBox3D"]["yMin"],
124+
"z": item["boundingBox3D"]["zMax"] - item["boundingBox3D"]["zMin"],
125+
}
126+
ret_values.append(dimensions)
92127
return ret_values

0 commit comments

Comments
 (0)