First commit for a simplified searching operation with suggestions

This commit is contained in:
falken10vdl
2025-12-16 17:13:07 +01:00
parent 33ba7c45b9
commit ce35903459
6 changed files with 953 additions and 57 deletions
+217 -1
View File
@@ -489,6 +489,8 @@ def draw_filter(
data.load()
sprops = tool.Search.get_search_props()
preferences = tool.Blender.get_addon_preferences()
enable_suggestions = getattr(preferences, "search_filter_suggestions", False)
if tool.Ifc.get():
row = layout.row(align=True)
@@ -498,6 +500,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"] and enable_suggestions:
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,7 +509,15 @@ 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
if not enable_suggestions:
row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD").module = module
else:
# When suggestions are enabled, show a simple "Add Filter" button if no filters exist
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
row.operator("bim.edit_filter_query", text="", icon="FILTER").module = module
for i, filter_group in enumerate(filter_groups):
@@ -524,43 +536,247 @@ def draw_filter(
for j, ifc_filter in enumerate(filter_group.filters):
if ifc_filter.type == "entity":
row = box.row(align=True)
if enable_suggestions 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_3D")
if enable_suggestions:
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 enable_suggestions 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")
if enable_suggestions:
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 enable_suggestions 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 = "attribute_value"
elif ifc_filter.type == "type":
row = box.row(align=True)
if enable_suggestions 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")
if enable_suggestions:
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 enable_suggestions 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")
if enable_suggestions:
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 enable_suggestions 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")
if enable_suggestions:
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 enable_suggestions and 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 enable_suggestions and 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 enable_suggestions 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")
if enable_suggestions:
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 enable_suggestions 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")
if enable_suggestions:
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 enable_suggestions 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")
if enable_suggestions:
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 enable_suggestions 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")
if enable_suggestions:
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 enable_suggestions 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)
if enable_suggestions 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="GRIP")
if enable_suggestions:
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
@@ -26,10 +26,12 @@ classes = (
operator.AddFilterGroup,
operator.ColourByProperty,
operator.EditFilterQuery,
operator.FilterValueSuggestions,
operator.LoadColourscheme,
operator.LoadSearch,
operator.RemoveFilter,
operator.RemoveFilterGroup,
operator.RemoveSearch,
operator.ResetObjectColours,
operator.SaveColourscheme,
operator.SaveSearch,
@@ -40,6 +42,7 @@ classes = (
operator.SelectIfcClass,
operator.SelectSimilar,
operator.ShowAllElements,
operator.ToggleFilterInclusion,
operator.ToggleFilterSelection,
prop.BIMColour,
prop.BIMFilterItem,
+553 -6
View File
@@ -40,9 +40,465 @@ from bpy.props import (
FloatVectorProperty,
CollectionProperty,
)
from bonsai.bim.prop import StrProperty
from typing import TYPE_CHECKING, Literal, get_args, assert_never
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()
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=300)
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 +552,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"
@@ -174,9 +657,15 @@ 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()
enable_suggestions = getattr(preferences, "search_filter_suggestions", False)
if enable_suggestions:
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 +731,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 +797,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 +815,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["=", "!=", ">=", "<=", ">", "<", "*=", "!*="]
+8
View File
@@ -689,6 +689,12 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
description="Show mass and time units section in the new project wizard panel",
default=False,
)
search_filter_suggestions: BoolProperty(
name="Enable advanced search with filter suggestions",
description="Enable filter mode (ADD/SUBTRACT/FILTER) and value suggestions for search filters",
default=False,
)
if TYPE_CHECKING:
svg2pdf_command: str
@@ -727,6 +733,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
default_parameters: DefaultParameters
container_hide_show_isolate: bool
mass_time_units_in_wizard: bool
search_filter_suggestions: bool
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
@@ -909,6 +916,7 @@ 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.prop(self, "search_filter_suggestions")
# Scene panel groups
+162 -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,127 @@ 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:
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
value = value.lstrip("!")
if ifc_filter.filter_mode == "SUBTRACT":
value = f"!{value}"
return value
elif ifc_filter.type == "entity":
value = ifc_filter.value
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).
"""
all_group_results = []
for filter_group in 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
mode = "ADD" if filter_index == 0 else ifc_filter.filter_mode
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 get_comparison_and_value(
cls, ifc_filter: BIMFacet
@@ -349,7 +455,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 +481,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