Merge pull request #7498 from falken10vdl/New-search-tool

New search tool interface. This one supersedes #7315.

Cheers!
This commit is contained in:
falken10vdl
2025-12-18 15:41:54 +01:00
committed by GitHub
8 changed files with 1289 additions and 82 deletions
+217 -2
View File
@@ -498,6 +498,8 @@ def draw_filter(
if data.data["saved_searches"]:
row.operator("bim.load_search", text="", icon="IMPORT").module = module
row.operator("bim.save_search", text="", icon="EXPORT").module = module
if data.data["saved_searches"]:
row.operator("bim.remove_search", text="", icon="REMOVE").module = module
if module != "search":
if module == "drawing_include":
row.operator("bim.edit_element_filter", icon="CHECKMARK", text="").filter_mode = "INCLUDE"
@@ -505,8 +507,17 @@ def draw_filter(
row.operator("bim.edit_element_filter", icon="CHECKMARK", text="").filter_mode = "EXCLUDE"
row.operator("bim.enable_editing_element_filter", icon="CANCEL", text="").filter_mode = "NONE"
row = layout.row(align=True)
row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD").module = module
row.operator("bim.edit_filter_query", text="", icon="FILTER").module = module
if not tool.Blender.get_addon_preferences().chain_filter_with_set_operations:
row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD").module = module
else:
if not filter_groups or not any(fg.filters for fg in filter_groups):
op = row.operator("bim.add_filter", text="Add Filter", icon="ADD")
op.type = "entity"
op.index = 0
op.module = module
op = row.operator("bim.edit_filter_query", text="", icon="FILTER")
if "module" in op.bl_rna.properties:
op.module = module
for i, filter_group in enumerate(filter_groups):
box = layout.box()
@@ -524,43 +535,247 @@ def draw_filter(
for j, ifc_filter in enumerate(filter_group.filters):
if ifc_filter.type == "entity":
row = box.row(align=True)
preferences = tool.Blender.get_addon_preferences()
if preferences.chain_filter_with_set_operations:
show_mode_toggle = j > 0
else:
show_mode_toggle = preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0 # PR 7315 mode
if show_mode_toggle:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "value", text="", icon="FILE_3D")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
elif ifc_filter.type == "attribute":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "name", text="", icon="COPY_ID")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
op.suggestion_type = "attribute_name"
row.prop(ifc_filter, "value", text="")
if ifc_filter.name:
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
op.suggestion_type = "attribute_value"
elif ifc_filter.type == "type":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "value", text="", icon="FILE_VOLUME")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
elif ifc_filter.type == "material":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "value", text="", icon="MATERIAL")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
elif ifc_filter.type == "property":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "pset", text="", icon="PROPERTIES")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
op.suggestion_type = "pset"
row.prop(ifc_filter, "name", text="")
if ifc_filter.pset:
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
op.suggestion_type = "property_name"
row.prop(ifc_filter, "comparison", text="")
row.prop(ifc_filter, "value", text="")
if ifc_filter.pset and ifc_filter.name:
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
op.suggestion_type = "property_value"
elif ifc_filter.type == "classification":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "value", text="", icon="OUTLINER")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
elif ifc_filter.type == "location":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "value", text="", icon="PACKAGE")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
elif ifc_filter.type == "group":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "value", text="", icon="OUTLINER_COLLECTION")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
elif ifc_filter.type == "parent":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "value", text="", icon="FILE_PARENT")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
elif ifc_filter.type == "query":
row = box.row(align=True)
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations and j > 0:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "name", text="", icon="POINTCLOUD_DATA")
row.prop(ifc_filter, "comparison", text="")
row.prop(ifc_filter, "value", text="")
elif ifc_filter.type == "instance":
row = box.row(align=True)
preferences = tool.Blender.get_addon_preferences()
if preferences.chain_filter_with_set_operations:
show_mode_toggle = j > 0
else:
show_mode_toggle = preferences.default_filter_with_set_operations_for_globalid_and_class and j > 0 # PR 7315 mode
if show_mode_toggle:
mode_icons = {"ADD": "ADD", "SUBTRACT": "REMOVE", "FILTER": "FILTER"}
op = row.operator(
"bim.toggle_filter_inclusion",
icon=mode_icons.get(ifc_filter.filter_mode, "ADD"),
text="",
depress=ifc_filter.filter_mode != "ADD",
)
op.group_index = i
op.filter_index = j
op.module = module
row.prop(ifc_filter, "value", text="", icon="GRIP")
op = row.operator("bim.filter_value_suggestions", text="", icon="VIEWZOOM")
op.group_index = i
op.filter_index = j
op.module = module
op.filter_type = ifc_filter.type
op = row.operator("bim.select_filter_elements", text="", icon="EYEDROPPER")
op.group_index = i
op.index = j
@@ -3680,8 +3680,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
@@ -3701,12 +3705,45 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator):
assert element
pset = tool.Pset.get_element_pset(element, "EPset_Drawing")
assert pset
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 tool.Blender.get_addon_preferences().chain_filter_with_set_operations 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)
@@ -24,12 +24,15 @@ classes = (
operator.ActivateIfcClassFilter,
operator.AddFilter,
operator.AddFilterGroup,
operator.ApplyFilterFromText,
operator.ColourByProperty,
operator.EditFilterQuery,
operator.FilterValueSuggestions,
operator.LoadColourscheme,
operator.LoadSearch,
operator.RemoveFilter,
operator.RemoveFilterGroup,
operator.RemoveSearch,
operator.ResetObjectColours,
operator.SaveColourscheme,
operator.SaveSearch,
@@ -40,6 +43,7 @@ classes = (
operator.SelectIfcClass,
operator.SelectSimilar,
operator.ShowAllElements,
operator.ToggleFilterInclusion,
operator.ToggleFilterSelection,
prop.BIMColour,
prop.BIMFilterItem,
@@ -55,7 +59,9 @@ classes = (
def register():
bpy.types.Scene.BIMSearchProperties = bpy.props.PointerProperty(type=prop.BIMSearchProperties)
bpy.types.TEXT_HT_header.append(operator.draw_text_editor_header)
def unregister():
del bpy.types.Scene.BIMSearchProperties
bpy.types.TEXT_HT_header.remove(operator.draw_text_editor_header)
+678 -20
View File
@@ -40,9 +40,472 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from bonsai.bim.prop import StrProperty
from typing import TYPE_CHECKING, Literal, get_args, assert_never
def draw_text_editor_header(self, context):
if context.space_data.text and context.space_data.text.name.startswith("FilterQuery_"):
layout = self.layout
layout.separator()
op = layout.operator("bim.apply_filter_from_text", text="Apply Filter Configuration", icon="CHECKMARK")
def update_filter_search_value(self: "FilterValueSuggestions", context: bpy.types.Context) -> None:
filter_groups = tool.Search.get_filter_groups(self.module)
ifc_filter = filter_groups[self.group_index].filters[self.filter_index]
value = self.search_value
if " < " in value:
value = value.split(" < ")[-1]
if ifc_filter.type == "entity":
if " (superclass)" in value:
value = value.replace(" (superclass)", "")
if " > " in value:
value = value.split(" > ")[-1]
elif ifc_filter.type == "instance":
if ": " in value:
value = value.split(": ")[-1]
elif " (" in value:
hierarchy_class = value.split(" (")
element_class = hierarchy_class[-1].rstrip(")")
element_name = hierarchy_class[0] if len(hierarchy_class) > 1 else None
ifc_file = tool.Ifc.get()
if ifc_file:
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
if element.is_a() == element_class:
if element_name is None or (hasattr(element, 'Name') and element.Name == element_name):
value = element.GlobalId
break
except:
continue
elif ifc_filter.type == "parent":
if " (" in value:
value = value.split(" (")[0]
if ifc_filter.type == "property":
if self.suggestion_type == "pset":
ifc_filter.pset = value
elif self.suggestion_type == "property_name":
ifc_filter.name = value
else:
ifc_filter.value = value
elif ifc_filter.type == "attribute":
if self.suggestion_type == "attribute_name":
ifc_filter.name = value
else:
ifc_filter.value = value
else:
ifc_filter.value = value
if self.first_launch:
self.first_launch = False
else:
context.window.screen = context.window.screen
class FilterValueSuggestions(Operator):
bl_idname = "bim.filter_value_suggestions"
bl_label = "Filter Value Suggestions"
bl_description = "Get suggestions for filter values from the current IFC file"
bl_options = {"REGISTER", "UNDO"}
group_index: IntProperty()
filter_index: IntProperty()
module: StringProperty(default="search")
filter_type: StringProperty()
suggestion_type: StringProperty(default="value")
first_launch: BoolProperty(default=True, options={"SKIP_SAVE"})
search_value: StringProperty(
name="Search",
description="Search for filter values",
update=update_filter_search_value,
default="",
options={"SKIP_SAVE"},
)
collection_values: CollectionProperty(type=StrProperty, options={"SKIP_SAVE"})
def execute(self, context):
return {"FINISHED"}
def invoke(self, context, event):
ifc_file = tool.Ifc.get()
if not ifc_file:
self.report({"WARNING"}, "No IFC file loaded")
return {"CANCELLED"}
filter_groups = tool.Search.get_filter_groups(self.module)
ifc_filter = filter_groups[self.group_index].filters[self.filter_index]
string_suggestions = self.get_suggestions(ifc_file, ifc_filter)
if not string_suggestions:
self.report({"INFO"}, f"No suggestions available")
return {"CANCELLED"}
self.collection_values.clear()
for suggestion in natsorted(string_suggestions):
self.collection_values.add().name = suggestion
return context.window_manager.invoke_props_dialog(self, width=800)
def draw(self, context):
layout = self.layout
filter_groups = tool.Search.get_filter_groups(self.module)
ifc_filter = filter_groups[self.group_index].filters[self.filter_index]
label_map = {
"entity": "Select Class",
"type": "Select Type",
"material": "Select Material",
"location": "Select Location",
"group": "Select Group",
"classification": "Select Classification",
"parent": "Select Parent",
"instance": "Select GlobalId",
}
if ifc_filter.type == "attribute":
if self.suggestion_type == "attribute_value":
label = f"Select Value for {ifc_filter.name}"
else:
label = "Select Attribute Name"
elif ifc_filter.type == "property":
if self.suggestion_type == "property_value":
label = f"Select Value for {ifc_filter.pset}.{ifc_filter.name}"
elif self.suggestion_type == "property_name":
label = f"Select Property from {ifc_filter.pset}"
else:
label = "Select Property Set"
else:
label = label_map.get(ifc_filter.type, "Select Value")
row = layout.row()
row.label(text=label)
row = layout.row()
row.prop_search(
self,
"search_value",
self,
"collection_values",
text="",
results_are_suggestions=True,
)
def get_suggestions(self, ifc_file, ifc_filter):
suggestions = set()
if ifc_filter.type == "entity":
suggestions = self.get_entity_suggestions(ifc_file)
elif ifc_filter.type == "type":
suggestions = self.get_type_suggestions(ifc_file)
elif ifc_filter.type == "material":
suggestions = self.get_material_suggestions(ifc_file)
elif ifc_filter.type == "location":
suggestions = self.get_location_suggestions(ifc_file)
elif ifc_filter.type == "group":
suggestions = self.get_group_suggestions(ifc_file)
elif ifc_filter.type == "classification":
suggestions = self.get_classification_suggestions(ifc_file)
elif ifc_filter.type == "parent":
suggestions = self.get_parent_suggestions(ifc_file)
elif ifc_filter.type == "instance":
suggestions = self.get_instance_suggestions(ifc_file)
elif ifc_filter.type == "attribute":
if self.suggestion_type == "attribute_name":
suggestions = self.get_attribute_names(ifc_file)
else:
suggestions = self.get_attribute_values(ifc_file, ifc_filter.name)
elif ifc_filter.type == "property":
if self.suggestion_type == "pset":
suggestions = self.get_property_sets(ifc_file)
elif self.suggestion_type == "property_name":
suggestions = self.get_property_names(ifc_file, ifc_filter.pset)
else:
suggestions = self.get_property_values(ifc_file, ifc_filter.pset, ifc_filter.name)
return suggestions
def build_hierarchy_path(self, element):
path = []
current = element
while current:
if hasattr(current, 'Name') and current.Name:
path.insert(0, current.Name)
parent = None
if hasattr(current, 'Decomposes') and current.Decomposes:
for rel in current.Decomposes:
if hasattr(rel, 'RelatingObject'):
parent = rel.RelatingObject
break
if not parent and hasattr(current, 'ContainedInStructure') and current.ContainedInStructure:
for rel in current.ContainedInStructure:
if hasattr(rel, 'RelatingStructure'):
parent = rel.RelatingStructure
break
current = parent
return path
def get_entity_suggestions(self, ifc_file):
all_classes = set()
schema = tool.Ifc.schema()
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
class_name = element.is_a()
try:
entity = schema.declaration_by_name(class_name).as_entity()
current = entity
chain_names = [class_name]
while current.supertype():
supertype = current.supertype()
chain_names.insert(0, supertype.name())
current = supertype
if len(chain_names) > 1:
all_classes.add(" > ".join(chain_names))
for i in range(len(chain_names) - 1):
superclass_chain = " > ".join(chain_names[:i+1])
all_classes.add(f"{superclass_chain} (superclass)")
else:
all_classes.add(class_name)
except:
all_classes.add(class_name)
except:
continue
return all_classes
def get_type_suggestions(self, ifc_file):
suggestions = set()
for element_type in ifc_file.by_type("IfcTypeObject"):
if element_type.Name:
hierarchy_path = self.build_hierarchy_path(element_type)
if len(hierarchy_path) > 1:
suggestions.add(" < ".join(hierarchy_path))
else:
suggestions.add(element_type.Name)
return suggestions
def get_material_suggestions(self, ifc_file):
suggestions = set()
for material in ifc_file.by_type("IfcMaterial"):
if material.Name:
suggestions.add(material.Name)
return suggestions
def get_location_suggestions(self, ifc_file):
suggestions = set()
for spatial in ifc_file.by_type("IfcSpatialStructureElement"):
if spatial.Name:
hierarchy_path = self.build_hierarchy_path(spatial)
if len(hierarchy_path) > 1:
suggestions.add(" < ".join(hierarchy_path))
else:
suggestions.add(spatial.Name)
return suggestions
def get_group_suggestions(self, ifc_file):
suggestions = set()
for group in ifc_file.by_type("IfcGroup"):
if group.Name:
hierarchy_path = self.build_hierarchy_path(group)
if len(hierarchy_path) > 1:
suggestions.add(" < ".join(hierarchy_path))
else:
suggestions.add(group.Name)
return suggestions
def get_classification_suggestions(self, ifc_file):
suggestions = set()
for ref in ifc_file.by_type("IfcClassificationReference"):
if ref.Identification:
suggestions.add(ref.Identification)
return suggestions
def get_parent_suggestions(self, ifc_file):
suggestions = set()
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
has_children = False
if hasattr(element, 'IsDecomposedBy') and element.IsDecomposedBy:
has_children = True
elif hasattr(element, 'ContainsElements') and element.ContainsElements:
has_children = True
elif hasattr(element, 'HasOpenings') and element.HasOpenings:
has_children = True
if has_children:
hierarchy_path = self.build_hierarchy_path(element)
element_class = element.is_a()
if len(hierarchy_path) > 0:
hierarchy_str = " < ".join(hierarchy_path)
suggestions.add(f"{hierarchy_str} ({element_class})")
else:
element_name = element.Name if hasattr(element, 'Name') and element.Name else element_class
suggestions.add(f"{element_name} ({element_class})")
except:
continue
return suggestions
def get_instance_suggestions(self, ifc_file):
suggestions = set()
element_data = []
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
if hasattr(element, 'GlobalId') and element.GlobalId:
hierarchy_path = self.build_hierarchy_path(element)
element_class = element.is_a()
if len(hierarchy_path) > 0:
hierarchy_str = " < ".join(hierarchy_path)
display_str = f"{hierarchy_str} ({element_class})"
else:
display_str = f"({element_class})"
element_data.append((display_str, element.GlobalId))
except:
continue
display_counts = {}
for display_str, global_id in element_data:
display_counts[display_str] = display_counts.get(display_str, 0) + 1
for display_str, global_id in element_data:
if display_counts[display_str] > 1:
suggestions.add(f"{display_str}: {global_id}")
else:
suggestions.add(display_str)
return suggestions
def get_property_sets(self, ifc_file):
psets = set()
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
for definition in getattr(element, 'IsDefinedBy', []):
if definition.is_a('IfcRelDefinesByProperties'):
pset = definition.RelatingPropertyDefinition
if pset.is_a("IfcPropertySet") and pset.Name:
psets.add(pset.Name)
except:
continue
return psets
def get_property_names(self, ifc_file, pset_name):
property_names = set()
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
for definition in getattr(element, 'IsDefinedBy', []):
if definition.is_a('IfcRelDefinesByProperties'):
pset = definition.RelatingPropertyDefinition
if pset.is_a("IfcPropertySet") and pset.Name == pset_name:
if pset.HasProperties:
for prop in pset.HasProperties:
if hasattr(prop, "Name") and prop.Name:
property_names.add(prop.Name)
except:
continue
return property_names
def get_property_values(self, ifc_file, pset_name, property_name):
property_values = set()
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
for definition in getattr(element, 'IsDefinedBy', []):
if definition.is_a('IfcRelDefinesByProperties'):
pset = definition.RelatingPropertyDefinition
if pset.is_a("IfcPropertySet") and pset.Name == pset_name:
if pset.HasProperties:
for prop in pset.HasProperties:
if hasattr(prop, "Name") and prop.Name == property_name:
if hasattr(prop, "NominalValue") and prop.NominalValue:
try:
value = prop.NominalValue.wrappedValue
if value is not None and value != "":
if not hasattr(value, 'is_a') and not isinstance(value, (tuple, list)):
str_value = str(value)
if not str_value.startswith("#") and not str_value.startswith("("):
property_values.add(str_value)
except:
continue
except:
continue
return property_values
def get_attribute_names(self, ifc_file):
attribute_names = set()
schema = tool.Ifc.schema()
ifc_classes = set()
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
ifc_classes.add(element.is_a())
except:
continue
for ifc_class in ifc_classes:
try:
entity = schema.declaration_by_name(ifc_class).as_entity()
attributes = entity.all_attributes()
for attr in attributes:
attribute_names.add(attr.name())
except:
continue
return attribute_names
def get_attribute_values(self, ifc_file, attribute_name):
attribute_values = set()
for element_id in IfcStore.id_map.keys():
try:
element = ifc_file.by_id(element_id)
if element.is_a("IfcRelationship") or element.is_a("IfcTypeObject"):
continue
if hasattr(element, attribute_name):
value = getattr(element, attribute_name, None)
if value is not None and value != "":
if not hasattr(value, 'is_a') and not isinstance(value, (tuple, list)):
str_value = str(value)
if not str_value.startswith("#") and not str_value.startswith("("):
attribute_values.add(str_value)
except:
continue
return attribute_values
class AddFilterGroup(Operator):
bl_idname = "bim.add_filter_group"
bl_label = "Add Filter Group"
@@ -96,11 +559,38 @@ class AddFilter(Operator):
def execute(self, context):
filter_groups = tool.Search.get_filter_groups(self.module)
if self.index >= len(filter_groups):
filter_groups.add()
new = filter_groups[self.index].filters.add()
new.type = self.type
return {"FINISHED"}
class ToggleFilterInclusion(Operator):
bl_idname = "bim.toggle_filter_inclusion"
bl_label = "Toggle Filter Mode"
bl_description = "Cycle between Add (+), Subtract (-), and Filter modes for this filter"
bl_options = {"REGISTER", "UNDO"}
group_index: IntProperty()
filter_index: IntProperty()
module: StringProperty()
def execute(self, context):
filter_groups = tool.Search.get_filter_groups(self.module)
filter_group = filter_groups[self.group_index]
ifc_filter = filter_group.filters[self.filter_index]
if ifc_filter.filter_mode == "ADD":
ifc_filter.filter_mode = "SUBTRACT"
elif ifc_filter.filter_mode == "SUBTRACT":
ifc_filter.filter_mode = "FILTER"
else:
ifc_filter.filter_mode = "ADD"
return {"FINISHED"}
class SelectFilterElements(bpy.types.Operator):
bl_idname = "bim.select_filter_elements"
bl_label = "Select Filter Elements"
@@ -120,6 +610,47 @@ class SelectFilterElements(bpy.types.Operator):
return {"FINISHED"}
class ApplyFilterFromText(Operator, tool.Ifc.Operator):
bl_idname = "bim.apply_filter_from_text"
bl_label = "Apply Filter Configuration"
bl_description = "Apply the JSON filter configuration from the current text block"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if context.area and context.area.type == 'TEXT_EDITOR':
space = context.space_data
if space.text and space.text.name.startswith("FilterQuery_"):
return True
return False
def execute(self, context):
space = context.space_data
text = space.text
if not text or not text.name.startswith("FilterQuery_"):
self.report({"ERROR"}, "No valid filter configuration text block")
return {'CANCELLED'}
module = text.name.replace("FilterQuery_", "")
try:
json_data = json.loads(text.as_string())
filter_structure = json_data.get("filter_structure", [])
filter_groups = tool.Search.get_filter_groups(module)
tool.Search.import_filter_structure(filter_structure, filter_groups)
self.report({"INFO"}, "Filter configuration applied successfully")
if len(context.window_manager.windows) > 1:
bpy.ops.wm.window_close()
except Exception as e:
self.report({"ERROR"}, f"Invalid JSON: {str(e)}")
return {'CANCELLED'}
return {'FINISHED'}
class EditFilterQuery(Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_filter_query"
bl_label = "Edit Filter Query"
@@ -127,29 +658,84 @@ class EditFilterQuery(Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
query: StringProperty(name="Query")
old_query: StringProperty(name="Old Query")
module: StringProperty()
module: StringProperty(default="search")
def _execute(self, context):
if self.query == self.old_query:
return
module = getattr(self, "module", "search")
if not tool.Blender.get_addon_preferences().chain_filter_with_set_operations:
if self.query == self.old_query:
return
filter_groups = tool.Search.get_filter_groups(self.module)
try:
tool.Search.import_filter_query(self.query, filter_groups)
except:
return
filter_groups = tool.Search.get_filter_groups(module)
try:
tool.Search.import_filter_query(self.query, filter_groups)
except:
return
def draw(self, context):
row = self.layout.row()
row.prop(self, "query", text="")
if not tool.Blender.get_addon_preferences().chain_filter_with_set_operations:
row = self.layout.row()
row.prop(self, "query", text="")
def invoke(self, context, event):
filter_groups = tool.Search.get_filter_groups(self.module)
module = getattr(self, "module", "search")
filter_groups = tool.Search.get_filter_groups(module)
self.query = tool.Search.export_filter_query(filter_groups)
self.old_query = self.query
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations:
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)
query = tool.Search.export_filter_query(filter_groups)
json_data = {
"type": "BBIM_Search",
"query": query,
"filter_structure": filter_structure
}
text_block_name = f"FilterQuery_{module}"
text = bpy.data.texts.get(text_block_name)
if not text:
text = bpy.data.texts.new(text_block_name)
text.clear()
text.write(json.dumps(json_data, indent=2))
bpy.ops.wm.window_new()
new_window = context.window_manager.windows[-1]
new_area = new_window.screen.areas[0]
new_area.type = 'TEXT_EDITOR'
text_space = None
for space in new_area.spaces:
if space.type == 'TEXT_EDITOR':
text_space = space
break
if text_space:
text_space.text = text
self.report({"INFO"}, "Compact text editor opened. Edit JSON and click 'Apply Filter Configuration' in header")
return {'FINISHED'}
return context.window_manager.invoke_props_dialog(self)
else:
self.query = tool.Search.export_filter_query(filter_groups)
self.old_query = self.query
return context.window_manager.invoke_props_dialog(self, width=400)
class Search(Operator):
@@ -174,9 +760,23 @@ class Search(Operator):
else:
assert_never(self.property_group)
results = ifcopenshell.util.selector.filter_elements(
tool.Ifc.get(), tool.Search.export_filter_query(props.filter_groups)
)
preferences = tool.Blender.get_addon_preferences()
# Migrate old ! prefix filters to new filter_mode system when preferences are enabled
if preferences.chain_filter_with_set_operations or preferences.default_filter_with_set_operations_for_globalid_and_class:
for filter_group in props.filter_groups:
for ifc_filter in filter_group.filters:
if ifc_filter.type not in ["entity", "instance"]:
continue
if ifc_filter.value.startswith("!"):
ifc_filter.value = ifc_filter.value[1:]
ifc_filter.filter_mode = "SUBTRACT"
results = tool.Search.execute_filter_groups(props.filter_groups)
else:
results = ifcopenshell.util.selector.filter_elements(
tool.Ifc.get(), tool.Search.export_filter_query(props.filter_groups)
)
objs = [obj for e in results if isinstance(obj := tool.Ifc.get_object(e), bpy.types.Object)]
for obj in objs:
@@ -242,13 +842,32 @@ class SaveSearch(Operator, tool.Ifc.Operator):
try:
query = tool.Search.export_filter_query(filter_groups)
results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query)
results = tool.Search.execute_filter_groups(filter_groups)
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)
except:
print(traceback.format_exc())
self.report({"ERROR"}, "Error occurred trying save search.")
return
description = json.dumps({"type": "BBIM_Search", "query": query})
description = json.dumps({
"type": "BBIM_Search",
"query": query,
"filter_structure": filter_structure
})
ifc_file = tool.Ifc.get()
group = next(
(
@@ -289,7 +908,12 @@ class LoadSearch(Operator, tool.Ifc.Operator):
filter_groups = tool.Search.get_filter_groups(self.module)
props = tool.Search.get_search_props()
group = tool.Ifc.get().by_id(int(props.saved_searches))
tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups)
group_data = tool.Search.get_group_data(group)
if group_data and "filter_structure" in group_data:
tool.Search.import_filter_structure(group_data["filter_structure"], filter_groups)
else:
tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups)
def draw(self, context):
assert self.layout
@@ -302,6 +926,40 @@ class LoadSearch(Operator, tool.Ifc.Operator):
return context.window_manager.invoke_props_dialog(self)
class RemoveSearch(Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_search"
bl_label = "Remove Search"
bl_description = "Remove a saved search filter"
bl_options = {"REGISTER", "UNDO"}
module: StringProperty()
def _execute(self, context):
props = tool.Search.get_search_props()
group_id = props.saved_searches
if not group_id:
self.report({"ERROR"}, "No search selected for removal")
return
group = tool.Ifc.get().by_id(int(group_id))
group_name = group.Name or "Unnamed"
ifcopenshell.api.group.remove_group(tool.Ifc.get(), group=group)
tool.Search.patch_search_ifcgroups()
self.report({"INFO"}, f"Removed saved search: {group_name}")
def draw(self, context):
self.layout.label(text="Select search to remove:", icon="ERROR")
row = self.layout.row()
props = tool.Search.get_search_props()
row.prop(props, "saved_searches", text="")
def invoke(self, context, event):
tool.Search.patch_search_ifcgroups()
from bonsai.bim.module.search.data import SearchData
if not SearchData.is_loaded:
SearchData.load()
return context.window_manager.invoke_props_dialog(self)
class ColourByProperty(Operator):
bl_idname = "bim.colour_by_property"
bl_label = "Colour by Property"
+10
View File
@@ -748,6 +748,15 @@ class BIMFacet(PropertyGroup):
pset: StringProperty(name="Pset")
value: StringProperty(name="Value")
type: StringProperty(name="Type")
filter_mode: EnumProperty(
name="Filter Mode",
items=[
("ADD", "Add", "Add elements to the result set (query entire IFC file)", "ADD", 0),
("SUBTRACT", "Subtract", "Subtract matching elements from previous results", "REMOVE", 1),
("FILTER", "Filter", "Filter down previous results to matching elements", "FILTER", 2),
],
default="ADD",
)
comparison: EnumProperty(
items=[
("=", "equal to", ""),
@@ -765,6 +774,7 @@ class BIMFacet(PropertyGroup):
pset: str
value: str
type: str
filter_mode: Literal["ADD", "SUBTRACT", "FILTER"]
comparison: Literal["=", "!=", ">=", "<=", ">", "<", "*=", "!*="]
+21
View File
@@ -689,6 +689,17 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
description="Show mass and time units section in the new project wizard panel",
default=False,
)
chain_filter_with_set_operations: BoolProperty(
name="NEW filter mode: Enable chained filters with set operations",
description="Enable chaining search filters with set operations: ADD (union: combine sets), SUBTRACT (difference: remove from set), FILTER (intersection: only elements in both sets), with autocomplete suggestions for filter values",
default=False,
)
default_filter_with_set_operations_for_globalid_and_class: BoolProperty(
name="DEFAULT filter mode: Enable set operations for GlobalId/Class",
description="Enable ADD/SUBTRACT/FILTER toggle buttons on entity (Class) and instance (GlobalId) filters for the DEFAULT filter mode",
default=False,
)
if TYPE_CHECKING:
svg2pdf_command: str
@@ -727,6 +738,8 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
default_parameters: DefaultParameters
container_hide_show_isolate: bool
mass_time_units_in_wizard: bool
chain_filter_with_set_operations: bool
default_filter_with_set_operations_for_globalid_and_class: bool
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
@@ -909,6 +922,14 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "container_hide_show_isolate")
layout.prop(self, "mass_time_units_in_wizard")
layout.label(text="Filtering modes:")
box = layout.box()
row = box.row(align=True)
row.prop(self, "chain_filter_with_set_operations")
row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/3270"
row = box.row(align=True)
row.prop(self, "default_filter_with_set_operations_for_globalid_and_class")
row.operator("bim.open_uri", text="", icon="HELP").uri = "https://community.osarch.org/discussion/comment/27030"
# Scene panel groups
+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
+279 -50
View File
@@ -42,6 +42,38 @@ class Search(bonsai.core.tool.Search):
def get_group_query(cls, group: ifcopenshell.entity_instance) -> str:
return json.loads(group.Description)["query"]
@classmethod
def get_group_data(cls, group: ifcopenshell.entity_instance) -> dict:
return json.loads(group.Description)
@classmethod
def import_filter_structure(
cls, filter_structure: list, filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]
) -> None:
filter_groups.clear()
for group_data in filter_structure:
if not isinstance(group_data, list):
continue
filter_group = filter_groups.add()
for filter_data in group_data:
if not isinstance(filter_data, dict):
continue
ifc_filter = filter_group.filters.add()
ifc_filter.type = filter_data.get("type", "")
ifc_filter.name = filter_data.get("name", "")
ifc_filter.value = filter_data.get("value", "")
ifc_filter.pset = filter_data.get("pset", "")
ifc_filter.comparison = filter_data.get("comparison", "=")
filter_mode = filter_data.get("filter_mode", "ADD")
if filter_mode in ["ADD", "SUBTRACT", "FILTER"]:
ifc_filter.filter_mode = filter_mode
else:
ifc_filter.filter_mode = "ADD"
FilterModule = Union[Literal["search", "csv", "diff", "drawing_include", "drawing_exclude"], str]
@classmethod
@@ -80,53 +112,244 @@ class Search(bonsai.core.tool.Search):
for ifc_filter in filter_group.filters:
if not ifc_filter.value:
continue
if ifc_filter.type == "instance":
if "bpy.data.texts" in ifc_filter.value:
data_name = ifc_filter.value.split("bpy.data.texts")[1][2:-2]
filter_group_query.append(bpy.data.texts[data_name].as_string())
else:
filter_group_query.append(ifc_filter.value)
elif ifc_filter.type == "entity":
filter_group_query.append(ifc_filter.value)
elif ifc_filter.type == "attribute":
if not ifc_filter.name:
continue
comparison, value = cls.get_comparison_and_value(ifc_filter)
filter_group_query.append(f"{ifc_filter.name}{comparison}{value}")
elif ifc_filter.type == "type":
comparison, value = cls.get_comparison_and_value(ifc_filter)
filter_group_query.append(f"type{comparison}{value}")
elif ifc_filter.type == "material":
comparison, value = cls.get_comparison_and_value(ifc_filter)
filter_group_query.append(f"material{comparison}{value}")
elif ifc_filter.type == "property":
if not ifc_filter.pset or not ifc_filter.name:
continue
pset = cls.wrap_value(ifc_filter, ifc_filter.pset)
name = cls.wrap_value(ifc_filter, ifc_filter.name)
comparison = ifc_filter.comparison
value = cls.wrap_value(ifc_filter, ifc_filter.value)
filter_group_query.append(f"{pset}.{name} {comparison} {value}")
elif ifc_filter.type == "classification":
comparison, value = cls.get_comparison_and_value(ifc_filter)
filter_group_query.append(f"classification{comparison}{value}")
elif ifc_filter.type == "location":
comparison, value = cls.get_comparison_and_value(ifc_filter)
filter_group_query.append(f"location{comparison}{value}")
elif ifc_filter.type == "group":
comparison, value = cls.get_comparison_and_value(ifc_filter)
filter_group_query.append(f"group{comparison}{value}")
elif ifc_filter.type == "parent":
comparison, value = cls.get_comparison_and_value(ifc_filter)
filter_group_query.append(f"parent{comparison}{value}")
elif ifc_filter.type == "query":
keys = cls.wrap_value(ifc_filter, ifc_filter.name)
comparison = ifc_filter.comparison or "="
value = cls.wrap_value(ifc_filter, ifc_filter.value)
filter_group_query.append(f"query:{keys}{comparison}{value}")
query.append(", ".join(filter_group_query))
query_part = cls._export_single_filter(ifc_filter)
if query_part:
filter_group_query.append(query_part)
if filter_group_query:
query.append(", ".join(filter_group_query))
return " + ".join(query)
@classmethod
def _export_single_filter(cls, ifc_filter: BIMFacet) -> str:
preferences = tool.Blender.get_addon_preferences()
if ifc_filter.type == "instance":
if "bpy.data.texts" in ifc_filter.value:
data_name = ifc_filter.value.split("bpy.data.texts")[1][2:-2]
value = bpy.data.texts[data_name].as_string()
else:
value = ifc_filter.value
if preferences.chain_filter_with_set_operations or preferences.default_filter_with_set_operations_for_globalid_and_class:
value = value.lstrip("!")
if ifc_filter.filter_mode == "SUBTRACT":
value = f"!{value}"
return value
elif ifc_filter.type == "entity":
value = ifc_filter.value
if preferences.chain_filter_with_set_operations or preferences.default_filter_with_set_operations_for_globalid_and_class:
value = value.lstrip("!")
if ifc_filter.filter_mode == "SUBTRACT":
value = f"!{value}"
return value
elif ifc_filter.type == "attribute":
if not ifc_filter.name:
return ""
comparison, value = cls.get_comparison_and_value(ifc_filter)
return f"{ifc_filter.name}{comparison}{value}"
elif ifc_filter.type == "type":
comparison, value = cls.get_comparison_and_value(ifc_filter)
return f"type{comparison}{value}"
elif ifc_filter.type == "material":
comparison, value = cls.get_comparison_and_value(ifc_filter)
return f"material{comparison}{value}"
elif ifc_filter.type == "property":
if not ifc_filter.pset or not ifc_filter.name:
return ""
pset = cls.wrap_value(ifc_filter, ifc_filter.pset)
name = cls.wrap_value(ifc_filter, ifc_filter.name)
comparison = ifc_filter.comparison
value = cls.wrap_value(ifc_filter, ifc_filter.value)
return f"{pset}.{name} {comparison} {value}"
elif ifc_filter.type == "classification":
comparison, value = cls.get_comparison_and_value(ifc_filter)
return f"classification{comparison}{value}"
elif ifc_filter.type == "location":
comparison, value = cls.get_comparison_and_value(ifc_filter)
return f"location{comparison}{value}"
elif ifc_filter.type == "group":
comparison, value = cls.get_comparison_and_value(ifc_filter)
return f"group{comparison}{value}"
elif ifc_filter.type == "parent":
comparison, value = cls.get_comparison_and_value(ifc_filter)
return f"parent{comparison}{value}"
elif ifc_filter.type == "query":
keys = cls.wrap_value(ifc_filter, ifc_filter.name)
comparison = ifc_filter.comparison or "="
value = cls.wrap_value(ifc_filter, ifc_filter.value)
return f"query:{keys}{comparison}{value}"
return ""
@classmethod
def execute_filter_groups(cls, filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]) -> set:
"""
Execute filter groups with simplified chaining support.
Within a single group chain, all filters chain sequentially with ADD/SUBTRACT/FILTER modes.
Groups are combined with union (same as original " + " behavior).
"""
preferences = tool.Blender.get_addon_preferences()
all_group_results = []
for group_idx, filter_group in enumerate(filter_groups):
group_results = set()
for filter_index, ifc_filter in enumerate(filter_group.filters):
if not ifc_filter.value:
continue
query = cls._export_single_filter(ifc_filter)
if not query:
continue
if filter_index == 0:
mode = "ADD"
else:
if ifc_filter.type in ["entity", "instance"]:
if preferences.chain_filter_with_set_operations or preferences.default_filter_with_set_operations_for_globalid_and_class:
mode = ifc_filter.filter_mode
else:
mode = "FILTER" if group_results else "ADD"
else:
if preferences.chain_filter_with_set_operations:
mode = ifc_filter.filter_mode
else:
mode = "FILTER" if group_results else "ADD"
if mode == "ADD":
results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query)
group_results.update(results)
elif mode == "SUBTRACT":
if group_results:
query_without_prefix = query[1:] if query.startswith("!") else query
elements_to_remove = ifcopenshell.util.selector.filter_elements(
tool.Ifc.get(), query_without_prefix, elements=group_results
)
group_results -= elements_to_remove
else:
results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query)
group_results.update(results)
elif mode == "FILTER":
if group_results:
results = ifcopenshell.util.selector.filter_elements(
tool.Ifc.get(), query, elements=group_results
)
group_results = results
else:
results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query)
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 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
@@ -349,7 +572,8 @@ class ImportFilterQueryTransformer(lark.Transformer):
def facet_list(self, args):
new = self.filter_groups.add()
global_ids = []
for arg in args:
is_first_group = len(self.filter_groups) == 1
for filter_index, arg in enumerate(args):
if arg["type"] == "instance" and global_ids:
if "bpy.data.texts" in new2.value:
data_name = new2.value.split("bpy.data.texts")[1][2:-2]
@@ -374,21 +598,26 @@ class ImportFilterQueryTransformer(lark.Transformer):
new2.pset = arg["pset"]
if "comparison" in arg:
new2.comparison = arg["comparison"] or "="
if "filter_mode" in arg:
new2.filter_mode = arg["filter_mode"]
elif not is_first_group and filter_index == 0 and arg["type"] in ("entity", "instance"):
if arg.get("filter_mode", "ADD") != "SUBTRACT":
new2.filter_mode = "FILTER"
def facet(self, args):
return args[0]
def instance(self, args):
if args[0].data == "not":
return {"type": "instance", "value": "!" + args[1].children[0].value}
return {"type": "instance", "value": args[1].children[0].value, "filter_mode": "SUBTRACT"}
else:
return {"type": "instance", "value": args[0].children[0].value}
return {"type": "instance", "value": args[0].children[0].value, "filter_mode": "ADD"}
def entity(self, args):
if args[0].data == "not":
return {"type": "entity", "value": "!" + args[1].children[0].value}
return {"type": "entity", "value": args[1].children[0].value, "filter_mode": "SUBTRACT"}
else:
return {"type": "entity", "value": args[0].children[0].value}
return {"type": "entity", "value": args[0].children[0].value, "filter_mode": "ADD"}
def attribute(self, args):
name, comparison, value = args