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.ifc_definition_id = group.id()
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.has_children = False
new.is_expanded = group.id() in self.expanded_groups
@@ -31,10 +31,12 @@ classes = (
operator.FilterModelElements,
operator.IfcSelector,
operator.LoadQuery,
operator.LoadSearch,
operator.OpenQueryLibrary,
operator.RemoveFilter,
operator.RemoveFilterGroup,
operator.ResetObjectColours,
operator.SaveSearch,
operator.SaveSelectorQuery,
operator.Search,
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 bpy
import json
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.selector
@@ -120,48 +121,9 @@ class Search(Operator):
def execute(self, context):
props = context.scene.BIMSearchProperties
query = []
for filter_group in 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)
results = ifcopenshell.util.selector.filter_elements(
tool.Ifc.get(), tool.Search.export_filter_query(props.filter_groups)
)
total_selected = 0
for element in results:
@@ -171,17 +133,47 @@ class Search(Operator):
self.report({"INFO"}, f"{len(results)} Results")
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):
if value.startswith("/") and value.endswith("/"):
return value
elif value in ("NULL", "TRUE", "FALSE"):
return value
return '"' + value + '"'
class SaveSearch(Operator, tool.Ifc.Operator):
bl_idname = "bim.save_search"
bl_label = "Save Search"
bl_description = "Save search filter to an IFC group"
bl_options = {"REGISTER", "UNDO"}
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):
@@ -22,6 +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 bpy.types import PropertyGroup
from blenderbim.tool.ifc import Ifc
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):
if self.is_selected:
for obj in self.unselected_objects:
@@ -106,6 +113,7 @@ class BIMSearchProperties(PropertyGroup):
("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_ignorecase: BoolProperty(name="Search Ignoring Case", default=True)
global_id: StringProperty(name="GlobalId")
@@ -18,7 +18,7 @@
import bpy
from bpy.types import Panel
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.search.data import SearchData
class BIM_PT_search(Panel):
@@ -30,8 +30,18 @@ class BIM_PT_search(Panel):
bl_parent_id = "BIM_PT_selection"
def draw(self, context):
if not SearchData.is_loaded:
SearchData.load()
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.operator("bim.add_filter_group", text="Add Search Group", icon="ADD")
+162
View File
@@ -1,11 +1,173 @@
import bpy
import json
import lark
import blenderbim.core.tool
import blenderbim.tool as tool
import ifcopenshell.util.selector
from ifcopenshell.util.selector import Selector
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
def from_selector_query(cls, query):
"""Returns a list of products from a selector 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
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):
l = lark.Lark(
"""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):
l = lark.Lark(
"""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
"""
)
l = lark.Lark(filter_elements_grammar)
transformer = FacetTransformer(ifc_file, elements)
transformer.transform(l.parse(query))
return transformer.get_results()