diff --git a/src/blenderbim/blenderbim/bim/helper.py b/src/blenderbim/blenderbim/bim/helper.py
index db2c706c2e..06508b50fa 100644
--- a/src/blenderbim/blenderbim/bim/helper.py
+++ b/src/blenderbim/blenderbim/bim/helper.py
@@ -132,6 +132,15 @@ def get_enum_items(data, prop_name, context):
return items
+# hack to close popup
+# https://blender.stackexchange.com/a/202576/130742
+def close_operator_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.01)
+
+
class IfcHeaderExtractor:
def __init__(self, filepath: str):
self.filepath = filepath
diff --git a/src/blenderbim/blenderbim/bim/module/group/__init__.py b/src/blenderbim/blenderbim/bim/module/group/__init__.py
index ddfc4ff48c..1b765a0a94 100644
--- a/src/blenderbim/blenderbim/bim/module/group/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/group/__init__.py
@@ -31,6 +31,7 @@ classes = (
operator.EnableEditingGroup,
operator.DisableEditingGroup,
operator.SelectGroupProducts,
+ operator.UpdateGroup,
prop.Group,
prop.BIMGroupProperties,
ui.BIM_PT_groups,
diff --git a/src/blenderbim/blenderbim/bim/module/group/operator.py b/src/blenderbim/blenderbim/bim/module/group/operator.py
index fdf7548826..c48ea81bcd 100644
--- a/src/blenderbim/blenderbim/bim/module/group/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/group/operator.py
@@ -22,6 +22,7 @@ import ifcopenshell.api
import blenderbim.bim.helper
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.group.data import Data
+from ifcopenshell.util.selector import Selector
class LoadGroups(bpy.types.Operator):
@@ -36,6 +37,7 @@ class LoadGroups(bpy.types.Operator):
new = props.groups.add()
new.ifc_definition_id = ifc_definition_id
new.name = group["Name"]
+ new.selection_query = group["Description"].split("*selector*")[1] if group["Description"] else ""
props.is_editing = True
bpy.ops.bim.disable_editing_group()
return {"FINISHED"}
@@ -167,7 +169,7 @@ class AssignGroup(bpy.types.Operator):
"group.assign_group",
self.file,
**{
- "product": self.file.by_id(product.BIMObjectProperties.ifc_definition_id),
+ "product": [self.file.by_id(product.BIMObjectProperties.ifc_definition_id)],
"group": self.file.by_id(self.group),
}
)
@@ -219,3 +221,31 @@ class SelectGroupProducts(bpy.types.Operator):
if self.group in product_groups:
obj.select_set(True)
return {"FINISHED"}
+
+class UpdateGroup(bpy.types.Operator):
+ bl_idname = "bim.update_group"
+ bl_label = "Update Group"
+ bl_options = {"REGISTER", "UNDO"}
+ query: bpy.props.StringProperty()
+ group_id: bpy.props.IntProperty()
+
+ def execute(self, context):
+ return IfcStore.execute_ifc_operator(self, context)
+
+ def _execute(self, context):
+ self.file = IfcStore.get_file()
+ group = self.file.by_id(self.group_id)
+ query = self.query
+
+ new_products = Selector.parse(self.file, query)
+ ifcopenshell.api.run(
+ "group.update_group_products",
+ self.file,
+ **{
+ "products": new_products,
+ "group": group,
+ }
+ )
+ Data.load(IfcStore.get_file())
+ bpy.ops.bim.load_groups()
+ return {"FINISHED"}
\ No newline at end of file
diff --git a/src/blenderbim/blenderbim/bim/module/group/prop.py b/src/blenderbim/blenderbim/bim/module/group/prop.py
index 03011af513..e97f024584 100644
--- a/src/blenderbim/blenderbim/bim/module/group/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/group/prop.py
@@ -34,7 +34,8 @@ from bpy.props import (
class Group(PropertyGroup):
name: StringProperty(name="Name")
ifc_definition_id: IntProperty(name="IFC Definition ID")
-
+ selection_query: StringProperty(name="Selection Query")
+
class BIMGroupProperties(PropertyGroup):
group_attributes: CollectionProperty(name="Group Attributes", type=Attribute)
diff --git a/src/blenderbim/blenderbim/bim/module/group/ui.py b/src/blenderbim/blenderbim/bim/module/group/ui.py
index ce24d514cc..2c7525132d 100644
--- a/src/blenderbim/blenderbim/bim/module/group/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/group/ui.py
@@ -40,7 +40,7 @@ class BIM_PT_groups(Panel):
self.props = context.scene.BIMGroupProperties
row = self.layout.row(align=True)
- row.label(text="{} Groups Found".format(len(Data.groups)), icon="OUTLINER")
+ row.label(text=f"{len(Data.groups)} Groups Found", icon="OUTLINER")
if self.props.is_editing:
row.operator("bim.add_group", text="", icon="ADD")
row.operator("bim.disable_group_editing_ui", text="", icon="CANCEL")
@@ -118,7 +118,7 @@ class BIM_UL_groups(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
- row.label(text=item.name)
+ row.label(text=f"*{item.name}") if item.selection_query != "" else row.label(text=item.name)
group_id = item.ifc_definition_id
if context.scene.BIMGroupProperties.active_group_id == group_id:
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
@@ -130,6 +130,10 @@ class BIM_UL_groups(UIList):
op.group = group_id
op = row.operator("bim.remove_group", text="", icon="X")
op.group = group_id
+ if item.selection_query != "":
+ op = row.operator("bim.update_group", text="", icon="FILE_REFRESH")
+ op.group_id = item.ifc_definition_id
+ op.query = item.selection_query
else:
op = row.operator("bim.select_group_products", text="", icon="RESTRICT_SELECT_OFF")
op.group = group_id
@@ -137,12 +141,18 @@ class BIM_UL_groups(UIList):
op.group = group_id
op = row.operator("bim.remove_group", text="", icon="X")
op.group = group_id
+ if item.selection_query != "":
+ op = row.operator("bim.update_group", text="", icon="FILE_REFRESH")
+ op.group_id = item.ifc_definition_id
+ op.query = item.selection_query
class BIM_UL_object_groups(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
if item:
row = layout.row(align=True)
- row.label(text=item.name)
+ row.label(text=f"*{item.name}") if item.selection_query != "" else row.label(text=item.name)
+ op = row.operator("bim.remove_group", text="", icon="X")
+ op.group = item.ifc_definition_id
op = row.operator("bim.assign_group", text="", icon="ADD")
op.group = item.ifc_definition_id
diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py
index dd4c00d9c4..f78f1858e1 100644
--- a/src/blenderbim/blenderbim/bim/module/project/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/project/operator.py
@@ -315,12 +315,16 @@ class AppendLibraryElement(bpy.types.Operator):
bl_idname = "bim.append_library_element"
bl_label = "Append Library Element"
bl_options = {"REGISTER", "UNDO"}
+ bl_description = "Append element to the current project"
definition: bpy.props.IntProperty()
prop_index: bpy.props.IntProperty()
@classmethod
def poll(cls, context):
- return IfcStore.get_file()
+ poll = bool(IfcStore.get_file())
+ if bpy.app.version > (3, 0, 0) and not poll:
+ cls.poll_message_set("Please create or load a project first.")
+ return poll
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
diff --git a/src/blenderbim/blenderbim/bim/module/search/__init__.py b/src/blenderbim/blenderbim/bim/module/search/__init__.py
index 0eadf9a42d..57379f90a1 100644
--- a/src/blenderbim/blenderbim/bim/module/search/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/search/__init__.py
@@ -38,6 +38,7 @@ classes = (
operator.SaveSelectorQuery,
operator.OpenQueryLibrary,
operator.LoadQuery,
+ operator.AddToIfcGroup,
prop.BIMFilterClasses,
prop.BIMFilterBuildingStoreys,
prop.BIMSearchProperties,
diff --git a/src/blenderbim/blenderbim/bim/module/search/operator.py b/src/blenderbim/blenderbim/bim/module/search/operator.py
index f1736d85f0..5ff9547041 100644
--- a/src/blenderbim/blenderbim/bim/module/search/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/search/operator.py
@@ -20,9 +20,11 @@ import re
import bpy
import ifcopenshell
import ifcopenshell.util.element
+from ifcopenshell.api.group.data import Data
from ifcopenshell.util.selector import Selector
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
+from blenderbim.bim.helper import close_operator_panel
from itertools import cycle
from bpy.types import PropertyGroup, Operator
from bpy.props import (
@@ -462,7 +464,6 @@ class ActivateIfcBuildingStoreyFilter(Operator):
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"""
@@ -479,7 +480,6 @@ class UnhideAllElements(Operator):
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")
@@ -513,7 +513,7 @@ class FilterModelElements(Operator):
selection = self.add_filters(selection, query)
elif query.selector == "GlobalId":
- selection += f"#{query.global_id}"
+ selection += f"#{query.value}"
elif query.selector == "IfcElementType":
index = int(query.active_sub_option.split(":")[0])
@@ -562,7 +562,6 @@ class FilterModelElements(Operator):
class IfcSelector(Operator):
"""Select elements in model with IFC Selector"""
-
bl_idname = "bim.ifc_selector"
bl_label = "Select elements with IFC Selector"
@@ -603,19 +602,11 @@ class SaveSelectorQuery(Operator):
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)
+ return context.window_manager.invoke_popup(self, width=400)
def draw(self, context):
layout = self.layout
@@ -641,8 +632,37 @@ class LoadQuery(Operator):
bl_idname = "bim.load_query"
bl_label = "Load Query"
index: IntProperty()
-
+
+ def invoke(self, context, event):
+ close_operator_panel(event)
+ return self.execute(context)
+
def execute(self, context):
ifc_selector = context.scene.IfcSelectorProperties
ifc_selector.selector_query_syntax = ifc_selector.query_library[self.index].query
return {"FINISHED"}
+
+class AddToIfcGroup(Operator):
+ bl_idname = "bim.add_to_ifc_group"
+ bl_label = "Add to IFC Group"
+ group_name: StringProperty(name="Group Name")
+
+ 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, "group_name")
+
+ def execute(self, context):
+ self.file = IfcStore.get_file()
+ ifc_selector = context.scene.IfcSelectorProperties
+ selector_query_syntax = ifc_selector.selector_query_syntax
+
+ group = ifcopenshell.api.run("group.add_group", self.file, **{"Name": self.group_name, "Description": f'*selector*{selector_query_syntax}*selector*'})
+ objects = Selector.parse(self.file, selector_query_syntax)
+
+ ifcopenshell.api.run("group.assign_group", self.file, **{"product": objects, "group": group})
+ Data.load(IfcStore.get_file())
+
+ return {"FINISHED"}
diff --git a/src/blenderbim/blenderbim/bim/module/search/ui.py b/src/blenderbim/blenderbim/bim/module/search/ui.py
index 01669337d0..0d0b1be7ca 100644
--- a/src/blenderbim/blenderbim/bim/module/search/ui.py
+++ b/src/blenderbim/blenderbim/bim/module/search/ui.py
@@ -153,7 +153,8 @@ class IfcSelectorUI:
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")
+ op = row.operator("bim.open_query_library", text="Load Query")
+ row.operator("bim.add_to_ifc_group", text="Add to IFC Group")
def draw_query_group_ui(self, ifc_selector, layout):
for index, group in enumerate(ifc_selector.groups):
diff --git a/src/blenderbim/blenderbim/bim/module/structural/operator.py b/src/blenderbim/blenderbim/bim/module/structural/operator.py
index f9ca9d17a8..bbe1a4a016 100644
--- a/src/blenderbim/blenderbim/bim/module/structural/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/structural/operator.py
@@ -662,7 +662,7 @@ class AddStructuralLoadGroup(bpy.types.Operator):
def _execute(self, context):
self.file = IfcStore.get_file()
load_group = ifcopenshell.api.run("structural.add_structural_load_group", self.file)
- ifcopenshell.api.run("group.assign_group", self.file, product=load_group, group=self.file.by_id(self.load_case))
+ ifcopenshell.api.run("group.assign_group", self.file, product=[load_group], group=self.file.by_id(self.load_case))
Data.load(IfcStore.get_file())
return {"FINISHED"}
@@ -754,7 +754,7 @@ class AddStructuralActivity(bpy.types.Operator):
structural_member=element,
)
ifcopenshell.api.run(
- "group.assign_group", self.file, product=activity, group=self.file.by_id(self.load_group)
+ "group.assign_group", self.file, product=[activity], group=self.file.by_id(self.load_group)
)
Data.load(IfcStore.get_file())
bpy.ops.bim.enable_editing_structural_load_group_activities(load_group=self.load_group)
diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py
index ad1ab0553a..83859a2bc0 100644
--- a/src/blenderbim/blenderbim/bim/operator.py
+++ b/src/blenderbim/blenderbim/bim/operator.py
@@ -27,7 +27,7 @@ from . import schema
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.prop import StrProperty
from blenderbim.bim.ui import IFCFileSelector
-from blenderbim.bim.helper import get_enum_items
+from blenderbim.bim.helper import get_enum_items, close_operator_panel
from mathutils import Vector, Matrix, Euler
from math import radians
@@ -531,6 +531,7 @@ def update_enum_property_search_prop(self, context):
for i, prop in enumerate(self.collection_names):
if prop.name == self.dummy_name:
setattr(context.data, self.prop_name, self.collection_identifiers[i].name)
+ close_operator_panel(self)
break
@@ -542,8 +543,11 @@ class BIM_OT_enum_property_search(bpy.types.Operator):
collection_names: bpy.props.CollectionProperty(type=StrProperty)
collection_identifiers: bpy.props.CollectionProperty(type=StrProperty)
prop_name: bpy.props.StringProperty()
+ mouse_x: bpy.props.IntProperty()
+ mouse_y: bpy.props.IntProperty()
def invoke(self, context, event):
+ self.mouse_x, self.mouse_y = event.mouse_x, event.mouse_y
self.clear_collections()
self.data = context.data
items = get_enum_items(self.data, self.prop_name, context)
@@ -551,7 +555,9 @@ class BIM_OT_enum_property_search(bpy.types.Operator):
return {"FINISHED"}
self.add_items_regular(items)
self.add_items_suggestions()
- return context.window_manager.invoke_props_dialog(self)
+ # Cursor is moved when we update dummy_name, set it back
+ context.window.cursor_warp(self.mouse_x, self.mouse_y)
+ return context.window_manager.invoke_props_popup(self, event)
def draw(self, context):
# Mandatory to access context.data in update :
diff --git a/src/blenderbim/blenderbim/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py
index bb836e2908..ab58f5407a 100644
--- a/src/blenderbim/blenderbim/core/drawing.py
+++ b/src/blenderbim/blenderbim/core/drawing.py
@@ -148,7 +148,7 @@ def add_drawing(ifc, collector, drawing, target_view=None, location_hint=None):
)
group = ifc.run("group.add_group")
ifc.run("group.edit_group", group=group, attributes={"Name": drawing_name, "ObjectType": "DRAWING"})
- ifc.run("group.assign_group", group=group, product=element)
+ ifc.run("group.assign_group", group=group, product=[element])
collector.assign(camera)
pset = ifc.run("pset.add_pset", product=element, name="EPset_Drawing")
ifc.run(
@@ -203,7 +203,7 @@ def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None)
context=context,
ifc_representation_class=drawing_tool.get_ifc_representation_class(object_type),
)
- ifc.run("group.assign_group", group=drawing_tool.get_drawing_group(drawing), product=element)
+ ifc.run("group.assign_group", group=drawing_tool.get_drawing_group(drawing), product=[element])
collector.assign(obj)
drawing_tool.enable_editing(obj)
@@ -251,7 +251,7 @@ def sync_references(ifc, collector, drawing_tool, drawing=None):
annotation = drawing_tool.generate_reference_annotation(drawing, reference_element, context)
if annotation:
ifc.run("drawing.assign_product", relating_product=reference_element, related_object=annotation)
- ifc.run("group.assign_group", group=group, product=annotation)
+ ifc.run("group.assign_group", group=group, product=[annotation])
collector.assign(ifc.get_object(annotation))
if reference_obj and ifc.is_moved(reference_obj):
diff --git a/src/blenderbim/docs/devs/running_tests.rst b/src/blenderbim/docs/devs/running_tests.rst
index 165ddd1027..e2559750c5 100644
--- a/src/blenderbim/docs/devs/running_tests.rst
+++ b/src/blenderbim/docs/devs/running_tests.rst
@@ -6,7 +6,8 @@ Running tests
The BlenderBIM Add-on has three layers of tests for each of its three technology
layers. These roughly form a test pyramid, moving from many abstract domain
logic tests, to low-level concrete unit tests, to a minimal number of UI and
-smoke tests. These tests use ``pytest`` as the test framework and runner.
+smoke tests. These tests use ``pytest`` as the test framework and runner, so you
+will need to install ``pytest``.
All development is expected to use test driven development, and so we expect
test coverage to be 100% where it is technically possible to test.
@@ -39,8 +40,8 @@ similar.
Tool tests
----------
-The tool layer tests actual concrete functions. These have the following
-dependencies:
+The tool layer tests actual concrete functions. You will need to install the
+following dependencies:
* pytest-blender, accessible to your system's Python
* Blender executable, accessible to pytest-blender on your system's Python
@@ -51,6 +52,16 @@ dependencies:
You can install the dependencies by running the ``scripts/setup_pytest.py``
script in Blender.
+.. warning::
+
+ The ``scripts/setup_pytest.py`` may not work for all operating systems and
+ installation environments. In this case, you may be required to install the
+ dependencies manually.
+
+ Please be aware that some Blender may come packaged with its own Python,
+ which may be separate to the Python installation on your system. Be sure to
+ install the dependencies to the correct Python environment.
+
Then, run the tests. This will launch Blender headlessly and check the behaviour
of all concrete functions.
diff --git a/src/blenderbim/scripts/setup_pytest.py b/src/blenderbim/scripts/setup_pytest.py
index 262f97587f..2e2f6eea51 100644
--- a/src/blenderbim/scripts/setup_pytest.py
+++ b/src/blenderbim/scripts/setup_pytest.py
@@ -16,16 +16,40 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see .
-import subprocess
import sys
-from pathlib import Path
+import subprocess
+
+print("Here are the detected system paths:")
+print(sys.path)
py_exec = str(sys.executable)
-lib = Path(py_exec).parent.parent / "lib"
+
+print("Detected executable:", py_exec)
subprocess.call([py_exec, "-m", "ensurepip", "--user"])
-subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pip"])
-subprocess.call([py_exec, "-m", "pip", "install", f"--target={str(lib)}", "pytest"])
-subprocess.call([py_exec, "-m", "pip", "install", f"--target={str(lib)}", "pytest-blender"])
-subprocess.call([py_exec, "-m", "pip", "install", f"--target={str(lib)}", "pytest-bdd"])
-subprocess.call([py_exec, "-m", "pip", "install", f"--target={str(lib)}", "pygments"])
+subprocess.call([py_exec, "-m", "pip", "install", "--upgrade", "pip"])
+
+sys_paths = [p for p in sys.path if "site-packages" in p]
+if sys_paths:
+ print("Detected installation directory:", sys_paths[-1])
+ subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "pytest"])
+ subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "pytest-bdd"])
+ subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "pytest-blender"])
+ subprocess.call([py_exec, "-m", "pip", "install", f"--target={sys_paths[-1]}", "pygments"])
+else:
+ print("Could not detect installation directory. Good luck.")
+ subprocess.call([py_exec, "-m", "pip", "install", "pytest"])
+ subprocess.call([py_exec, "-m", "pip", "install", "pytest-bdd"])
+ subprocess.call([py_exec, "-m", "pip", "install", "pytest-blender"])
+ subprocess.call([py_exec, "-m", "pip", "install", "pygments"])
+
+try:
+ import pytest
+ import pytest_bdd
+ import pytest_blender
+ import pygments
+
+ print("Test dependency installation was successful!")
+except Exception as e:
+ print("Installation failed :(")
+ print(e)
diff --git a/src/blenderbim/test/bim/feature/search.feature b/src/blenderbim/test/bim/feature/search.feature
new file mode 100644
index 0000000000..b4070cc05a
--- /dev/null
+++ b/src/blenderbim/test/bim/feature/search.feature
@@ -0,0 +1,17 @@
+@search
+Feature: Search
+
+Scenario: Select all walls
+ Given an empty IFC project
+ And I add a cube
+ And the object "Cube" is selected
+ And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
+ And I press "bim.assign_class"
+ And I add a new group to IfcSelector
+ And I add a new query to IfcSelector
+ And I set "scene.IfcSelectorProperties.groups[0].queries[0].selector" to "IFC Class"
+ And I set "scene.IfcSelectorProperties.groups[0].queries[0].active_option" to "124: IfcWall"
+ When I press "bim.filter_model_elements(option='select')"
+ Then the object "IfcWall/Cube" is selected
+
+
diff --git a/src/blenderbim/test/bim/test_feature.py b/src/blenderbim/test/bim/test_feature.py
index bfd8a8bc87..07249bb456 100644
--- a/src/blenderbim/test/bim/test_feature.py
+++ b/src/blenderbim/test/bim/test_feature.py
@@ -99,6 +99,15 @@ def i_add_a_sun():
def i_add_a_material():
bpy.context.active_object.active_material = bpy.data.materials.new("Material")
+@given("I add a new group to IfcSelector")
+@when("I add a new group to IfcSelector")
+def i_add_a_new_collection_item():
+ bpy.data.scenes["Scene"].IfcSelectorProperties.groups.add()
+
+@given("I add a new query to IfcSelector")
+@when("I add a new query to IfcSelector")
+def i_add_a_new_collection_item():
+ bpy.data.scenes["Scene"].IfcSelectorProperties.groups[0].queries.add()
@given(parsers.parse('the material "{name}" colour is set to "{colour}"'))
@when(parsers.parse('the material "{name}" colour is set to "{colour}"'))
diff --git a/src/blenderbim/test/core/test_drawing.py b/src/blenderbim/test/core/test_drawing.py
index 3746379740..d6cd58d45e 100644
--- a/src/blenderbim/test/core/test_drawing.py
+++ b/src/blenderbim/test/core/test_drawing.py
@@ -223,7 +223,7 @@ class TestAddDrawing:
ifc.run(
"group.edit_group", group="group", attributes={"Name": "name", "ObjectType": "DRAWING"}
).should_be_called()
- ifc.run("group.assign_group", group="group", product="element").should_be_called()
+ ifc.run("group.assign_group", group="group", product=["element"]).should_be_called()
collector.assign("obj").should_be_called()
ifc.run("pset.add_pset", product="element", name="EPset_Drawing").should_be_called().will_return("pset")
ifc.run(
@@ -293,7 +293,7 @@ class TestAddAnnotation:
ifc_representation_class="ifc_representation_class",
).should_be_called().will_return("element")
drawing.get_drawing_group("drawing").should_be_called().will_return("group")
- ifc.run("group.assign_group", group="group", product="element").should_be_called()
+ ifc.run("group.assign_group", group="group", product=["element"]).should_be_called()
collector.assign("obj").should_be_called()
drawing.enable_editing("obj").should_be_called()
subject.add_annotation(ifc, collector, drawing, drawing="drawing", object_type="object_type")
diff --git a/src/blenderbim/test/tool/test_collector.py b/src/blenderbim/test/tool/test_collector.py
index 496d3fc929..f9b7c76b08 100644
--- a/src/blenderbim/test/tool/test_collector.py
+++ b/src/blenderbim/test/tool/test_collector.py
@@ -222,7 +222,7 @@ class TestAssign(NewFile):
tool.Ifc.link(element, element_obj)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get())
group.ObjectType = "DRAWING"
- ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), product=element, group=group)
+ ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), product=[element], group=group)
subject.assign(element_obj)
assert element_obj.users_collection[0].name == "IfcGroup/Unnamed"
assert bpy.data.collections.get("Views").children.get("IfcGroup/Unnamed")
@@ -235,7 +235,7 @@ class TestAssign(NewFile):
tool.Ifc.link(element, element_obj)
group = ifcopenshell.api.run("group.add_group", tool.Ifc.get())
group.ObjectType = "DRAWING"
- ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), product=element, group=group)
+ ifcopenshell.api.run("group.assign_group", tool.Ifc.get(), product=[element], group=group)
subject.assign(element_obj)
assert element_obj.users_collection[0].name == "IfcGroup/Unnamed"
assert bpy.data.collections.get("Views").children.get("IfcGroup/Unnamed")
diff --git a/src/blenderbim/test/tool/test_drawing.py b/src/blenderbim/test/tool/test_drawing.py
index 64048c8192..b55459df99 100644
--- a/src/blenderbim/test/tool/test_drawing.py
+++ b/src/blenderbim/test/tool/test_drawing.py
@@ -260,7 +260,7 @@ class TestGetDrawingGroup(NewFile):
tool.Ifc.set(ifc)
element = ifc.createIfcAnnotation()
group = ifcopenshell.api.run("group.add_group", ifc)
- ifcopenshell.api.run("group.assign_group", ifc, product=element, group=group)
+ ifcopenshell.api.run("group.assign_group", ifc, product=[element], group=group)
assert subject.get_drawing_group(element) == group
@@ -280,7 +280,7 @@ class TestGetGroupElements(NewFile):
tool.Ifc.set(ifc)
element = ifc.createIfcAnnotation()
group = ifcopenshell.api.run("group.add_group", ifc)
- ifcopenshell.api.run("group.assign_group", ifc, product=element, group=group)
+ ifcopenshell.api.run("group.assign_group", ifc, product=[element], group=group)
assert subject.get_group_elements(group) == (element,)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/add_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/add_group.py
index 6a4ebfc2f5..4014fbae97 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/group/add_group.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/group/add_group.py
@@ -23,7 +23,10 @@ import ifcopenshell.api
class Usecase:
def __init__(self, file, **settings):
self.file = file
- self.settings = {}
+ self.settings = {
+ "Name": "Unnamed",
+ "Description": "",
+ }
for key, value in settings.items():
self.settings[key] = value
@@ -33,6 +36,7 @@ class Usecase:
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
- "Name": "Unnamed",
+ "Name": self.settings["Name"],
+ "Description": self.settings["Description"],
}
)
diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py
index b8cdea2d9f..0318caba09 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py
@@ -37,12 +37,13 @@ class Usecase:
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
- "RelatedObjects": [self.settings["product"]],
+ "RelatedObjects": self.settings["product"],
"RelatingGroup": self.settings["group"],
}
)
rel = self.settings["group"].IsGroupedBy[0]
related_objects = set(rel.RelatedObjects) or set()
- related_objects.add(self.settings["product"])
+ for obj in self.settings["product"]:
+ related_objects.add(obj)
rel.RelatedObjects = list(related_objects)
ifcopenshell.api.run("owner.update_owner_history", self.file, **{"element": rel})
diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py
index a47d571ca6..2851b85029 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py
@@ -20,7 +20,8 @@
class Usecase:
def __init__(self, file, **settings):
self.file = file
- self.settings = {"group": None, "attributes": {}}
+ self.settings = {
+ "group": None, "attributes": {}}
for key, value in settings.items():
self.settings[key] = value
diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py b/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py
new file mode 100644
index 0000000000..f7caac2885
--- /dev/null
+++ b/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py
@@ -0,0 +1,33 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2021 Dion Moult
+#
+# This file is part of IfcOpenShell.
+#
+# IfcOpenShell is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcOpenShell 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with IfcOpenShell. If not, see .
+
+
+class Usecase:
+ def __init__(self, file, **settings):
+ self.file = file
+ self.settings = {
+ "group": None,
+ "products": None,
+ }
+ for key, value in settings.items():
+ self.settings[key] = value
+
+ def execute(self):
+ rel = self.settings["group"].IsGroupedBy[0]
+ rel.RelatedObjects = self.settings["products"]
+
diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py
index e2d663cf8f..6f2cdfec05 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py
@@ -41,6 +41,8 @@ class Usecase:
properties = self.settings["pset"].HasProperties or []
elif self.settings["pset"].is_a("IfcQuantitySet"):
properties = self.settings["pset"].Quantities or []
+ elif self.settings["pset"].is_a() in ("IfcMaterialProperties", "IfcProfileProperties"):
+ properties = self.settings["pset"].Properties or []
for prop in properties:
self.file.remove(prop)
self.file.remove(self.settings["pset"])
diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py
index fb98c7a87d..fd4b790350 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/selector.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py
@@ -189,7 +189,7 @@ class Selector:
value = None
for element in elements:
element_value = cls.get_element_value(element, key)
- if element_value is None and value is not None:
+ if element_value is None and value is not None and "not" not in comparison:
continue
if comparison and cls.filter_element(element, element_value, comparison, value):
results.append(element)
diff --git a/src/ifcopenshell-python/test/api/pset/test_remove_pset.py b/src/ifcopenshell-python/test/api/pset/test_remove_pset.py
index 4f5291a3ef..e06ebf1a86 100644
--- a/src/ifcopenshell-python/test/api/pset/test_remove_pset.py
+++ b/src/ifcopenshell-python/test/api/pset/test_remove_pset.py
@@ -29,6 +29,22 @@ class TestRemovePset(test.bootstrap.IFC4):
assert len(self.file.by_type("IfcRelDefinesByProperties")) == 0
assert len(self.file.by_type("IfcPropertySet")) == 0
+ def test_removing_material_psets(self):
+ element = self.file.createIfcMaterial()
+ pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Foo_Bar")
+ assert len(element.HasProperties) == 1
+ ifcopenshell.api.run("pset.remove_pset", self.file, product=element, pset=pset)
+ assert len(element.HasProperties) == 0
+ assert len(self.file.by_type("IfcMaterialProperties")) == 0
+
+ def test_removing_profile_psets(self):
+ element = self.file.createIfcProfileDef()
+ pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Foo_Bar")
+ assert len(element.HasProperties) == 1
+ ifcopenshell.api.run("pset.remove_pset", self.file, product=element, pset=pset)
+ assert len(element.HasProperties) == 0
+ assert len(self.file.by_type("IfcMaterialProperties")) == 0
+
def test_only_unassigning_if_pset_is_used_by_other_elements(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
diff --git a/src/ifcopenshell-python/test/test_ids.py b/src/ifcopenshell-python/test/test_ids.py
index ccd7e20fac..55ac1aa55e 100644
--- a/src/ifcopenshell-python/test/test_ids.py
+++ b/src/ifcopenshell-python/test/test_ids.py
@@ -1111,7 +1111,7 @@ class TestIdsAuthoring(unittest.TestCase):
group = ifcopenshell.api.run("group.add_group", ifc)
facet = ids.partOf.create(entity="IfcGroup")
run("", facet=facet, inst=element, expected=False)
- ifcopenshell.api.run("group.assign_group", ifc, product=element, group=group)
+ ifcopenshell.api.run("group.assign_group", ifc, product=[element], group=group)
run("", facet=facet, inst=element, expected=True)
# An IfcGroup can be passed by subtypes
@@ -1119,7 +1119,7 @@ class TestIdsAuthoring(unittest.TestCase):
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
group = ifc.createIfcInventory()
facet = ids.partOf.create(entity="IfcGroup")
- ifcopenshell.api.run("group.assign_group", ifc, product=element, group=group)
+ ifcopenshell.api.run("group.assign_group", ifc, product=[element], group=group)
run("", facet=facet, inst=element, expected=True)
# An IfcSystem only checks that a system is assigned without any other logic
diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py
index 37a744e6e4..787b4b2b60 100644
--- a/src/ifcopenshell-python/test/util/test_selector.py
+++ b/src/ifcopenshell-python/test/util/test_selector.py
@@ -112,6 +112,27 @@ class TestSelector(test.bootstrap.IFC4):
assert subject.Selector.parse(self.file, '.IfcElement[Name*="oba"]') == [element]
assert subject.Selector.parse(self.file, '.IfcElement[Name*="abc"]') == []
+ def test_selecting_if_value_not_matching(self):
+ element_1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ element_2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ pset_1 = ifcopenshell.api.run("pset.add_pset", self.file, product=element_1, name="Foo_Bar")
+ pset_2 = ifcopenshell.api.run("pset.add_pset", self.file, product=element_2, name="Foo_Bar")
+ ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset_1, properties={"Foo": "Bar"})
+ ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset_2, properties={"Foo": "BOO"})
+ assert subject.Selector.parse(self.file, '.IfcElement[Foo_Bar.Foo != "Bar"]') == [element_2]
+ assert subject.Selector.parse(self.file, '.IfcElement[Foo_Bar.Foo != "BOO"]') == [element_1]
+
+
+ def test_selecting_when_attribute_is_none(self):
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ assert subject.Selector.parse(self.file, '.IfcElement[PredefinedType !="non-existent predefined type"]') == [element]
+
+ def test_selecting_a_property_which_includes_non_standard_characters(self):
+ element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
+ pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="a !%$§&/()?|*-+,€~#@µ^°a")
+ ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"a !%$§&/()?|*-+,€~#@µ^°a": "Bar"})
+ assert subject.Selector.parse(self.file, '.IfcElement[a !%$§&/()?|*-+,€~#@µ^°a.a !%$§&/()?|*-+,€~#@µ^°a="Bar"]') == [element]
+
def test_comparing_if_value_is_in_a_list(self):
element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall")
element.Name = "Foobar"
diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py
index bec92041f9..9b3187ab39 100644
--- a/src/ifctester/ifctester/facet.py
+++ b/src/ifctester/ifctester/facet.py
@@ -42,6 +42,7 @@ def cast_to_value(from_value, to_value):
class Facet:
def __init__(self, *parameters):
+ self.status = None
self.failed_entities = []
self.failed_reasons = []
for i, name in enumerate(self.parameters):
diff --git a/src/ifctester/ifctester/ids.py b/src/ifctester/ifctester/ids.py
index 51fb043a8d..4b8afaf3c5 100644
--- a/src/ifctester/ifctester/ids.py
+++ b/src/ifctester/ifctester/ids.py
@@ -217,7 +217,8 @@ class Specification:
self.applicable_entities.append(element)
for facet in self.requirements:
result = facet(element)
- if not bool(result):
+ facet.status = bool(result)
+ if not facet.status:
self.failed_entities.add(element)
facet.failed_entities.append(element)
facet.failed_reasons.append(str(result))
@@ -227,5 +228,7 @@ class Specification:
self.status = False
elif self.minOccurs != 0 and not self.applicable_entities:
self.status = False
+ for facet in self.requirements:
+ facet.status = False
elif len(self.applicable_entities) > (self.maxOccurs or 1):
self.status = False
diff --git a/src/ifctester/ifctester/reporter.py b/src/ifctester/ifctester/reporter.py
index 6148526033..6a98ecc0c0 100644
--- a/src/ifctester/ifctester/reporter.py
+++ b/src/ifctester/ifctester/reporter.py
@@ -28,10 +28,10 @@ class Reporter:
def report(self, ids):
pass
- def to_string():
+ def to_string(self):
return ""
- def write(filepath):
+ def write(self, filepath):
pass
@@ -142,7 +142,7 @@ class Json(Reporter):
requirements.append(
{
"description": requirement.to_string("requirement"),
- "success": not requirement.failed_entities,
+ "status": requirement.status,
"failed_entities": [
{"reason": requirement.failed_reasons[i], "element": str(e)}
for i, e in enumerate(requirement.failed_entities[0:10])
diff --git a/src/ifctester/test/test_facet.py b/src/ifctester/test/test_facet.py
index 9980eb685b..9f0c7c582e 100644
--- a/src/ifctester/test/test_facet.py
+++ b/src/ifctester/test/test_facet.py
@@ -1157,7 +1157,7 @@ class TestPartOf:
group = ifcopenshell.api.run("group.add_group", ifc)
facet = PartOf(entity="IfcGroup")
run("", facet=facet, inst=element, expected=False)
- ifcopenshell.api.run("group.assign_group", ifc, product=element, group=group)
+ ifcopenshell.api.run("group.assign_group", ifc, product=[element], group=group)
run("", facet=facet, inst=element, expected=True)
# An IfcGroup can be passed by subtypes
@@ -1165,7 +1165,7 @@ class TestPartOf:
element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcElementAssembly")
group = ifc.createIfcInventory()
facet = PartOf(entity="IfcGroup")
- ifcopenshell.api.run("group.assign_group", ifc, product=element, group=group)
+ ifcopenshell.api.run("group.assign_group", ifc, product=[element], group=group)
run("", facet=facet, inst=element, expected=True)
# An IfcSystem only checks that a system is assigned without any other logic
diff --git a/src/ifctester/webapp/app.py b/src/ifctester/webapp/app.py
new file mode 100644
index 0000000000..ca79f7848d
--- /dev/null
+++ b/src/ifctester/webapp/app.py
@@ -0,0 +1,77 @@
+# IfcTester - IDS based model auditing
+# Copyright (C) 2022 Dion Moult
+#
+# This file is part of IfcTester.
+#
+# IfcTester is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcTester 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with IfcTester. If not, see .
+
+
+import os
+import time
+import ifctester
+import ifctester.reporter
+import ifcopenshell
+from flask import Flask, request, send_from_directory
+
+app = Flask(__name__)
+
+
+class Ifc:
+ ifc = None
+ filepath = None
+
+ @classmethod
+ def get(cls, filepath=None):
+ if filepath is None or filepath == cls.filepath:
+ return cls.ifc
+ cls.filepath = filepath
+ cls.ifc = ifcopenshell.open(filepath)
+ return cls.ifc
+
+
+@app.route("/")
+def index():
+ with open("www/index.html") as template:
+ return template.read()
+
+
+@app.route("/.")
+def get_asset(asset, ext):
+ if ext in ("js", "css"):
+ return send_from_directory("www", asset + "." + ext)
+
+
+@app.route("/audit", methods=["POST"])
+def audit():
+ filename = ifcopenshell.guid.new()
+ ids_filepath = os.path.join("uploads", filename + ".ids")
+ ifc_filepath = os.path.join("uploads", filename + ".ifc")
+ request.files.get("ids").save(ids_filepath)
+ request.files.get("ifc").save(ifc_filepath)
+
+ start = time.time()
+ specs = ifctester.open(ids_filepath)
+ ifc = Ifc.get(ifc_filepath)
+ print("Finished loading:", time.time() - start)
+ start = time.time()
+ specs.validate(ifc)
+ print("Finished validating:", time.time() - start)
+ start = time.time()
+
+ os.remove(ids_filepath)
+ os.remove(ifc_filepath)
+
+ engine = ifctester.reporter.Json(specs)
+ engine.report()
+ return engine.to_string()
diff --git a/src/ifctester/webapp/www/app.js b/src/ifctester/webapp/www/app.js
index fe56432c2a..9514820818 100644
--- a/src/ifctester/webapp/www/app.js
+++ b/src/ifctester/webapp/www/app.js
@@ -10,6 +10,7 @@ class IDSContainer extends HTMLElement {
this.filename = 'specifications.ids';
this.ids = null;
this.containerId = crypto.randomUUID();
+ this.isEditing = true;
}
}
@@ -315,6 +316,13 @@ class IDSFacets extends HTMLElement {
}
feather.replace();
}
+
+ showResults(requirements) {
+ var facetElements = this.getElementsByTagName('ids-facet');
+ for (var i=0; i SAVE
-
- AUDIT MODEL
-
+
+
+ AUDIT MODEL
+
+
CLOSE
@@ -139,6 +141,13 @@
+
+
+ 32
+
+
+
+
Optionally write instructions about how to achieve this requirement.
@@ -163,7 +172,7 @@
-
+
diff --git a/src/ifctester/webapp/www/style.css b/src/ifctester/webapp/www/style.css
index 382aa1f57b..fd86d02296 100644
--- a/src/ifctester/webapp/www/style.css
+++ b/src/ifctester/webapp/www/style.css
@@ -270,20 +270,30 @@ ids-spec ids-spec-handle .snippet {
bottom: -5px;
position: relative;
}
-.result-icon {
+ids-result {
display: inline-block;
position: absolute;
margin-left: -30px;
- margin-top: -2px;
+ margin-top: -3px;
cursor: help;
}
-.result-icon.pass {
+ids-result span {
+ position: absolute;
+ float: right;
+ right: 30px;
+ background-color: #333;
+ color: white;
+ font-family: 'Nova Mono';
+ padding: 3px;
+ border-radius: 5px;
+}
+ids-result[name="pass"] {
color: var(--green);
}
-.result-icon.fail {
+ids-result[name="fail"] {
color: var(--red);
}
-.result-icon:hover {
+ids-result:hover {
color: var(--blue);
}
.hidden {