mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 02:02:22 +00:00
Enhance filtering capabilities with JSON support for include/exclude operations
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user