mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-26 18:21:59 +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()):
|
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()}")
|
filter_groups = tool.Search.get_filter_groups(f"drawing_{self.filter_mode.lower()}")
|
||||||
try:
|
try:
|
||||||
tool.Search.import_filter_query(query, filter_groups)
|
data = json.loads(query)
|
||||||
except:
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -3682,12 +3686,48 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
assert element
|
assert element
|
||||||
pset = tool.Pset.get_element_pset(element, "EPset_Drawing")
|
pset = tool.Pset.get_element_pset(element, "EPset_Drawing")
|
||||||
assert pset
|
assert pset
|
||||||
|
|
||||||
|
preferences = tool.Blender.get_addon_preferences()
|
||||||
|
enable_suggestions = getattr(preferences, "search_filter_suggestions", False)
|
||||||
|
|
||||||
if self.filter_mode == "INCLUDE":
|
if self.filter_mode == "INCLUDE":
|
||||||
query = tool.Search.export_filter_query(props.include_filter_groups) or None
|
filter_groups = props.include_filter_groups
|
||||||
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Include": query})
|
|
||||||
elif self.filter_mode == "EXCLUDE":
|
elif self.filter_mode == "EXCLUDE":
|
||||||
query = tool.Search.export_filter_query(props.exclude_filter_groups) or None
|
filter_groups = props.exclude_filter_groups
|
||||||
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Exclude": query})
|
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"
|
props.filter_mode = "NONE"
|
||||||
bpy.ops.bim.activate_drawing(drawing=element.id(), should_view_from_camera=False)
|
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", {})
|
pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {})
|
||||||
include = pset.get("Include", None)
|
include = pset.get("Include", None)
|
||||||
if include:
|
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:
|
else:
|
||||||
if ifc_file.schema == "IFC2X3":
|
if ifc_file.schema == "IFC2X3":
|
||||||
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
|
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"):
|
if not i.is_a("IfcAnnotation"):
|
||||||
updated_set.add(i)
|
updated_set.add(i)
|
||||||
# add aggregate too, if element is host by one
|
# 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
|
aggregate = decomposes[0].RelatingObject
|
||||||
# remove IfcProject for class iterator. See https://github.com/IfcOpenShell/IfcOpenShell/issues/4361#issuecomment-2081223615
|
# remove IfcProject for class iterator. See https://github.com/IfcOpenShell/IfcOpenShell/issues/4361#issuecomment-2081223615
|
||||||
if aggregate.is_a("IfcProduct"):
|
if aggregate.is_a("IfcProduct"):
|
||||||
@@ -2304,7 +2313,18 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
|
|
||||||
exclude = pset.get("Exclude", None)
|
exclude = pset.get("Exclude", None)
|
||||||
if exclude:
|
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"))
|
elements -= set(ifc_file.by_type("IfcOpeningElement"))
|
||||||
return elements
|
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
|
# NOTE: EPset_Drawing.Include is not used to avoid adding other elements besides spaces
|
||||||
exclude = pset.get("Exclude", None)
|
exclude = pset.get("Exclude", None)
|
||||||
if exclude:
|
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
|
return elements
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -233,6 +233,102 @@ class Search(bonsai.core.tool.Search):
|
|||||||
|
|
||||||
return final_results
|
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
|
@classmethod
|
||||||
def get_comparison_and_value(
|
def get_comparison_and_value(
|
||||||
cls, ifc_filter: BIMFacet
|
cls, ifc_filter: BIMFacet
|
||||||
|
|||||||
Reference in New Issue
Block a user