You can now load and save searches from the search panel

This commit is contained in:
Dion Moult
2023-07-22 22:49:22 +10:00
parent 7bbc89a3c3
commit 4396f937b5
8 changed files with 343 additions and 120 deletions
@@ -48,7 +48,7 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator):
new = self.props.groups.add() new = self.props.groups.add()
new.ifc_definition_id = group.id() new.ifc_definition_id = group.id()
new.name = group.Name or "Unnamed" new.name = group.Name or "Unnamed"
new.selection_query = group.Description.split("*selector*")[1] if group.Description else "" new.selection_query = ""
new.tree_depth = tree_depth new.tree_depth = tree_depth
new.has_children = False new.has_children = False
new.is_expanded = group.id() in self.expanded_groups new.is_expanded = group.id() in self.expanded_groups
@@ -31,10 +31,12 @@ classes = (
operator.FilterModelElements, operator.FilterModelElements,
operator.IfcSelector, operator.IfcSelector,
operator.LoadQuery, operator.LoadQuery,
operator.LoadSearch,
operator.OpenQueryLibrary, operator.OpenQueryLibrary,
operator.RemoveFilter, operator.RemoveFilter,
operator.RemoveFilterGroup, operator.RemoveFilterGroup,
operator.ResetObjectColours, operator.ResetObjectColours,
operator.SaveSearch,
operator.SaveSelectorQuery, operator.SaveSelectorQuery,
operator.Search, operator.Search,
operator.SelectAttribute, operator.SelectAttribute,
@@ -0,0 +1,49 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import json
import blenderbim.tool as tool
def refresh():
SearchData.is_loaded = False
class SearchData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.is_loaded = True
cls.data = {}
cls.data["saved_searches"] = cls.saved_searches()
@classmethod
def saved_searches(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":
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")]
@@ -18,6 +18,7 @@
import re import re
import bpy import bpy
import json
import ifcopenshell import ifcopenshell
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.selector import ifcopenshell.util.selector
@@ -120,48 +121,9 @@ class Search(Operator):
def execute(self, context): def execute(self, context):
props = context.scene.BIMSearchProperties props = context.scene.BIMSearchProperties
query = [] results = ifcopenshell.util.selector.filter_elements(
for filter_group in props.filter_groups: tool.Ifc.get(), tool.Search.export_filter_query(props.filter_groups)
filter_group_query = [] )
has_instance_or_entity_filter = False
for ifc_filter in filter_group.filters:
if not ifc_filter.value:
continue
if ifc_filter.type == "instance":
has_instance_or_entity_filter = True
filter_group_query.append(ifc_filter.value)
elif ifc_filter.type == "entity":
has_instance_or_entity_filter = True
filter_group_query.append(ifc_filter.value)
elif ifc_filter.type == "attribute":
if not ifc_filter.name:
continue
comparison, value = self.get_comparison_and_value(ifc_filter)
filter_group_query.append(f"{ifc_filter.name}{comparison}{value}")
elif ifc_filter.type == "type":
comparison, value = self.get_comparison_and_value(ifc_filter)
filter_group_query.append(f"type{comparison}{value}")
elif ifc_filter.type == "material":
comparison, value = self.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 = self.wrap_value(ifc_filter, ifc_filter.pset)
name = self.wrap_value(ifc_filter, ifc_filter.name)
comparison, value = self.get_comparison_and_value(ifc_filter)
filter_group_query.append(f"{pset}.{name}{comparison}{value}")
elif ifc_filter.type == "classification":
comparison, value = self.get_comparison_and_value(ifc_filter)
filter_group_query.append(f"classification{comparison}{value}")
elif ifc_filter.type == "location":
comparison, value = self.get_comparison_and_value(ifc_filter)
filter_group_query.append(f"location{comparison}{value}")
if not has_instance_or_entity_filter:
filter_group_query.insert(0, "IfcElement")
query.append(", ".join(filter_group_query))
query = " + ".join(query)
results = ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), query)
total_selected = 0 total_selected = 0
for element in results: for element in results:
@@ -171,17 +133,47 @@ class Search(Operator):
self.report({"INFO"}, f"{len(results)} Results") self.report({"INFO"}, f"{len(results)} Results")
return {"FINISHED"} return {"FINISHED"}
def get_comparison_and_value(self, ifc_filter):
if ifc_filter.value.startswith("!="):
return ("!=", self.wrap_value(ifc_filter, ifc_filter.value[2:].strip()))
return ("=", self.wrap_value(ifc_filter, ifc_filter.value.strip()))
def wrap_value(self, ifc_filter, value): class SaveSearch(Operator, tool.Ifc.Operator):
if value.startswith("/") and value.endswith("/"): bl_idname = "bim.save_search"
return value bl_label = "Save Search"
elif value in ("NULL", "TRUE", "FALSE"): bl_description = "Save search filter to an IFC group"
return value bl_options = {"REGISTER", "UNDO"}
return '"' + value + '"' name: StringProperty(name="Name")
def _execute(self, context):
if not self.name:
return
query = tool.Search.export_filter_query(context.scene.BIMSearchProperties.filter_groups)
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)
ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), products=results, group=group)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
class LoadSearch(Operator, tool.Ifc.Operator):
bl_idname = "bim.load_search"
bl_label = "Load Search"
bl_description = "Load search filter 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))
query = tool.Search.import_filter_query(group, context.scene.BIMSearchProperties.filter_groups)
def draw(self, context):
props = context.scene.BIMSearchProperties
row = self.layout.row()
row.prop(props, "saved_searches", text="")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
class SelectGlobalId(Operator): class SelectGlobalId(Operator):
@@ -22,6 +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 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
@@ -37,6 +38,12 @@ from bpy.props import (
) )
def get_saved_searches(self, context):
if not SearchData.is_loaded:
SearchData.load()
return SearchData.data["saved_searches"]
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:
@@ -106,6 +113,7 @@ class BIMSearchProperties(PropertyGroup):
("instance", "GlobalId", "", "GRIP", 7), ("instance", "GlobalId", "", "GRIP", 7),
], ],
) )
saved_searches: EnumProperty(items=get_saved_searches, name="Saved Searches")
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.ifc import IfcStore from blenderbim.bim.module.search.data import SearchData
class BIM_PT_search(Panel): class BIM_PT_search(Panel):
@@ -30,8 +30,18 @@ class BIM_PT_search(Panel):
bl_parent_id = "BIM_PT_selection" bl_parent_id = "BIM_PT_selection"
def draw(self, context): def draw(self, context):
if not SearchData.is_loaded:
SearchData.load()
props = context.scene.BIMSearchProperties props = context.scene.BIMSearchProperties
row = self.layout.row(align=True)
row.label(text=f"{len(SearchData.data['saved_searches'])} Saved Searches")
if SearchData.data["saved_searches"]:
row.operator("bim.load_search", text="", icon="IMPORT")
row.operator("bim.save_search", text="", icon="EXPORT")
row = self.layout.row(align=True) row = self.layout.row(align=True)
row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD") row.operator("bim.add_filter_group", text="Add Search Group", icon="ADD")
+162
View File
@@ -1,11 +1,173 @@
import bpy import bpy
import json
import lark
import blenderbim.core.tool import blenderbim.core.tool
import blenderbim.tool as tool import blenderbim.tool as tool
import ifcopenshell.util.selector
from ifcopenshell.util.selector import Selector from ifcopenshell.util.selector import Selector
class Search(blenderbim.core.tool.Search): class Search(blenderbim.core.tool.Search):
@classmethod
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))
@classmethod
def export_filter_query(cls, filter_groups):
query = []
for filter_group in filter_groups:
filter_group_query = []
has_instance_or_entity_filter = False
for ifc_filter in filter_group.filters:
if not ifc_filter.value:
continue
if ifc_filter.type == "instance":
has_instance_or_entity_filter = True
filter_group_query.append(ifc_filter.value)
elif ifc_filter.type == "entity":
has_instance_or_entity_filter = True
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, value = cls.get_comparison_and_value(ifc_filter)
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}")
if not has_instance_or_entity_filter:
filter_group_query.insert(0, "IfcElement")
query.append(", ".join(filter_group_query))
return " + ".join(query)
@classmethod
def get_comparison_and_value(cls, ifc_filter):
if ifc_filter.value.startswith("!="):
return ("!=", cls.wrap_value(ifc_filter, ifc_filter.value[2:].strip()))
return ("=", cls.wrap_value(ifc_filter, ifc_filter.value.strip()))
@classmethod
def wrap_value(cls, ifc_filter, value):
if value.startswith("/") and value.endswith("/"):
return value
elif value in ("NULL", "TRUE", "FALSE"):
return value
return '"' + value.replace('"', '\\"') + '"'
@classmethod @classmethod
def from_selector_query(cls, query): def from_selector_query(cls, query):
"""Returns a list of products from a selector query""" """Returns a list of products from a selector query"""
return Selector().parse(tool.Ifc.get(), query) return Selector().parse(tool.Ifc.get(), query)
class ImportFilterQueryTransformer(lark.Transformer):
def __init__(self, filter_groups):
self.filter_groups = filter_groups
def get_results(self):
results = set()
for r in self.results:
results |= r
return results
def facet_list(self, args):
new = self.filter_groups.add()
for arg in args:
new2 = new.filters.add()
new2.type = arg["type"]
new2.value = arg["value"]
if "name" in arg:
new2.name = arg["name"]
if "pset" in arg:
new2.pset = arg["pset"]
def facet(self, args):
return args[0]
def instance(self, args):
return {"type": "instance", "value": " ".join([a.children[0].value for a in args])}
def entity(self, args):
return {"type": "entity", "value": " ".join([a.children[0].value for a in args])}
def attribute(self, args):
name, comparison, value = args
name = name.children[0].value
return {"type": "attribute", "name": name, "value": f"{comparison}{value}"}
def type(self, args):
comparison, value = args
return {"type": "type", "value": f"{comparison}{value}"}
def material(self, args):
comparison, value = args
return {"type": "material", "value": f"{comparison}{value}"}
def property(self, args):
pset, prop, comparison, value = args
return {"type": "property", "pset": pset, "name": prop, "value": f"{comparison}{value}"}
def classification(self, args):
comparison, value = args
return {"type": "classification", "value": f"{comparison}{value}"}
def location(self, args):
comparison, value = args
return {"type": "location", "value": f"{comparison}{value}"}
def comparison(self, args):
return "" if args[0].data == "equals" else "!="
def pset(self, args):
return self.value(args)
def prop(self, args):
return self.value(args)
def value(self, args):
if args[0].data == "unquoted_string":
return args[0].children[0].value
elif args[0].data == "quoted_string":
return args[0].children[0].value[1:-1].replace('\\"', '"')
elif args[0].data == "regex_string":
return args[0].children[0].value
elif args[0].data == "special":
if args[0].children[0].data == "null":
return "NULL"
elif args[0].children[0].data == "true":
return "TRUE"
elif args[0].children[0].data == "false":
return "FALSE"
def compare(self, element_value, comparison, value):
if isinstance(value, str):
if isinstance(element_value, int):
value = int(value)
elif isinstance(element_value, float):
value = float(value)
result = element_value == value
elif isinstance(value, re.Pattern):
result = bool(value.match(element_value))
elif value in (None, True, False):
result = element_value is value
return result if comparison == "=" else not result
@@ -24,6 +24,71 @@ import ifcopenshell.util.element
import ifcopenshell.util.classification import ifcopenshell.util.classification
filter_elements_grammar = """start: filter_group
filter_group: facet_list ("+" facet_list)*
facet_list: facet ("," facet)*
facet: instance | entity | attribute | type | material | property | classification | location
instance: not? globalid
globalid: /[0-3][a-zA-Z0-9_$]{21}/
entity: not? ifc_class
attribute: attribute_name comparison value
type: "type" comparison value
material: "material" comparison value
property: pset "." prop comparison value
classification: "classification" comparison value
location: "location" comparison value
pset: quoted_string | unquoted_string | regex_string
prop: quoted_string | unquoted_string | regex_string
attribute_name: /[A-Z]\\w+/
ifc_class: /Ifc\\w+/
value: special | quoted_string | unquoted_string | regex_string
unquoted_string: /[^.=\\s]+/
quoted_string: ESCAPED_STRING
regex_string: "/" /[^\\/]+/ "/"
special: null | true | false
comparison: not? equals
not: "!"
equals: "="
null: "NULL"
true: "TRUE"
false: "FALSE"
// Embed common.lark for packaging
DIGIT: "0".."9"
HEXDIGIT: "a".."f"|"A".."F"|DIGIT
INT: DIGIT+
SIGNED_INT: ["+"|"-"] INT
DECIMAL: INT "." INT? | "." INT
_EXP: ("e"|"E") SIGNED_INT
FLOAT: INT _EXP | DECIMAL _EXP?
SIGNED_FLOAT: ["+"|"-"] FLOAT
NUMBER: FLOAT | INT
SIGNED_NUMBER: ["+"|"-"] NUMBER
_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+
CNAME: ("_"|LETTER) ("_"|LETTER|DIGIT)*
WS_INLINE: (" "|/\\t/)+
WS: /[ \\t\\f\\r\\n]/+
CR : /\\r/
LF : /\\n/
NEWLINE: (CR? LF)+
%ignore WS // Disregard spaces in text
"""
def get_element_value(element, query): def get_element_value(element, query):
l = lark.Lark( l = lark.Lark(
"""start: WORD | ESCAPED_STRING | keys_regex | keys_quoted | keys_simple """start: WORD | ESCAPED_STRING | keys_regex | keys_quoted | keys_simple
@@ -50,72 +115,7 @@ def get_element_value(element, query):
def filter_elements(ifc_file, query, elements=None): def filter_elements(ifc_file, query, elements=None):
l = lark.Lark( l = lark.Lark(filter_elements_grammar)
"""start: filter_group
filter_group: facet_list ("+" facet_list)*
facet_list: facet ("," facet)*
facet: instance | entity | attribute | type | material | property | classification | location
instance: not? globalid
globalid: /[0-3][a-zA-Z0-9_$]{21}/
entity: not? ifc_class
attribute: attribute_name comparison value
type: "type" comparison value
material: "material" comparison value
property: pset "." prop comparison value
classification: "classification" comparison value
location: "location" comparison value
pset: quoted_string | unquoted_string | regex_string
prop: quoted_string | unquoted_string | regex_string
attribute_name: /[A-Z]\\w+/
ifc_class: /Ifc\\w+/
value: special | quoted_string | unquoted_string | regex_string
unquoted_string: /[^.=\\s]+/
quoted_string: ESCAPED_STRING
regex_string: "/" /[^\\/]+/ "/"
special: null | true | false
comparison: not? equals
not: "!"
equals: "="
null: "NULL"
true: "TRUE"
false: "FALSE"
// Embed common.lark for packaging
DIGIT: "0".."9"
HEXDIGIT: "a".."f"|"A".."F"|DIGIT
INT: DIGIT+
SIGNED_INT: ["+"|"-"] INT
DECIMAL: INT "." INT? | "." INT
_EXP: ("e"|"E") SIGNED_INT
FLOAT: INT _EXP | DECIMAL _EXP?
SIGNED_FLOAT: ["+"|"-"] FLOAT
NUMBER: FLOAT | INT
SIGNED_NUMBER: ["+"|"-"] NUMBER
_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+
CNAME: ("_"|LETTER) ("_"|LETTER|DIGIT)*
WS_INLINE: (" "|/\\t/)+
WS: /[ \\t\\f\\r\\n]/+
CR : /\\r/
LF : /\\n/
NEWLINE: (CR? LF)+
%ignore WS // Disregard spaces in text
"""
)
transformer = FacetTransformer(ifc_file, elements) transformer = FacetTransformer(ifc_file, elements)
transformer.transform(l.parse(query)) transformer.transform(l.parse(query))
return transformer.get_results() return transformer.get_results()