Merge branch 'IfcOpenShell:v0.7.0' into v0.7.0

This commit is contained in:
Carlos Villagrasa
2022-07-22 18:42:25 +02:00
committed by GitHub
36 changed files with 475 additions and 71 deletions
+9
View File
@@ -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
@@ -31,6 +31,7 @@ classes = (
operator.EnableEditingGroup,
operator.DisableEditingGroup,
operator.SelectGroupProducts,
operator.UpdateGroup,
prop.Group,
prop.BIMGroupProperties,
ui.BIM_PT_groups,
@@ -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"}
@@ -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)
@@ -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
@@ -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)
@@ -38,6 +38,7 @@ classes = (
operator.SaveSelectorQuery,
operator.OpenQueryLibrary,
operator.LoadQuery,
operator.AddToIfcGroup,
prop.BIMFilterClasses,
prop.BIMFilterBuildingStoreys,
prop.BIMSearchProperties,
@@ -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"}
@@ -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):
@@ -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)
+8 -2
View File
@@ -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 :
+3 -3
View File
@@ -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):
+14 -3
View File
@@ -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.
+32 -8
View File
@@ -16,16 +16,40 @@
# 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 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)
@@ -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
+9
View File
@@ -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}"'))
+2 -2
View File
@@ -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")
+2 -2
View File
@@ -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")
+2 -2
View File
@@ -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,)