Added UI for IFC Selector Tool

UI for IFC Selector tool
This commit is contained in:
Vukas Pajic
2022-07-19 14:54:39 +02:00
committed by GitHub
5 changed files with 541 additions and 18 deletions
@@ -20,6 +20,7 @@ import bpy
from . import ui, prop, operator
classes = (
operator.EditBlenderCollection,
operator.ActivateIfcClassFilter,
operator.ActivateIfcBuildingStoreyFilter,
operator.ColourByAttribute,
@@ -31,18 +32,32 @@ classes = (
operator.SelectGlobalId,
operator.SelectIfcClass,
operator.SelectPset,
operator.UnhideAllElements,
operator.FilterModelElements,
operator.IfcSelector,
operator.SaveSelectorQuery,
operator.OpenQueryLibrary,
operator.LoadQuery,
prop.BIMFilterClasses,
prop.BIMFilterBuildingStoreys,
prop.BIMSearchProperties,
prop.SearchCollection,
prop.SearchQueryFilter,
prop.SearchQuery,
prop.SearchQueryGroup,
prop.IfcSelectorProperties,
ui.BIM_PT_search,
ui.BIM_UL_ifc_class_filter,
ui.BIM_UL_ifc_building_storey_filter,
ui.BIM_PT_IFCSelector,
)
def register():
bpy.types.Scene.BIMSearchProperties = bpy.props.PointerProperty(type=prop.BIMSearchProperties)
bpy.types.Scene.IfcSelectorProperties = bpy.props.PointerProperty(type=prop.IfcSelectorProperties)
def unregister():
del bpy.types.Scene.BIMSearchProperties
del bpy.types.Scene.IfcSelectorProperties
@@ -20,9 +20,21 @@ import re
import bpy
import ifcopenshell
import ifcopenshell.util.element
from ifcopenshell.util.selector import Selector
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from itertools import cycle
from bpy.types import PropertyGroup, Operator
from bpy.props import (
PointerProperty,
StringProperty,
EnumProperty,
BoolProperty,
IntProperty,
FloatProperty,
FloatVectorProperty,
CollectionProperty,
)
colour_list = [
@@ -54,13 +66,29 @@ def does_keyword_exist(pattern, string, context):
return True
class SelectGlobalId(bpy.types.Operator):
class EditBlenderCollection(Operator):
bl_idname = "bim.edit_blender_collection"
bl_label = "Add or Remove blender collection item"
bl_options = {"REGISTER", "UNDO"}
option: StringProperty()
collection: StringProperty()
index: IntProperty()
def execute(self, context):
if self.option == "add":
getattr(context.bim_prop_group, self.collection).add()
else:
getattr(context.bim_prop_group, self.collection).remove(self.index)
return {"FINISHED"}
class SelectGlobalId(Operator):
"""Click to select the objects that match with the given Global ID"""
bl_idname = "bim.select_global_id"
bl_label = "Select GlobalId"
bl_options = {"REGISTER", "UNDO"}
global_id: bpy.props.StringProperty()
global_id: StringProperty()
def execute(self, context):
ifc_file = tool.Ifc.get()
@@ -73,13 +101,13 @@ class SelectGlobalId(bpy.types.Operator):
return {"FINISHED"}
class SelectIfcClass(bpy.types.Operator):
class SelectIfcClass(Operator):
"""Click to select all objects that match with the given IFC class"""
bl_idname = "bim.select_ifc_class"
bl_label = "Select IFC Class"
bl_options = {"REGISTER", "UNDO"}
ifc_class: bpy.props.StringProperty()
ifc_class: StringProperty()
def execute(self, context):
self.file = IfcStore.get_file()
@@ -92,7 +120,7 @@ class SelectIfcClass(bpy.types.Operator):
return {"FINISHED"}
class SelectAttribute(bpy.types.Operator):
class SelectAttribute(Operator):
"""Click to select all objects that match with the given Attribute Name and Value"""
bl_idname = "bim.select_attribute"
@@ -118,7 +146,7 @@ class SelectAttribute(bpy.types.Operator):
return {"FINISHED"}
class SelectPset(bpy.types.Operator):
class SelectPset(Operator):
"""Click to select all objects that match with the given Pset Name, Properties Name and Value"""
bl_idname = "bim.select_pset"
@@ -152,7 +180,7 @@ class SelectPset(bpy.types.Operator):
return {"FINISHED"}
class ColourByAttribute(bpy.types.Operator):
class ColourByAttribute(Operator):
"""Click to colour different objects according to given Attribute Name"""
bl_idname = "bim.colour_by_attribute"
@@ -203,7 +231,7 @@ class ColourByAttribute(bpy.types.Operator):
data["area"].spaces[0].shading.color_type = "OBJECT"
class ColourByPset(bpy.types.Operator):
class ColourByPset(Operator):
"""Click to colour different objects according to given Prop Name"""
bl_idname = "bim.colour_by_pset"
@@ -262,7 +290,7 @@ class ColourByPset(bpy.types.Operator):
data["area"].spaces[0].shading.color_type = "OBJECT"
class ColourByClass(bpy.types.Operator):
class ColourByClass(Operator):
"""Click to colour different objects according to their IFC Classes"""
bl_idname = "bim.colour_by_class"
@@ -308,7 +336,7 @@ class ColourByClass(bpy.types.Operator):
data["area"].spaces[0].shading.color_type = "OBJECT"
class ResetObjectColours(bpy.types.Operator):
class ResetObjectColours(Operator):
"""Reset the colour of selected objects"""
bl_idname = "bim.reset_object_colours"
@@ -320,11 +348,11 @@ class ResetObjectColours(bpy.types.Operator):
return {"FINISHED"}
class ToggleFilterSelection(bpy.types.Operator):
class ToggleFilterSelection(Operator):
"Click to select/deselect current selection"
bl_idname = "bim.toggle_filter_selection"
bl_label = "Toggle Filter Selection"
action: bpy.props.EnumProperty(items=(("SELECT", "Select", ""), ("DESELECT", "Deselect", "")))
action: EnumProperty(items=(("SELECT", "Select", ""), ("DESELECT", "Deselect", "")))
def execute(self, context):
props = bpy.context.scene.BIMSearchProperties
@@ -341,7 +369,7 @@ class ToggleFilterSelection(bpy.types.Operator):
return {"FINISHED"}
class ActivateIfcClassFilter(bpy.types.Operator):
class ActivateIfcClassFilter(Operator):
"""Filter the current selection by IFC class"""
bl_idname = "bim.activate_ifc_class_filter"
@@ -387,7 +415,7 @@ class ActivateIfcClassFilter(bpy.types.Operator):
row.operator("bim.toggle_filter_selection", text="Deselect All").action = "DESELECT"
class ActivateIfcBuildingStoreyFilter(bpy.types.Operator):
class ActivateIfcBuildingStoreyFilter(Operator):
"""Filter the current selection by Building Storey"""
bl_idname = "bim.activate_ifc_building_storey_filter"
@@ -433,3 +461,188 @@ class ActivateIfcBuildingStoreyFilter(bpy.types.Operator):
row = self.layout.row(align=True)
row.operator("bim.toggle_filter_selection", text="Select All").action = "SELECT"
row.operator("bim.toggle_filter_selection", text="Deselect All").action = "DESELECT"
class UnhideAllElements(Operator):
"""Filter model elements based on selection"""
bl_idname = "bim.reset_3d_view"
bl_label = "Reset 3D View"
bl_idname = "bim.unhide_all_elements"
bl_label = "Unhide All Elements"
def execute(self, context):
for obj in bpy.data.scenes["Scene"].objects:
obj.hide_set(False)
return {"FINISHED"}
class FilterModelElements(Operator):
"""Filter model elements based on selection"""
bl_idname = "bim.filter_model_elements"
bl_label = "Filter Model Elements"
option: StringProperty("select|isolate|hide")
def execute(self, context):
selector = context.scene.IfcSelectorProperties
selection = selector.selector_query_syntax if selector.manual_override else self.add_groups(selector)
selector.selector_query_syntax = selection
self.update_model_view(context, selection)
return {"FINISHED"}
def add_groups(self, selector):
selection = ""
for group_index, group in enumerate(selector.groups):
if group_index != 0:
selection += " | "
selection += "(" if len(selector.groups) > 1 else ""
selection = self.add_queries(selection, group)
selection += ")" if len(selector.groups) > 1 else ""
return selection
def add_queries(self, selection, group):
for query_index, query in enumerate(group.queries):
if query_index != 0:
selection += " & " if query.and_or == "and" else " | "
if query.selector == "IFC Class":
active_option = query.active_option.split(": ")[1]
selection += f".{active_option}"
selection = self.add_filters(selection, query)
elif query.selector == "GlobalId":
selection += f"#{query.global_id}"
elif query.selector == "IfcElementType":
index = int(query.active_sub_option.split(":")[0])
selection += f"* #{query.sub_options[index].global_id}"
elif query.selector == "IfcSpatialElement":
index = int(query.active_sub_option.split(":")[0])
selection += f"@ #{query.sub_options[index].global_id}"
return selection
def add_filters(self, selection, query):
for f_index, f in enumerate(query.filters):
if f_index != 0:
selection += " & " if f.and_or == "and" else " | "
selection += f".{query.active_option}"
selection += "["
if f.selector == "IfcPropertySet":
selection += f'{f.active_option.split(": ")[1]}.{f.active_sub_option.split(": ")[1]} {"!" if f.negation else ""}="{f.value}"'
elif f.selector == "Attribute":
selection += f'{f.attribute} {"!" if f.negation else ""}= "{f.value}"'
selection += "]"
return selection
def update_model_view(self, context, selection):
query = Selector.parse(IfcStore.file, selection)
sel_element_ids = [e.id() for e in query]
bpy.ops.object.select_all(action="DESELECT")
for obj in bpy.data.scenes["Scene"].objects:
obj.hide_set(False) # reset 3d view
if self.option == "select":
if obj.BIMObjectProperties.ifc_definition_id in sel_element_ids:
obj.select_set(True)
elif self.option == "isolate":
if obj.BIMObjectProperties.ifc_definition_id not in sel_element_ids:
obj.hide_set(True)
elif self.option == "hide":
if obj.BIMObjectProperties.ifc_definition_id in sel_element_ids:
obj.hide_set(True)
class IfcSelector(Operator):
"""Select elements in model with IFC Selector"""
bl_idname = "bim.ifc_selector"
bl_label = "Select elements with IFC Selector"
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=800)
@classmethod
def poll(cls, context):
return IfcStore.get_file()
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
from . import ui
ui.IfcSelectorUI.draw(context, self.layout)
class SaveSelectorQuery(Operator):
bl_idname = "bim.save_selector_query"
bl_label = "Save Selector Query"
save_name: StringProperty()
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=400)
def draw(self, context):
layout = self.layout
layout.prop(self, "save_name")
def execute(self, context):
ifc_selector = context.scene.IfcSelectorProperties
new = ifc_selector.query_library.add()
new.name = self.save_name
new.query = ifc_selector.selector_query_syntax
return {"FINISHED"}
class OpenQueryLibrary(Operator):
"""Open Query Library"""
bl_idname = "bim.open_query_library"
bl_label = "Open Query Library"
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=400)
def close_panel(event):
x, y = event.mouse_x, event.mouse_y
bpy.context.window.cursor_warp(10, 10)
move_back = lambda: bpy.context.window.cursor_warp(x, y)
bpy.app.timers.register(move_back, first_interval=0.001)
def draw(self, context):
layout = self.layout
ifc_selector = context.scene.IfcSelectorProperties
for index, query in enumerate(ifc_selector.query_library):
row = layout.row(align=True)
row.prop(query, "query", text=query.name)
op = row.operator("bim.load_query", icon="SORT_ASC", text="")
op.index = index
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
op.option = "remove"
op.collection = "query_library"
op.index = index
def execute(self, context):
return {"FINISHED"}
class LoadQuery(Operator):
bl_idname = "bim.load_query"
bl_label = "Load Query"
index: IntProperty()
def execute(self, context):
ifc_selector = context.scene.IfcSelectorProperties
ifc_selector.selector_query_syntax = ifc_selector.query_library[self.index].query
return {"FINISHED"}
@@ -17,9 +17,13 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from ifcopenshell import util
from ifcopenshell.util.selector import Selector
import blenderbim.tool as tool
from blenderbim.bim.prop import ObjProperty
from blenderbim.bim.prop import ObjProperty, StrProperty
from blenderbim.bim.ifc import IfcStore
from bpy.types import PropertyGroup
from blenderbim.tool.ifc import Ifc
from . import ui, prop, operator
from bpy.props import (
PointerProperty,
@@ -90,3 +94,146 @@ class BIMSearchProperties(PropertyGroup):
filter_classes_index: IntProperty(name="Filter Classes Index")
filter_building_storeys: CollectionProperty(type=BIMFilterBuildingStoreys, name="Filter Level")
filter_building_storeys_index: IntProperty(name="Filter Level Index")
def get_classes(self, ifc_product):
declaration = tool.Ifc.schema().declaration_by_name(ifc_product)
declarations = util.schema.get_subtypes(declaration)
names = [d.name() for d in declarations]
return [(c, c, "") for c in sorted(names)]
def load_sub_options(self, context):
if self.selector not in ["IfcClass"]:
self.load_option = "sub_options"
load_selection_options(self, context)
def load_selection_options(self, context):
ifc = IfcStore.file
load_option = self.load_option
op = getattr(self, load_option)
op.clear()
options = []
if load_option == "options":
self.sub_options.clear()
if self.selector == "IFC Class":
options = get_classes(self, "IfcElement")
options.append(("IfcSpace", "IfcSpace", ""))
elif self.selector == "IfcSpatialElement":
options = get_classes(self, "IfcSpatialElement")
elif self.selector == "IfcElementType":
options = get_classes(self, "IfcElementType")
elif self.selector == "GlobalId":
return
elif self.selector == "IfcPropertySet":
psets = Selector.parse(ifc, ".IfcPropertySet")
options = set([o.Name for o in psets])
elif load_option == "sub_options":
if self.selector in ["IfcSpatialElement", "IfcElementType"]:
active_option = self.active_option.split(": ")[1]
options = Selector.parse(ifc, f".{active_option}")
elif self.selector == "IfcPropertySet":
options = set()
active_pset = self.active_option.split(": ")[1]
psets_in_file = Selector.parse(ifc, f'.IfcPropertySet[Name="{active_pset}"]')
for pset in psets_in_file:
for prop in pset.HasProperties:
options.add(prop.Name)
for index, option in enumerate(options):
new = op.add()
if self.selector in ["IfcSpatialElement", "IfcElementType"]:
if self.load_option == "sub_options":
new.name = f"{index}: {option.Name}"
new.global_id = option.GlobalId
else:
new.name = f"{index}: {option[0]}"
elif self.selector == "IfcPropertySet":
new.name = f"{index}: {option}"
else:
new.name = f"{index}: {option[0]}"
self.load_option = "options"
def load_spatial_elements(self, context):
ifc = IfcStore.file
col_items = Selector.parse(ifc, f".{self.selected_spatial_element}")
collection = getattr(self, "sub_spatial_elements", None)
collection.clear()
for index, c in enumerate(col_items):
new = collection.add()
new.name = f"{index}-{c.Name}"
class SearchCollection(PropertyGroup):
name: StringProperty()
long_name: StringProperty()
global_id: StringProperty()
query: StringProperty()
class IfcSelector:
and_or: EnumProperty(
items=[(i, i, i) for i in ["and", "or"]],
)
negation: BoolProperty(name="not")
comparison: EnumProperty(
items=[
("=", "equal to", ""),
("*=", "contains", ""),
(">=", "greater than or equal to", ""),
("<=", "lesser than or equal to", ""),
(">", "greater than", ""),
("<", "less than", ""),
],
)
load_option: StringProperty(
default="options", description="controls whether or not options or sub_options are loaded"
)
options: CollectionProperty(type=SearchCollection)
active_option: StringProperty(update=load_sub_options)
sub_options: CollectionProperty(type=SearchCollection)
active_sub_option: StringProperty()
value: StringProperty(description="generic 'value' that can be used in multiple scenarios")
class SearchQueryFilter(PropertyGroup, IfcSelector):
selector: EnumProperty(
items=[(i, i, i) for i in ["-", "IfcPropertySet", "Attribute"]],
name="Filter selection by",
update=load_selection_options,
default="-",
)
attribute: EnumProperty(
items=[(i, i, i) for i in ["GlobalId", "Name", "Description", "ObjectType", "Tag", "PredefinedType"]],
name="Filter selection by",
update=load_selection_options,
)
class SearchQuery(PropertyGroup, IfcSelector):
filters: CollectionProperty(type=SearchQueryFilter)
selector: EnumProperty(
items=[(i, i, i) for i in ["-", "IFC Class", "IfcSpatialElement", "IfcElementType", "GlobalId"]],
name="Selector type",
update=load_selection_options,
default="-",
)
class SearchQueryGroup(PropertyGroup, IfcSelector):
queries: CollectionProperty(type=SearchQuery)
class IfcSelectorProperties(PropertyGroup, IfcSelector):
groups: CollectionProperty(type=SearchQueryGroup)
selector_query_syntax: StringProperty()
query_library: CollectionProperty(type=SearchCollection)
active_query: StringProperty()
active_query: StringProperty()
manual_override: BoolProperty(default=False, description="Toggle to allow manual typing of query-syntax")
@@ -102,3 +102,152 @@ class BIM_UL_ifc_building_storey_filter(bpy.types.UIList):
split = split.column()
split.scale_x = 0.5
split.label(text=str(item.total))
class BIM_PT_IFCSelector(Panel):
bl_label = "IFC Selector"
bl_idname = "BIM_PT_ifc_selector"
bl_options = {"DEFAULT_CLOSED"}
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_parent_id = "BIM_PT_collaboration"
@classmethod
def poll(cls, context):
return IfcStore.get_file()
def draw(self, context):
layout = self.layout
layout.operator("bim.ifc_selector")
# this doesn't inherit from Panel since it's just a class to abstract away UI code from the IfcSelector Operator
class IfcSelectorUI:
@classmethod
def draw(self, context, layout):
ifc_selector = context.scene.IfcSelectorProperties
row = layout.row()
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
op = row.operator("bim.edit_blender_collection", text="Add selection group")
op.option = "add"
op.collection = "groups"
layout.separator()
self.draw_query_group_ui(self, ifc_selector, layout)
if len(ifc_selector.groups) != 0:
row = layout.row(align=True)
row.prop(ifc_selector, "selector_query_syntax", text="Query Syntax")
row.prop(ifc_selector, "manual_override", text="")
select = row.operator("bim.filter_model_elements", text="", icon="RESTRICT_SELECT_OFF")
select.option = "select"
isolate = row.operator("bim.filter_model_elements", text="", icon="ZOOM_SELECTED")
isolate.option = "isolate"
hide = row.operator("bim.filter_model_elements", text="", icon="HIDE_ON")
hide.option = "hide"
row.operator("bim.unhide_all_elements", text="", icon="HIDE_OFF")
row = layout.row(align=True)
row.alignment = "CENTER"
row.operator("bim.save_selector_query", text="Save Query")
row.operator("bim.open_query_library", text="Load Query")
def draw_query_group_ui(self, ifc_selector, layout):
for index, group in enumerate(ifc_selector.groups):
row = layout.row()
row.alignment = "CENTER"
row.label(text="or") if index != 0 else None
box = layout.box()
row = box.row(align=True)
row.alignment = "CENTER"
row.label(text=f"Group #{str(index+1)}")
row.context_pointer_set(name="bim_prop_group", data=group)
op = row.operator("bim.edit_blender_collection", text="Add query", icon="PLUS")
op.option = "add"
op.collection = "queries"
row.context_pointer_set(name="bim_prop_group", data=ifc_selector)
op = row.operator("bim.edit_blender_collection", text="Remove selection group")
op.option = "remove"
op.collection = "groups"
op.index = index
self.draw_query_ui(self, group, box)
def draw_query_ui(self, group, box):
for index, query in enumerate(group.queries):
row = box.row()
row.alignment = "LEFT"
row.prop(query, "and_or", text="") if index != 0 else None
if query.and_or == "or":
row = box.row()
row.alignment = "LEFT"
row.prop(query, "selector", text="")
if query.selector in ["IFC Class", "IfcSpatialElement", "IfcElementType"]:
row.label(text="Equals")
row.prop_search(query, "active_option", query, "options", text="")
row.prop_search(query, "active_sub_option", query, "sub_options", text="") if len(
query.sub_options
) != 0 else None
self.draw_filter_ui(self, box, query)
elif query.selector in ["GlobalId", "Attribute"]:
row.prop(query, "negation", text="")
row.prop(query, "comparison", text="")
row.prop(query, "value", text="")
row.context_pointer_set(name="bim_prop_group", data=group)
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
op.option = "remove"
op.collection = "queries"
op.index = index
row.context_pointer_set(name="bim_prop_group", data=query)
op = (
row.operator("bim.edit_blender_collection", text="Add filter")
if query.selector == "IFC Class"
else None
)
if op:
op.option = "add"
op.collection = "filters"
def draw_filter_ui(self, box, query):
for filter_index, f in enumerate(query.filters):
row = box.row()
row.alignment = "LEFT"
row.label(text="")
row.prop(f, "and_or", text="") if filter_index != 0 else None
if f.and_or == "or":
row = box.row()
row.alignment = "LEFT"
row.label(text="")
row.prop(f, "selector", text="")
if f.selector == "Attribute":
row.prop(f, "attribute", text="")
row.prop(
f,
"negation",
)
row.prop(f, "comparison", text="")
row.prop(f, "value", text="")
elif f.selector == "IfcPropertySet":
row.prop_search(f, "active_option", f, "options", text="")
row.prop_search(f, "active_sub_option", f, "sub_options", text="")
row.prop(f, "negation")
row.prop(f, "comparison", text="")
row.prop(f, "value", text="")
row.context_pointer_set(name="bim_prop_group", data=query)
op = row.operator("bim.edit_blender_collection", icon="REMOVE", text="")
op.option = "remove"
op.collection = "filters"
op.index = filter_index
@@ -27,7 +27,6 @@ class Selector:
def parse(cls, ifc_file, query, elements=None):
cls.file = ifc_file
cls.elements = elements
l = lark.Lark(
"""start: query (lfunction query)*
query: selector | group
@@ -38,7 +37,7 @@ class Selector:
filter: "[" filter_key (comparison filter_value)? "]"
filter_key: WORD | pset_or_qto
filter_value: ESCAPED_STRING | SIGNED_FLOAT | SIGNED_INT | BOOLEAN | NULL
pset_or_qto: /[A-Za-z0-9_]+/ "." /[A-Za-z0-9_]+/
pset_or_qto: /[^\W][^.=<>]*[^\W]/ "." /[^\W][^.=<>]*[^\W]/
lfunction: and | or
inverse_relationship: types | decomposed_by | bounded_by
types: "*"
@@ -185,7 +184,7 @@ class Selector:
elif token_type == "SIGNED_FLOAT":
value = float(filter_rule.children[2].children[0])
elif token_type == "BOOLEAN":
value = filter_rule.children[2].children[0].lower() == 'true'
value = filter_rule.children[2].children[0].lower() == "true"
elif token_type == "NULL":
value = None
for element in elements: