You can now save, load, and create custom colour legends of properties

This commit is contained in:
Dion Moult
2023-07-23 15:20:14 +10:00
parent b1cab9484c
commit dd827bb4c5
8 changed files with 246 additions and 35 deletions
@@ -28,7 +28,7 @@ class BIM_PT_groups(Panel):
bl_space_type = "PROPERTIES" bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW" bl_region_type = "WINDOW"
bl_context = "scene" bl_context = "scene"
bl_parent_id = "BIM_PT_project_setup" bl_parent_id = "BIM_PT_tab_grouping_and_filtering"
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -25,17 +25,20 @@ classes = (
operator.AddFilter, operator.AddFilter,
operator.AddFilterGroup, operator.AddFilterGroup,
operator.AddToIfcGroup, operator.AddToIfcGroup,
operator.ColourByProperty,
operator.ColourByAttribute, operator.ColourByAttribute,
operator.ColourByClass, operator.ColourByClass,
operator.ColourByPset, operator.ColourByPset,
operator.FilterModelElements, operator.FilterModelElements,
operator.IfcSelector, operator.IfcSelector,
operator.LoadColourscheme,
operator.LoadQuery, operator.LoadQuery,
operator.LoadSearch, operator.LoadSearch,
operator.OpenQueryLibrary, operator.OpenQueryLibrary,
operator.RemoveFilter, operator.RemoveFilter,
operator.RemoveFilterGroup, operator.RemoveFilterGroup,
operator.ResetObjectColours, operator.ResetObjectColours,
operator.SaveColourscheme,
operator.SaveSearch, operator.SaveSearch,
operator.SaveSelectorQuery, operator.SaveSelectorQuery,
operator.Search, operator.Search,
@@ -45,6 +48,7 @@ classes = (
operator.SelectPset, operator.SelectPset,
operator.ShowAllElements, operator.ShowAllElements,
operator.ToggleFilterSelection, operator.ToggleFilterSelection,
prop.BIMColour,
prop.BIMFacet, prop.BIMFacet,
prop.BIMFilterGroup, prop.BIMFilterGroup,
prop.BIMFilterClasses, prop.BIMFilterClasses,
@@ -56,6 +60,8 @@ classes = (
prop.SearchQueryGroup, prop.SearchQueryGroup,
prop.IfcSelectorProperties, prop.IfcSelectorProperties,
ui.BIM_PT_search, ui.BIM_PT_search,
ui.BIM_PT_colour_by_property,
ui.BIM_UL_colourscheme,
ui.BIM_UL_ifc_class_filter, ui.BIM_UL_ifc_class_filter,
ui.BIM_UL_ifc_building_storey_filter, ui.BIM_UL_ifc_building_storey_filter,
ui.BIM_PT_IFCSelector, ui.BIM_PT_IFCSelector,
@@ -23,6 +23,7 @@ import blenderbim.tool as tool
def refresh(): def refresh():
SearchData.is_loaded = False SearchData.is_loaded = False
ColourByPropertyData.is_loaded = False
class SearchData: class SearchData:
@@ -47,3 +48,31 @@ class SearchData:
except: except:
pass pass
return [(str(g.id()), g.Name or "Unnamed", "") for g in sorted(results, key=lambda x: x.Name or "Unnamed")] 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")]
@@ -149,7 +149,11 @@ class SaveSearch(Operator, tool.Ifc.Operator):
results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query) results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query)
description = json.dumps({"type": "BBIM_Search", "query": 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) ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), products=results, group=group)
def invoke(self, context, event): def invoke(self, context, event):
@@ -176,6 +180,121 @@ class LoadSearch(Operator, tool.Ifc.Operator):
return context.window_manager.invoke_props_dialog(self) 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): class SelectGlobalId(Operator):
"""Click to select the objects that match with the given Global ID""" """Click to select the objects that match with the given Global ID"""
@@ -503,14 +622,16 @@ class ColourByClass(Operator):
class ResetObjectColours(Operator): class ResetObjectColours(Operator):
"""Reset the colour of selected objects""" """Reset the colour of visible objects"""
bl_idname = "bim.reset_object_colours" bl_idname = "bim.reset_object_colours"
bl_label = "Reset Colours" bl_label = "Reset Colours"
def execute(self, context): def execute(self, context):
for obj in context.selected_objects: for obj in context.visible_objects:
obj.color = (1, 1, 1, 1) obj.color = (1, 1, 1, 1)
props = context.scene.BIMSearchProperties
props.colourscheme.clear()
return {"FINISHED"} return {"FINISHED"}
@@ -22,7 +22,7 @@ from ifcopenshell.util.selector import Selector
import blenderbim.tool as tool import blenderbim.tool as tool
from blenderbim.bim.prop import ObjProperty, StrProperty from blenderbim.bim.prop import ObjProperty, StrProperty
from blenderbim.bim.ifc import IfcStore 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 bpy.types import PropertyGroup
from blenderbim.tool.ifc import Ifc from blenderbim.tool.ifc import Ifc
from . import ui, prop, operator from . import ui, prop, operator
@@ -44,6 +44,12 @@ def get_saved_searches(self, context):
return SearchData.data["saved_searches"] 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): def update_is_class_selected(self, context):
if self.is_selected: if self.is_selected:
for obj in self.unselected_objects: for obj in self.unselected_objects:
@@ -87,9 +93,9 @@ class BIMFilterBuildingStoreys(PropertyGroup):
class BIMFacet(PropertyGroup): class BIMFacet(PropertyGroup):
name: StringProperty(name="Type") name: StringProperty(name="Name")
pset: StringProperty(name="Type") pset: StringProperty(name="Pset")
value: StringProperty(name="Type") value: StringProperty(name="Value")
type: StringProperty(name="Type") type: StringProperty(name="Type")
comparison: StringProperty(name="Comparison") comparison: StringProperty(name="Comparison")
@@ -98,6 +104,11 @@ class BIMFilterGroup(PropertyGroup):
filters: CollectionProperty(type=BIMFacet, name="filters") 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): class BIMSearchProperties(PropertyGroup):
filter_query: StringProperty(name="Filter Query") filter_query: StringProperty(name="Filter Query")
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups") 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_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_use_regex: BoolProperty(name="Search With Regex", default=False)
should_ignorecase: BoolProperty(name="Search Ignoring Case", default=True) should_ignorecase: BoolProperty(name="Search Ignoring Case", default=True)
global_id: StringProperty(name="GlobalId") global_id: StringProperty(name="GlobalId")
@@ -18,7 +18,7 @@
import bpy import bpy
from bpy.types import Panel 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): class BIM_PT_search(Panel):
@@ -138,6 +138,49 @@ class BIM_PT_search(Panel):
row.operator("bim.activate_ifc_building_storey_filter", icon="FILTER") 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): class BIM_UL_ifc_class_filter(bpy.types.UIList):
use_filter_linked: bpy.props.BoolProperty(name="Included", default=True, options=set(), description="Filter") use_filter_linked: bpy.props.BoolProperty(name="Included", default=True, options=set(), description="Filter")
+1 -2
View File
@@ -12,9 +12,8 @@ class Search(blenderbim.core.tool.Search):
def import_filter_query(cls, group, filter_groups): def import_filter_query(cls, group, filter_groups):
query = json.loads(group.Description)["query"] query = json.loads(group.Description)["query"]
filter_groups.clear() filter_groups.clear()
l = lark.Lark(ifcopenshell.util.selector.filter_elements_grammar)
transformer = ImportFilterQueryTransformer(filter_groups) transformer = ImportFilterQueryTransformer(filter_groups)
transformer.transform(l.parse(query)) transformer.transform(ifcopenshell.util.selector.filter_elements_grammar.parse(query))
@classmethod @classmethod
def export_filter_query(cls, filter_groups): def export_filter_query(cls, filter_groups):
@@ -24,7 +24,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.classification import ifcopenshell.util.classification
filter_elements_grammar = """start: filter_group filter_elements_grammar = lark.Lark("""start: filter_group
filter_group: facet_list ("+" facet_list)* filter_group: facet_list ("+" facet_list)*
facet_list: facet ("," facet)* facet_list: facet ("," facet)*
@@ -86,38 +86,36 @@ filter_elements_grammar = """start: filter_group
NEWLINE: (CR? LF)+ NEWLINE: (CR? LF)+
%ignore WS // Disregard spaces in text %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 /(?<!\\\\)(\\\\\\\\)*?/
ESCAPED_STRING : "\\"" _STRING_ESC_INNER "\\""
LCASE_LETTER: "a".."z"
UCASE_LETTER: "A".."Z"
LETTER: UCASE_LETTER | LCASE_LETTER
WORD: LETTER+
WS: /[ \\t\\f\\r\\n]/+
%ignore WS // Disregard spaces in text
""")
def get_element_value(element, query): def get_element_value(element, query):
l = lark.Lark( start = get_element_grammar.parse(query)
"""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 /(?<!\\\\)(\\\\\\\\)*?/
ESCAPED_STRING : "\\"" _STRING_ESC_INNER "\\""
LCASE_LETTER: "a".."z"
UCASE_LETTER: "A".."Z"
LETTER: UCASE_LETTER | LCASE_LETTER
WORD: LETTER+
WS: /[ \\t\\f\\r\\n]/+
%ignore WS // Disregard spaces in text
"""
)
start = l.parse(query)
filter_query = Selector.parse_filter_query(start.children[0]) filter_query = Selector.parse_filter_query(start.children[0])
return Selector.get_element_value(element, filter_query["keys"], filter_query["is_regex"]) return Selector.get_element_value(element, filter_query["keys"], filter_query["is_regex"])
def filter_elements(ifc_file, query, elements=None): def filter_elements(ifc_file, query, elements=None):
l = lark.Lark(filter_elements_grammar)
transformer = FacetTransformer(ifc_file, elements) transformer = FacetTransformer(ifc_file, elements)
transformer.transform(l.parse(query)) transformer.transform(filter_elements_grammar.parse(query))
return transformer.get_results() return transformer.get_results()
return transformer.elements return transformer.elements