From dd827bb4c5d1111ac99a5f66ff5fa74dc7c4a27b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 23 Jul 2023 15:20:14 +1000 Subject: [PATCH] You can now save, load, and create custom colour legends of properties --- .../blenderbim/bim/module/group/ui.py | 2 +- .../blenderbim/bim/module/search/__init__.py | 6 + .../blenderbim/bim/module/search/data.py | 29 ++++ .../blenderbim/bim/module/search/operator.py | 127 +++++++++++++++++- .../blenderbim/bim/module/search/prop.py | 23 +++- .../blenderbim/bim/module/search/ui.py | 45 ++++++- src/blenderbim/blenderbim/tool/search.py | 3 +- .../ifcopenshell/util/selector.py | 46 +++---- 8 files changed, 246 insertions(+), 35 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/group/ui.py b/src/blenderbim/blenderbim/bim/module/group/ui.py index a2d6d8a362..1582b1fdd7 100644 --- a/src/blenderbim/blenderbim/bim/module/group/ui.py +++ b/src/blenderbim/blenderbim/bim/module/group/ui.py @@ -28,7 +28,7 @@ class BIM_PT_groups(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_project_setup" + bl_parent_id = "BIM_PT_tab_grouping_and_filtering" @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/search/__init__.py b/src/blenderbim/blenderbim/bim/module/search/__init__.py index 599fdf12ca..01c01e0c33 100644 --- a/src/blenderbim/blenderbim/bim/module/search/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/search/__init__.py @@ -25,17 +25,20 @@ classes = ( operator.AddFilter, operator.AddFilterGroup, operator.AddToIfcGroup, + operator.ColourByProperty, operator.ColourByAttribute, operator.ColourByClass, operator.ColourByPset, operator.FilterModelElements, operator.IfcSelector, + operator.LoadColourscheme, operator.LoadQuery, operator.LoadSearch, operator.OpenQueryLibrary, operator.RemoveFilter, operator.RemoveFilterGroup, operator.ResetObjectColours, + operator.SaveColourscheme, operator.SaveSearch, operator.SaveSelectorQuery, operator.Search, @@ -45,6 +48,7 @@ classes = ( operator.SelectPset, operator.ShowAllElements, operator.ToggleFilterSelection, + prop.BIMColour, prop.BIMFacet, prop.BIMFilterGroup, prop.BIMFilterClasses, @@ -56,6 +60,8 @@ classes = ( prop.SearchQueryGroup, prop.IfcSelectorProperties, ui.BIM_PT_search, + ui.BIM_PT_colour_by_property, + ui.BIM_UL_colourscheme, ui.BIM_UL_ifc_class_filter, ui.BIM_UL_ifc_building_storey_filter, ui.BIM_PT_IFCSelector, diff --git a/src/blenderbim/blenderbim/bim/module/search/data.py b/src/blenderbim/blenderbim/bim/module/search/data.py index 63c25b4e08..41345d8a16 100644 --- a/src/blenderbim/blenderbim/bim/module/search/data.py +++ b/src/blenderbim/blenderbim/bim/module/search/data.py @@ -23,6 +23,7 @@ import blenderbim.tool as tool def refresh(): SearchData.is_loaded = False + ColourByPropertyData.is_loaded = False class SearchData: @@ -47,3 +48,31 @@ class SearchData: except: pass return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")] + + +class ColourByPropertyData: + data = {} + is_loaded = False + + @classmethod + def load(cls): + cls.is_loaded = True + cls.data = {} + cls.data["saved_colourschemes"] = cls.saved_colourschemes() + + @classmethod + def saved_colourschemes(cls): + groups = tool.Ifc.get().by_type("IfcGroup") + results = [] + for group in groups: + try: + data = json.loads(group.Description) + if ( + isinstance(data, dict) + and data.get("type", None) == "BBIM_Search" + and data.get("colourscheme", None) + ): + results.append(group) + except: + pass + return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")] diff --git a/src/blenderbim/blenderbim/bim/module/search/operator.py b/src/blenderbim/blenderbim/bim/module/search/operator.py index 1875dffed3..5719cabfa5 100644 --- a/src/blenderbim/blenderbim/bim/module/search/operator.py +++ b/src/blenderbim/blenderbim/bim/module/search/operator.py @@ -149,7 +149,11 @@ class SaveSearch(Operator, tool.Ifc.Operator): results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query) description = json.dumps({"type": "BBIM_Search", "query": query}) - group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description) + group = [g for g in tool.Ifc.get().by_type("IfcGroup") if g.Name == self.name] + if group: + group = group[0] + else: + group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description) ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), products=results, group=group) def invoke(self, context, event): @@ -176,6 +180,121 @@ class LoadSearch(Operator, tool.Ifc.Operator): return context.window_manager.invoke_props_dialog(self) +class ColourByProperty(Operator): + bl_idname = "bim.colour_by_property" + bl_label = "Colour by Property" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + IfcStore.begin_transaction(self) + self.store_state(context) + result = self._execute(context) + IfcStore.add_transaction_operation(self) + IfcStore.end_transaction(self) + return result + + def _execute(self, context): + props = context.scene.BIMSearchProperties + query = props.colourscheme_query + + if not query: + self.report({"ERROR"}, "No Query Provided") + return {"CANCELLED"} + + colours = cycle(colour_list) + colourscheme = {} + + if len(props.colourscheme): + colourscheme = {cs.name: cs.colour[0:3] for cs in props.colourscheme} + + for obj in context.visible_objects: + element = tool.Ifc.get_entity(obj) + if not element: + continue + value = str(ifcopenshell.util.selector.get_element_value(element, query)) + if value not in colourscheme: + colourscheme[value] = next(colours)[0:3] + obj.color = (*colourscheme[value], 1) + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if areas: + areas[0].spaces[0].shading.color_type = "OBJECT" + + props.colourscheme.clear() + for value, colour in colourscheme.items(): + new = props.colourscheme.add() + new.name = str(value) + new.colour = colour[0:3] + return {"FINISHED"} + + def store_state(self, context): + areas = [a for a in context.screen.areas if a.type == "VIEW_3D"] + if areas: + self.transaction_data = {"area": areas[0], "color_type": areas[0].spaces[0].shading.color_type} + + def rollback(self, data): + if data: + data["area"].spaces[0].shading.color_type = data["color_type"] + + def commit(self, data): + if data: + data["area"].spaces[0].shading.color_type = "OBJECT" + + +class SaveColourscheme(Operator, tool.Ifc.Operator): + bl_idname = "bim.save_colourscheme" + bl_label = "Save Colourscheme" + bl_description = "Save colourscheme to an IFC group" + bl_options = {"REGISTER", "UNDO"} + name: StringProperty(name="Name") + + def _execute(self, context): + if not self.name: + return + + props = context.scene.BIMSearchProperties + query = props.colourscheme_query + + group = [g for g in tool.Ifc.get().by_type("IfcGroup") if g.Name == self.name] + if group: + group = group[0] + description = json.loads(group.Description) + description["colourscheme"] = {cs.name: cs.colour[0:3] for cs in props.colourscheme} + description["colourscheme_query"] = query + group.Description = json.dumps(description) + else: + description = json.dumps({"type": "BBIM_Search", "colourscheme": query}) + group = ifcopenshell.api.run("group.add_group", tool.Ifc.get(), Name=self.name, Description=description) + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + +class LoadColourscheme(Operator, tool.Ifc.Operator): + bl_idname = "bim.load_colourscheme" + bl_label = "Load Colourscheme" + bl_description = "Load colourscheme from an IFC group" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + props = context.scene.BIMSearchProperties + group = tool.Ifc.get().by_id(int(props.saved_searches)) + description = json.loads(group.Description) + props.colourscheme_query = description.get("colourscheme_query") + props.colourscheme.clear() + for name, colour in description.get("colourscheme", {}).items(): + new = props.colourscheme.add() + new.name = name + new.colour = colour + + def draw(self, context): + props = context.scene.BIMSearchProperties + row = self.layout.row() + row.prop(props, "saved_colourschemes", text="") + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + class SelectGlobalId(Operator): """Click to select the objects that match with the given Global ID""" @@ -503,14 +622,16 @@ class ColourByClass(Operator): class ResetObjectColours(Operator): - """Reset the colour of selected objects""" + """Reset the colour of visible objects""" bl_idname = "bim.reset_object_colours" bl_label = "Reset Colours" def execute(self, context): - for obj in context.selected_objects: + for obj in context.visible_objects: obj.color = (1, 1, 1, 1) + props = context.scene.BIMSearchProperties + props.colourscheme.clear() return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/search/prop.py b/src/blenderbim/blenderbim/bim/module/search/prop.py index 42ce5a5216..047132cc51 100644 --- a/src/blenderbim/blenderbim/bim/module/search/prop.py +++ b/src/blenderbim/blenderbim/bim/module/search/prop.py @@ -22,7 +22,7 @@ from ifcopenshell.util.selector import Selector import blenderbim.tool as tool from blenderbim.bim.prop import ObjProperty, StrProperty from blenderbim.bim.ifc import IfcStore -from blenderbim.bim.module.search.data import SearchData +from blenderbim.bim.module.search.data import SearchData, ColourByPropertyData from bpy.types import PropertyGroup from blenderbim.tool.ifc import Ifc from . import ui, prop, operator @@ -44,6 +44,12 @@ def get_saved_searches(self, context): return SearchData.data["saved_searches"] +def get_saved_colourschemes(self, context): + if not ColourByPropertyData.is_loaded: + ColourByPropertyData.load() + return ColourByPropertyData.data["saved_colourschemes"] + + def update_is_class_selected(self, context): if self.is_selected: for obj in self.unselected_objects: @@ -87,9 +93,9 @@ class BIMFilterBuildingStoreys(PropertyGroup): class BIMFacet(PropertyGroup): - name: StringProperty(name="Type") - pset: StringProperty(name="Type") - value: StringProperty(name="Type") + name: StringProperty(name="Name") + pset: StringProperty(name="Pset") + value: StringProperty(name="Value") type: StringProperty(name="Type") comparison: StringProperty(name="Comparison") @@ -98,6 +104,11 @@ class BIMFilterGroup(PropertyGroup): filters: CollectionProperty(type=BIMFacet, name="filters") +class BIMColour(PropertyGroup): + name: StringProperty(name="Name") + colour: FloatVectorProperty(name="Colour", subtype="COLOR", default=(1, 0, 0), min=0.0, max=1.0) + + class BIMSearchProperties(PropertyGroup): filter_query: StringProperty(name="Filter Query") filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups") @@ -114,6 +125,10 @@ class BIMSearchProperties(PropertyGroup): ], ) saved_searches: EnumProperty(items=get_saved_searches, name="Saved Searches") + saved_colourschemes: EnumProperty(items=get_saved_colourschemes, name="Saved Colourschemes") + colourscheme_query: StringProperty(name="Colourscheme Query", default="class") + colourscheme: CollectionProperty(type=BIMColour) + active_colourscheme_index: IntProperty(name="Active Colourscheme Index") should_use_regex: BoolProperty(name="Search With Regex", default=False) should_ignorecase: BoolProperty(name="Search Ignoring Case", default=True) global_id: StringProperty(name="GlobalId") diff --git a/src/blenderbim/blenderbim/bim/module/search/ui.py b/src/blenderbim/blenderbim/bim/module/search/ui.py index 3f837ed015..231e8114fd 100644 --- a/src/blenderbim/blenderbim/bim/module/search/ui.py +++ b/src/blenderbim/blenderbim/bim/module/search/ui.py @@ -18,7 +18,7 @@ import bpy from bpy.types import Panel -from blenderbim.bim.module.search.data import SearchData +from blenderbim.bim.module.search.data import SearchData, ColourByPropertyData class BIM_PT_search(Panel): @@ -138,6 +138,49 @@ class BIM_PT_search(Panel): row.operator("bim.activate_ifc_building_storey_filter", icon="FILTER") +class BIM_PT_colour_by_property(Panel): + bl_label = "Colour By Property" + bl_idname = "BIM_PT_colour_by_property" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_options = {"DEFAULT_CLOSED"} + bl_parent_id = "BIM_PT_tab_grouping_and_filtering" + + def draw(self, context): + if not ColourByPropertyData.is_loaded: + ColourByPropertyData.load() + + props = context.scene.BIMSearchProperties + + row = self.layout.row(align=True) + row.label(text=f"{len(ColourByPropertyData.data['saved_colourschemes'])} Saved Colourschemes") + + if ColourByPropertyData.data["saved_colourschemes"]: + row.operator("bim.load_colourscheme", text="", icon="IMPORT") + row.operator("bim.save_colourscheme", text="", icon="EXPORT") + + row = self.layout.row() + row.prop(props, "colourscheme_query", text="Query") + + row = self.layout.row(align=True) + row.operator("bim.colour_by_property", icon="BRUSH_DATA") + row.operator("bim.reset_object_colours") + + if len(props.colourscheme): + self.layout.template_list("BIM_UL_colourscheme", "", props, "colourscheme", props, "active_colourscheme_index") + + +class BIM_UL_colourscheme(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + props = context.scene.BIMWorkScheduleProperties + if not item: + return + row = layout.row(align=True) + row.label(text=item.name) + row.prop(item, "colour", text="") + + class BIM_UL_ifc_class_filter(bpy.types.UIList): use_filter_linked: bpy.props.BoolProperty(name="Included", default=True, options=set(), description="Filter") diff --git a/src/blenderbim/blenderbim/tool/search.py b/src/blenderbim/blenderbim/tool/search.py index f5569a2931..5edeb24e55 100644 --- a/src/blenderbim/blenderbim/tool/search.py +++ b/src/blenderbim/blenderbim/tool/search.py @@ -12,9 +12,8 @@ class Search(blenderbim.core.tool.Search): def import_filter_query(cls, group, filter_groups): query = json.loads(group.Description)["query"] filter_groups.clear() - l = lark.Lark(ifcopenshell.util.selector.filter_elements_grammar) transformer = ImportFilterQueryTransformer(filter_groups) - transformer.transform(l.parse(query)) + transformer.transform(ifcopenshell.util.selector.filter_elements_grammar.parse(query)) @classmethod def export_filter_query(cls, filter_groups): diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index 6a09fae999..6f940bdf8b 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -24,7 +24,7 @@ import ifcopenshell.util.element import ifcopenshell.util.classification -filter_elements_grammar = """start: filter_group +filter_elements_grammar = lark.Lark("""start: filter_group filter_group: facet_list ("+" facet_list)* facet_list: facet ("," facet)* @@ -86,38 +86,36 @@ filter_elements_grammar = """start: filter_group NEWLINE: (CR? LF)+ %ignore WS // Disregard spaces in text -""" +""") + +get_element_grammar = lark.Lark("""start: WORD | ESCAPED_STRING | keys_regex | keys_quoted | keys_simple + keys_regex: "r" ESCAPED_STRING ("." ESCAPED_STRING)* + keys_quoted: ESCAPED_STRING ("." ESCAPED_STRING)* + keys_simple: /[^\\W][^.=<>!%*\\]]*/ ("." /[^\\W][^.=<>!%*\\]]*/)* + + // Embed common.lark for packaging + _STRING_INNER: /.*?/ + _STRING_ESC_INNER: _STRING_INNER /(?!%*\\]]*/ ("." /[^\\W][^.=<>!%*\\]]*/)* - - // Embed common.lark for packaging - _STRING_INNER: /.*?/ - _STRING_ESC_INNER: _STRING_INNER /(?