Enhance filtering capabilities with JSON support for include/exclude operations

This commit is contained in:
falken10vdl
2025-12-16 19:47:41 +01:00
parent ce35903459
commit bf75509fdf
3 changed files with 177 additions and 10 deletions
@@ -3661,8 +3661,12 @@ class EnableEditingElementFilter(bpy.types.Operator, tool.Ifc.Operator):
if query := ifcopenshell.util.element.get_pset(element, "EPset_Drawing", self.filter_mode.title()):
filter_groups = tool.Search.get_filter_groups(f"drawing_{self.filter_mode.lower()}")
try:
tool.Search.import_filter_query(query, filter_groups)
except:
data = json.loads(query)
if isinstance(data, dict) and "filter_structure" in data:
tool.Search.import_filter_structure(data["filter_structure"], filter_groups)
else:
tool.Search.import_filter_query(query, filter_groups)
except Exception:
pass
@@ -3682,12 +3686,48 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator):
assert element
pset = tool.Pset.get_element_pset(element, "EPset_Drawing")
assert pset
preferences = tool.Blender.get_addon_preferences()
enable_suggestions = getattr(preferences, "search_filter_suggestions", False)
if self.filter_mode == "INCLUDE":
query = tool.Search.export_filter_query(props.include_filter_groups) or None
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Include": query})
filter_groups = props.include_filter_groups
elif self.filter_mode == "EXCLUDE":
query = tool.Search.export_filter_query(props.exclude_filter_groups) or None
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Exclude": query})
filter_groups = props.exclude_filter_groups
else:
return
query = tool.Search.export_filter_query(filter_groups) or None
if enable_suggestions and query:
filter_structure = []
for filter_group in filter_groups:
group_data = []
for ifc_filter in filter_group.filters:
filter_data = {
"type": ifc_filter.type,
"name": ifc_filter.name,
"value": ifc_filter.value,
"pset": ifc_filter.pset,
"comparison": ifc_filter.comparison,
"filter_mode": ifc_filter.filter_mode,
}
group_data.append(filter_data)
filter_structure.append(group_data)
value = json.dumps({
"type": "BBIM_Search",
"query": query,
"filter_structure": filter_structure
})
else:
value = query
if self.filter_mode == "INCLUDE":
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Include": value})
elif self.filter_mode == "EXCLUDE":
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Exclude": value})
props.filter_mode = "NONE"
bpy.ops.bim.activate_drawing(drawing=element.id(), should_view_from_camera=False)
+35 -4
View File
@@ -2277,7 +2277,16 @@ class Drawing(bonsai.core.tool.Drawing):
pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {})
include = pset.get("Include", None)
if include:
elements = ifcopenshell.util.selector.filter_elements(ifc_file, include)
try:
data = json.loads(include)
if isinstance(data, dict) and "filter_structure" in data:
elements = tool.Search.execute_filter_groups_from_json(data, ifc_file)
elif isinstance(data, dict) and "query" in data:
elements = ifcopenshell.util.selector.filter_elements(ifc_file, data["query"])
else:
elements = ifcopenshell.util.selector.filter_elements(ifc_file, include)
except (json.JSONDecodeError, ValueError):
elements = ifcopenshell.util.selector.filter_elements(ifc_file, include)
else:
if ifc_file.schema == "IFC2X3":
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
@@ -2291,7 +2300,7 @@ class Drawing(bonsai.core.tool.Drawing):
if not i.is_a("IfcAnnotation"):
updated_set.add(i)
# add aggregate too, if element is host by one
if decomposes := i.Decomposes:
if hasattr(i, "Decomposes") and (decomposes := i.Decomposes):
aggregate = decomposes[0].RelatingObject
# remove IfcProject for class iterator. See https://github.com/IfcOpenShell/IfcOpenShell/issues/4361#issuecomment-2081223615
if aggregate.is_a("IfcProduct"):
@@ -2304,7 +2313,18 @@ class Drawing(bonsai.core.tool.Drawing):
exclude = pset.get("Exclude", None)
if exclude:
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
try:
data = json.loads(exclude)
if isinstance(data, dict) and "filter_structure" in data:
exclude_elements = tool.Search.execute_filter_groups_from_json(data, ifc_file)
elements -= exclude_elements
elif isinstance(data, dict) and "query" in data:
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, data["query"])
else:
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
except (json.JSONDecodeError, ValueError):
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
elements -= set(ifc_file.by_type("IfcOpeningElement"))
return elements
@@ -2318,7 +2338,18 @@ class Drawing(bonsai.core.tool.Drawing):
# NOTE: EPset_Drawing.Include is not used to avoid adding other elements besides spaces
exclude = pset.get("Exclude", None)
if exclude:
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
try:
data = json.loads(exclude)
if isinstance(data, dict) and "filter_structure" in data:
exclude_elements = tool.Search.execute_filter_groups_from_json(data, ifc_file)
elements -= exclude_elements
elif isinstance(data, dict) and "query" in data:
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, data["query"])
else:
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
except (json.JSONDecodeError, ValueError):
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
return elements
@classmethod
+96
View File
@@ -233,6 +233,102 @@ class Search(bonsai.core.tool.Search):
return final_results
@classmethod
def execute_filter_groups_from_json(
cls, data: dict, ifc_file: ifcopenshell.file
) -> set[ifcopenshell.entity_instance]:
"""Execute filter groups from JSON data with filter_structure
This is used by drawing include/exclude to properly handle ADD/SUBTRACT/FILTER modes
without needing to instantiate Blender property groups.
"""
filter_structure = data.get("filter_structure", [])
all_group_results = []
for group_data in filter_structure:
group_results = set()
for filter_data in group_data:
filter_mode = filter_data.get("filter_mode", "ADD")
filter_type = filter_data.get("type", "")
value = filter_data.get("value", "")
if not filter_type or not value:
continue
query_part = None
if filter_type == "entity":
query_part = value
elif filter_type == "attribute":
name = filter_data.get("name", "")
if not name:
continue
comparison = filter_data.get("comparison", "=")
query_part = f"{name}{comparison}{cls._wrap_json_value(value)}"
elif filter_type == "property":
pset = filter_data.get("pset", "")
name = filter_data.get("name", "")
if not pset or not name:
continue
comparison = filter_data.get("comparison", " = ")
wrapped_pset = cls._wrap_json_value(pset)
wrapped_name = cls._wrap_json_value(name)
wrapped_value = cls._wrap_json_value(value)
query_part = f"{wrapped_pset}.{wrapped_name} {comparison} {wrapped_value}"
elif filter_type == "type":
query_part = f"type={cls._wrap_json_value(value)}"
elif filter_type == "material":
query_part = f"material={cls._wrap_json_value(value)}"
elif filter_type == "classification":
query_part = f"classification={cls._wrap_json_value(value)}"
elif filter_type == "location":
query_part = f"location={cls._wrap_json_value(value)}"
elif filter_type == "group":
query_part = f"group={cls._wrap_json_value(value)}"
elif filter_type == "parent":
query_part = f"parent={cls._wrap_json_value(value)}"
elif filter_type == "query":
name = filter_data.get("name", "")
if not name:
continue
keys = cls._wrap_json_value(name)
comparison = filter_data.get("comparison", "=")
wrapped_value = cls._wrap_json_value(value)
query_part = f"query:{keys}{comparison}{wrapped_value}"
elif filter_type == "instance":
query_part = value
if not query_part:
continue
if filter_mode == "FILTER" and group_results:
results = ifcopenshell.util.selector.filter_elements(ifc_file, query_part, elements=group_results)
group_results = results
elif filter_mode == "SUBTRACT":
results = ifcopenshell.util.selector.filter_elements(ifc_file, query_part)
group_results -= results
else: # ADD
results = ifcopenshell.util.selector.filter_elements(ifc_file, query_part)
group_results.update(results)
if group_results:
all_group_results.append(group_results)
final_results = set()
for group_results in all_group_results:
final_results.update(group_results)
return final_results
@classmethod
def _wrap_json_value(cls, value: str) -> str:
"""Wrap value for use in query string"""
if value.startswith("/") and value.endswith("/"):
return value
elif value in ("NULL", "TRUE", "FALSE"):
return value
return '"' + value.replace('"', '\\"') + '"'
@classmethod
def get_comparison_and_value(
cls, ifc_filter: BIMFacet