mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-25 17:57:02 +00:00
Merge pull request #7498 from falken10vdl/New-search-tool
New search tool interface. This one supersedes #7315. Cheers!
This commit is contained in:
@@ -498,6 +498,8 @@ def draw_filter(
|
|||||||
if data.data["saved_searches"]:
|
if data.data["saved_searches"]:
|
||||||
row.operator("bim.load_search", text="", icon="IMPORT").module = module
|
row.operator("bim.load_search", text="", icon="IMPORT").module = module
|
||||||
row.operator("bim.save_search", text="", icon="EXPORT").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 != "search":
|
||||||
if module == "drawing_include":
|
if module == "drawing_include":
|
||||||
row.operator("bim.edit_element_filter", icon="CHECKMARK", text="").filter_mode = "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.edit_element_filter", icon="CHECKMARK", text="").filter_mode = "EXCLUDE"
|
||||||
row.operator("bim.enable_editing_element_filter", icon="CANCEL", text="").filter_mode = "NONE"
|
row.operator("bim.enable_editing_element_filter", icon="CANCEL", text="").filter_mode = "NONE"
|
||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD").module = module
|
if not tool.Blender.get_addon_preferences().chain_filter_with_set_operations:
|
||||||
row.operator("bim.edit_filter_query", text="", icon="FILTER").module = module
|
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):
|
for i, filter_group in enumerate(filter_groups):
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
@@ -524,43 +535,247 @@ def draw_filter(
|
|||||||
for j, ifc_filter in enumerate(filter_group.filters):
|
for j, ifc_filter in enumerate(filter_group.filters):
|
||||||
if ifc_filter.type == "entity":
|
if ifc_filter.type == "entity":
|
||||||
row = box.row(align=True)
|
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")
|
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":
|
elif ifc_filter.type == "attribute":
|
||||||
row = box.row(align=True)
|
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")
|
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="")
|
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":
|
elif ifc_filter.type == "type":
|
||||||
row = box.row(align=True)
|
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")
|
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":
|
elif ifc_filter.type == "material":
|
||||||
row = box.row(align=True)
|
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")
|
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":
|
elif ifc_filter.type == "property":
|
||||||
row = box.row(align=True)
|
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")
|
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="")
|
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, "comparison", text="")
|
||||||
row.prop(ifc_filter, "value", 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":
|
elif ifc_filter.type == "classification":
|
||||||
row = box.row(align=True)
|
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")
|
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":
|
elif ifc_filter.type == "location":
|
||||||
row = box.row(align=True)
|
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")
|
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":
|
elif ifc_filter.type == "group":
|
||||||
row = box.row(align=True)
|
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")
|
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":
|
elif ifc_filter.type == "parent":
|
||||||
row = box.row(align=True)
|
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")
|
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":
|
elif ifc_filter.type == "query":
|
||||||
row = box.row(align=True)
|
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, "name", text="", icon="POINTCLOUD_DATA")
|
||||||
row.prop(ifc_filter, "comparison", text="")
|
row.prop(ifc_filter, "comparison", text="")
|
||||||
row.prop(ifc_filter, "value", text="")
|
row.prop(ifc_filter, "value", text="")
|
||||||
elif ifc_filter.type == "instance":
|
elif ifc_filter.type == "instance":
|
||||||
row = box.row(align=True)
|
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")
|
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 = row.operator("bim.select_filter_elements", text="", icon="EYEDROPPER")
|
||||||
op.group_index = i
|
op.group_index = i
|
||||||
op.index = j
|
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()):
|
if query := ifcopenshell.util.element.get_pset(element, "EPset_Drawing", self.filter_mode.title()):
|
||||||
filter_groups = tool.Search.get_filter_groups(f"drawing_{self.filter_mode.lower()}")
|
filter_groups = tool.Search.get_filter_groups(f"drawing_{self.filter_mode.lower()}")
|
||||||
try:
|
try:
|
||||||
tool.Search.import_filter_query(query, filter_groups)
|
data = json.loads(query)
|
||||||
except:
|
if isinstance(data, dict) and "filter_structure" in data:
|
||||||
|
tool.Search.import_filter_structure(data["filter_structure"], filter_groups)
|
||||||
|
else:
|
||||||
|
tool.Search.import_filter_query(query, filter_groups)
|
||||||
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -3701,12 +3705,45 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
assert element
|
assert element
|
||||||
pset = tool.Pset.get_element_pset(element, "EPset_Drawing")
|
pset = tool.Pset.get_element_pset(element, "EPset_Drawing")
|
||||||
assert pset
|
assert pset
|
||||||
|
|
||||||
if self.filter_mode == "INCLUDE":
|
if self.filter_mode == "INCLUDE":
|
||||||
query = tool.Search.export_filter_query(props.include_filter_groups) or None
|
filter_groups = props.include_filter_groups
|
||||||
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Include": query})
|
|
||||||
elif self.filter_mode == "EXCLUDE":
|
elif self.filter_mode == "EXCLUDE":
|
||||||
query = tool.Search.export_filter_query(props.exclude_filter_groups) or None
|
filter_groups = props.exclude_filter_groups
|
||||||
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Exclude": query})
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
query = tool.Search.export_filter_query(filter_groups) or None
|
||||||
|
|
||||||
|
if 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"
|
props.filter_mode = "NONE"
|
||||||
bpy.ops.bim.activate_drawing(drawing=element.id(), should_view_from_camera=False)
|
bpy.ops.bim.activate_drawing(drawing=element.id(), should_view_from_camera=False)
|
||||||
|
|
||||||
|
|||||||
@@ -24,12 +24,15 @@ classes = (
|
|||||||
operator.ActivateIfcClassFilter,
|
operator.ActivateIfcClassFilter,
|
||||||
operator.AddFilter,
|
operator.AddFilter,
|
||||||
operator.AddFilterGroup,
|
operator.AddFilterGroup,
|
||||||
|
operator.ApplyFilterFromText,
|
||||||
operator.ColourByProperty,
|
operator.ColourByProperty,
|
||||||
operator.EditFilterQuery,
|
operator.EditFilterQuery,
|
||||||
|
operator.FilterValueSuggestions,
|
||||||
operator.LoadColourscheme,
|
operator.LoadColourscheme,
|
||||||
operator.LoadSearch,
|
operator.LoadSearch,
|
||||||
operator.RemoveFilter,
|
operator.RemoveFilter,
|
||||||
operator.RemoveFilterGroup,
|
operator.RemoveFilterGroup,
|
||||||
|
operator.RemoveSearch,
|
||||||
operator.ResetObjectColours,
|
operator.ResetObjectColours,
|
||||||
operator.SaveColourscheme,
|
operator.SaveColourscheme,
|
||||||
operator.SaveSearch,
|
operator.SaveSearch,
|
||||||
@@ -40,6 +43,7 @@ classes = (
|
|||||||
operator.SelectIfcClass,
|
operator.SelectIfcClass,
|
||||||
operator.SelectSimilar,
|
operator.SelectSimilar,
|
||||||
operator.ShowAllElements,
|
operator.ShowAllElements,
|
||||||
|
operator.ToggleFilterInclusion,
|
||||||
operator.ToggleFilterSelection,
|
operator.ToggleFilterSelection,
|
||||||
prop.BIMColour,
|
prop.BIMColour,
|
||||||
prop.BIMFilterItem,
|
prop.BIMFilterItem,
|
||||||
@@ -55,7 +59,9 @@ classes = (
|
|||||||
|
|
||||||
def register():
|
def register():
|
||||||
bpy.types.Scene.BIMSearchProperties = bpy.props.PointerProperty(type=prop.BIMSearchProperties)
|
bpy.types.Scene.BIMSearchProperties = bpy.props.PointerProperty(type=prop.BIMSearchProperties)
|
||||||
|
bpy.types.TEXT_HT_header.append(operator.draw_text_editor_header)
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
del bpy.types.Scene.BIMSearchProperties
|
del bpy.types.Scene.BIMSearchProperties
|
||||||
|
bpy.types.TEXT_HT_header.remove(operator.draw_text_editor_header)
|
||||||
|
|||||||
@@ -40,9 +40,472 @@ from bpy.props import (
|
|||||||
FloatVectorProperty,
|
FloatVectorProperty,
|
||||||
CollectionProperty,
|
CollectionProperty,
|
||||||
)
|
)
|
||||||
|
from bonsai.bim.prop import StrProperty
|
||||||
from typing import TYPE_CHECKING, Literal, get_args, assert_never
|
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):
|
class AddFilterGroup(Operator):
|
||||||
bl_idname = "bim.add_filter_group"
|
bl_idname = "bim.add_filter_group"
|
||||||
bl_label = "Add Filter Group"
|
bl_label = "Add Filter Group"
|
||||||
@@ -96,11 +559,38 @@ class AddFilter(Operator):
|
|||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
filter_groups = tool.Search.get_filter_groups(self.module)
|
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 = filter_groups[self.index].filters.add()
|
||||||
new.type = self.type
|
new.type = self.type
|
||||||
return {"FINISHED"}
|
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):
|
class SelectFilterElements(bpy.types.Operator):
|
||||||
bl_idname = "bim.select_filter_elements"
|
bl_idname = "bim.select_filter_elements"
|
||||||
bl_label = "Select Filter Elements"
|
bl_label = "Select Filter Elements"
|
||||||
@@ -120,6 +610,47 @@ class SelectFilterElements(bpy.types.Operator):
|
|||||||
return {"FINISHED"}
|
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):
|
class EditFilterQuery(Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.edit_filter_query"
|
bl_idname = "bim.edit_filter_query"
|
||||||
bl_label = "Edit Filter Query"
|
bl_label = "Edit Filter Query"
|
||||||
@@ -127,29 +658,84 @@ class EditFilterQuery(Operator, tool.Ifc.Operator):
|
|||||||
bl_options = {"REGISTER", "UNDO"}
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
query: StringProperty(name="Query")
|
query: StringProperty(name="Query")
|
||||||
old_query: StringProperty(name="Old Query")
|
old_query: StringProperty(name="Old Query")
|
||||||
module: StringProperty()
|
module: StringProperty(default="search")
|
||||||
|
|
||||||
def _execute(self, context):
|
def _execute(self, context):
|
||||||
if self.query == self.old_query:
|
module = getattr(self, "module", "search")
|
||||||
return
|
|
||||||
|
|
||||||
filter_groups = tool.Search.get_filter_groups(self.module)
|
if not tool.Blender.get_addon_preferences().chain_filter_with_set_operations:
|
||||||
try:
|
if self.query == self.old_query:
|
||||||
tool.Search.import_filter_query(self.query, filter_groups)
|
return
|
||||||
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):
|
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):
|
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)
|
if tool.Blender.get_addon_preferences().chain_filter_with_set_operations:
|
||||||
self.old_query = self.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)
|
||||||
|
|
||||||
return context.window_manager.invoke_props_dialog(self)
|
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'}
|
||||||
|
|
||||||
|
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):
|
class Search(Operator):
|
||||||
@@ -174,9 +760,23 @@ class Search(Operator):
|
|||||||
else:
|
else:
|
||||||
assert_never(self.property_group)
|
assert_never(self.property_group)
|
||||||
|
|
||||||
results = ifcopenshell.util.selector.filter_elements(
|
preferences = tool.Blender.get_addon_preferences()
|
||||||
tool.Ifc.get(), tool.Search.export_filter_query(props.filter_groups)
|
|
||||||
)
|
# 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)]
|
objs = [obj for e in results if isinstance(obj := tool.Ifc.get_object(e), bpy.types.Object)]
|
||||||
for obj in objs:
|
for obj in objs:
|
||||||
@@ -242,13 +842,32 @@ class SaveSearch(Operator, tool.Ifc.Operator):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
query = tool.Search.export_filter_query(filter_groups)
|
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:
|
except:
|
||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
self.report({"ERROR"}, "Error occurred trying save search.")
|
self.report({"ERROR"}, "Error occurred trying save search.")
|
||||||
return
|
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()
|
ifc_file = tool.Ifc.get()
|
||||||
group = next(
|
group = next(
|
||||||
(
|
(
|
||||||
@@ -289,7 +908,12 @@ class LoadSearch(Operator, tool.Ifc.Operator):
|
|||||||
filter_groups = tool.Search.get_filter_groups(self.module)
|
filter_groups = tool.Search.get_filter_groups(self.module)
|
||||||
props = tool.Search.get_search_props()
|
props = tool.Search.get_search_props()
|
||||||
group = tool.Ifc.get().by_id(int(props.saved_searches))
|
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):
|
def draw(self, context):
|
||||||
assert self.layout
|
assert self.layout
|
||||||
@@ -302,6 +926,40 @@ class LoadSearch(Operator, tool.Ifc.Operator):
|
|||||||
return context.window_manager.invoke_props_dialog(self)
|
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):
|
class ColourByProperty(Operator):
|
||||||
bl_idname = "bim.colour_by_property"
|
bl_idname = "bim.colour_by_property"
|
||||||
bl_label = "Colour by Property"
|
bl_label = "Colour by Property"
|
||||||
|
|||||||
@@ -748,6 +748,15 @@ class BIMFacet(PropertyGroup):
|
|||||||
pset: StringProperty(name="Pset")
|
pset: StringProperty(name="Pset")
|
||||||
value: StringProperty(name="Value")
|
value: StringProperty(name="Value")
|
||||||
type: StringProperty(name="Type")
|
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(
|
comparison: EnumProperty(
|
||||||
items=[
|
items=[
|
||||||
("=", "equal to", ""),
|
("=", "equal to", ""),
|
||||||
@@ -765,6 +774,7 @@ class BIMFacet(PropertyGroup):
|
|||||||
pset: str
|
pset: str
|
||||||
value: str
|
value: str
|
||||||
type: str
|
type: str
|
||||||
|
filter_mode: Literal["ADD", "SUBTRACT", "FILTER"]
|
||||||
comparison: Literal["=", "!=", ">=", "<=", ">", "<", "*=", "!*="]
|
comparison: Literal["=", "!=", ">=", "<=", ">", "<", "*=", "!*="]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -690,6 +690,17 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
|||||||
default=False,
|
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:
|
if TYPE_CHECKING:
|
||||||
svg2pdf_command: str
|
svg2pdf_command: str
|
||||||
svg2dxf_command: str
|
svg2dxf_command: str
|
||||||
@@ -727,6 +738,8 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
|||||||
default_parameters: DefaultParameters
|
default_parameters: DefaultParameters
|
||||||
container_hide_show_isolate: bool
|
container_hide_show_isolate: bool
|
||||||
mass_time_units_in_wizard: 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:
|
def draw(self, context: bpy.types.Context) -> None:
|
||||||
layout = self.layout
|
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:
|
def draw_extras_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||||
layout.prop(self, "container_hide_show_isolate")
|
layout.prop(self, "container_hide_show_isolate")
|
||||||
layout.prop(self, "mass_time_units_in_wizard")
|
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
|
# Scene panel groups
|
||||||
|
|||||||
@@ -2277,7 +2277,16 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {})
|
pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {})
|
||||||
include = pset.get("Include", None)
|
include = pset.get("Include", None)
|
||||||
if include:
|
if include:
|
||||||
elements = ifcopenshell.util.selector.filter_elements(ifc_file, include)
|
try:
|
||||||
|
data = json.loads(include)
|
||||||
|
if isinstance(data, dict) and "filter_structure" in data:
|
||||||
|
elements = tool.Search.execute_filter_groups_from_json(data, ifc_file)
|
||||||
|
elif isinstance(data, dict) and "query" in data:
|
||||||
|
elements = ifcopenshell.util.selector.filter_elements(ifc_file, data["query"])
|
||||||
|
else:
|
||||||
|
elements = ifcopenshell.util.selector.filter_elements(ifc_file, include)
|
||||||
|
except (json.JSONDecodeError, ValueError):
|
||||||
|
elements = ifcopenshell.util.selector.filter_elements(ifc_file, include)
|
||||||
else:
|
else:
|
||||||
if ifc_file.schema == "IFC2X3":
|
if ifc_file.schema == "IFC2X3":
|
||||||
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
|
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
|
||||||
@@ -2291,7 +2300,7 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
if not i.is_a("IfcAnnotation"):
|
if not i.is_a("IfcAnnotation"):
|
||||||
updated_set.add(i)
|
updated_set.add(i)
|
||||||
# add aggregate too, if element is host by one
|
# add aggregate too, if element is host by one
|
||||||
if decomposes := i.Decomposes:
|
if hasattr(i, "Decomposes") and (decomposes := i.Decomposes):
|
||||||
aggregate = decomposes[0].RelatingObject
|
aggregate = decomposes[0].RelatingObject
|
||||||
# remove IfcProject for class iterator. See https://github.com/IfcOpenShell/IfcOpenShell/issues/4361#issuecomment-2081223615
|
# remove IfcProject for class iterator. See https://github.com/IfcOpenShell/IfcOpenShell/issues/4361#issuecomment-2081223615
|
||||||
if aggregate.is_a("IfcProduct"):
|
if aggregate.is_a("IfcProduct"):
|
||||||
@@ -2304,7 +2313,18 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
|
|
||||||
exclude = pset.get("Exclude", None)
|
exclude = pset.get("Exclude", None)
|
||||||
if exclude:
|
if exclude:
|
||||||
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
|
try:
|
||||||
|
data = json.loads(exclude)
|
||||||
|
if isinstance(data, dict) and "filter_structure" in data:
|
||||||
|
exclude_elements = tool.Search.execute_filter_groups_from_json(data, ifc_file)
|
||||||
|
elements -= exclude_elements
|
||||||
|
elif isinstance(data, dict) and "query" in data:
|
||||||
|
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, data["query"])
|
||||||
|
else:
|
||||||
|
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
|
||||||
|
except (json.JSONDecodeError, ValueError):
|
||||||
|
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
|
||||||
|
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
|
||||||
elements -= set(ifc_file.by_type("IfcOpeningElement"))
|
elements -= set(ifc_file.by_type("IfcOpeningElement"))
|
||||||
return elements
|
return elements
|
||||||
|
|
||||||
@@ -2318,7 +2338,18 @@ class Drawing(bonsai.core.tool.Drawing):
|
|||||||
# NOTE: EPset_Drawing.Include is not used to avoid adding other elements besides spaces
|
# NOTE: EPset_Drawing.Include is not used to avoid adding other elements besides spaces
|
||||||
exclude = pset.get("Exclude", None)
|
exclude = pset.get("Exclude", None)
|
||||||
if exclude:
|
if exclude:
|
||||||
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
|
try:
|
||||||
|
data = json.loads(exclude)
|
||||||
|
if isinstance(data, dict) and "filter_structure" in data:
|
||||||
|
exclude_elements = tool.Search.execute_filter_groups_from_json(data, ifc_file)
|
||||||
|
elements -= exclude_elements
|
||||||
|
elif isinstance(data, dict) and "query" in data:
|
||||||
|
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, data["query"])
|
||||||
|
else:
|
||||||
|
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
|
||||||
|
except (json.JSONDecodeError, ValueError):
|
||||||
|
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
|
||||||
|
elements -= ifcopenshell.util.selector.filter_elements(ifc_file, exclude)
|
||||||
return elements
|
return elements
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -42,6 +42,38 @@ class Search(bonsai.core.tool.Search):
|
|||||||
def get_group_query(cls, group: ifcopenshell.entity_instance) -> str:
|
def get_group_query(cls, group: ifcopenshell.entity_instance) -> str:
|
||||||
return json.loads(group.Description)["query"]
|
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]
|
FilterModule = Union[Literal["search", "csv", "diff", "drawing_include", "drawing_exclude"], str]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -80,53 +112,244 @@ class Search(bonsai.core.tool.Search):
|
|||||||
for ifc_filter in filter_group.filters:
|
for ifc_filter in filter_group.filters:
|
||||||
if not ifc_filter.value:
|
if not ifc_filter.value:
|
||||||
continue
|
continue
|
||||||
if ifc_filter.type == "instance":
|
|
||||||
if "bpy.data.texts" in ifc_filter.value:
|
query_part = cls._export_single_filter(ifc_filter)
|
||||||
data_name = ifc_filter.value.split("bpy.data.texts")[1][2:-2]
|
if query_part:
|
||||||
filter_group_query.append(bpy.data.texts[data_name].as_string())
|
filter_group_query.append(query_part)
|
||||||
else:
|
|
||||||
filter_group_query.append(ifc_filter.value)
|
if filter_group_query:
|
||||||
elif ifc_filter.type == "entity":
|
query.append(", ".join(filter_group_query))
|
||||||
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))
|
|
||||||
return " + ".join(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
|
@classmethod
|
||||||
def get_comparison_and_value(
|
def get_comparison_and_value(
|
||||||
cls, ifc_filter: BIMFacet
|
cls, ifc_filter: BIMFacet
|
||||||
@@ -349,7 +572,8 @@ class ImportFilterQueryTransformer(lark.Transformer):
|
|||||||
def facet_list(self, args):
|
def facet_list(self, args):
|
||||||
new = self.filter_groups.add()
|
new = self.filter_groups.add()
|
||||||
global_ids = []
|
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 arg["type"] == "instance" and global_ids:
|
||||||
if "bpy.data.texts" in new2.value:
|
if "bpy.data.texts" in new2.value:
|
||||||
data_name = new2.value.split("bpy.data.texts")[1][2:-2]
|
data_name = new2.value.split("bpy.data.texts")[1][2:-2]
|
||||||
@@ -374,21 +598,26 @@ class ImportFilterQueryTransformer(lark.Transformer):
|
|||||||
new2.pset = arg["pset"]
|
new2.pset = arg["pset"]
|
||||||
if "comparison" in arg:
|
if "comparison" in arg:
|
||||||
new2.comparison = arg["comparison"] or "="
|
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):
|
def facet(self, args):
|
||||||
return args[0]
|
return args[0]
|
||||||
|
|
||||||
def instance(self, args):
|
def instance(self, args):
|
||||||
if args[0].data == "not":
|
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:
|
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):
|
def entity(self, args):
|
||||||
if args[0].data == "not":
|
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:
|
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):
|
def attribute(self, args):
|
||||||
name, comparison, value = args
|
name, comparison, value = args
|
||||||
|
|||||||
Reference in New Issue
Block a user