From 6e6cbcef6f5206a8a4b93aaac2d14cb2d2631fb3 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 4 Oct 2021 13:14:38 +1100 Subject: [PATCH] Test edit object placement. See #1711 --- .../bim/module/geometry/operator.py | 42 ++++----------- .../blenderbim/core/tool/__init__.py | 1 + src/blenderbim/blenderbim/core/tool/ifc.py | 5 ++ .../blenderbim/core/tool/surveyor.py | 8 +++ src/blenderbim/blenderbim/tool/__init__.py | 1 + src/blenderbim/blenderbim/tool/ifc.py | 12 ++++- src/blenderbim/blenderbim/tool/surveyor.py | 25 +++++++++ .../test/bim/feature/geometry.feature | 13 +++++ src/blenderbim/test/bim/test_feature.py | 5 +- src/blenderbim/test/core/bootstrap.py | 7 +++ src/blenderbim/test/core/test_geometry.py | 9 ++++ src/blenderbim/test/tool/test_ifc.py | 54 +++++++++++++++---- src/blenderbim/test/tool/test_surveyor.py | 39 ++++++++++++++ .../ifcopenshell/api/unit/add_si_unit.py | 6 ++- .../ifcopenshell/util/geolocation.py | 4 +- .../test/api/unit/test_add_si_unit.py | 10 ++++ 16 files changed, 192 insertions(+), 49 deletions(-) create mode 100644 src/blenderbim/blenderbim/core/tool/surveyor.py create mode 100644 src/blenderbim/blenderbim/tool/surveyor.py create mode 100644 src/blenderbim/test/bim/feature/geometry.feature create mode 100644 src/blenderbim/test/core/test_geometry.py create mode 100644 src/blenderbim/test/tool/test_surveyor.py create mode 100644 src/ifcopenshell-python/test/api/unit/test_add_si_unit.py diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 565dce4095..cd682f52a0 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -24,6 +24,8 @@ import ifcopenshell.util.element import ifcopenshell.util.representation import logging import ifcopenshell.api +import blenderbim.core.geometry as core +import blenderbim.tool as tool from blenderbim.bim.ifc import IfcStore from blenderbim.bim import import_ifc from ifcopenshell.api.geometry.data import Data @@ -32,46 +34,22 @@ from ifcopenshell.api.void.data import Data as VoidData from mathutils import Vector -class EditObjectPlacement(bpy.types.Operator): +class Operator: + def execute(self, context): + IfcStore.execute_ifc_operator(self, context) + return {"FINISHED"} + + +class EditObjectPlacement(bpy.types.Operator, Operator): bl_idname = "bim.edit_object_placement" bl_label = "Edit Object Placement" bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() - def execute(self, context): - return IfcStore.execute_ifc_operator(self, context) - def _execute(self, context): objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects - self.file = IfcStore.get_file() - # TODO: determine how to deal with this module dependency - props = context.scene.BIMGeoreferenceProperties for obj in objs: - if not obj.BIMObjectProperties.ifc_definition_id: - continue - matrix = np.array(obj.matrix_world) - if props.has_blender_offset and obj.BIMObjectProperties.blender_offset_type == "OBJECT_PLACEMENT": - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) - # TODO: np.array? Why not matrix? - matrix = np.array( - ifcopenshell.util.geolocation.local2global( - np.matrix(obj.matrix_world), - float(props.blender_eastings) * unit_scale, - float(props.blender_northings) * unit_scale, - float(props.blender_orthogonal_height) * unit_scale, - float(props.blender_x_axis_abscissa), - float(props.blender_x_axis_ordinate), - ) - ) - ifcopenshell.api.run( - "geometry.edit_object_placement", - self.file, - **{ - "product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id), - "matrix": matrix, - }, - ) - return {"FINISHED"} + core.edit_object_placement(tool.Ifc, tool.Surveyor, obj=obj) class AddRepresentation(bpy.types.Operator): diff --git a/src/blenderbim/blenderbim/core/tool/__init__.py b/src/blenderbim/blenderbim/core/tool/__init__.py index 33eb9d7515..36768c367f 100644 --- a/src/blenderbim/blenderbim/core/tool/__init__.py +++ b/src/blenderbim/blenderbim/core/tool/__init__.py @@ -24,3 +24,4 @@ from blenderbim.core.tool.address_editor import AddressEditor from blenderbim.core.tool.organisation_editor import OrganisationEditor from blenderbim.core.tool.context_editor import ContextEditor from blenderbim.core.tool.owner import Owner +from blenderbim.core.tool.surveyor import Surveyor diff --git a/src/blenderbim/blenderbim/core/tool/ifc.py b/src/blenderbim/blenderbim/core/tool/ifc.py index 07c59144b4..43b406338f 100644 --- a/src/blenderbim/blenderbim/core/tool/ifc.py +++ b/src/blenderbim/blenderbim/core/tool/ifc.py @@ -24,3 +24,8 @@ class Ifc(abc.ABC): @abc.abstractmethod def run(cls, command, **kwargs): pass + + @classmethod + @abc.abstractmethod + def get_entity(cls, obj): + pass diff --git a/src/blenderbim/blenderbim/core/tool/surveyor.py b/src/blenderbim/blenderbim/core/tool/surveyor.py new file mode 100644 index 0000000000..7d64571ee5 --- /dev/null +++ b/src/blenderbim/blenderbim/core/tool/surveyor.py @@ -0,0 +1,8 @@ +import abc + + +class Surveyor(abc.ABC): + @classmethod + @abc.abstractmethod + def get_absolute_matrix(cls, obj): + pass diff --git a/src/blenderbim/blenderbim/tool/__init__.py b/src/blenderbim/blenderbim/tool/__init__.py index f39d65c33d..15254bd9fb 100644 --- a/src/blenderbim/blenderbim/tool/__init__.py +++ b/src/blenderbim/blenderbim/tool/__init__.py @@ -24,3 +24,4 @@ from blenderbim.tool.address_editor import AddressEditor from blenderbim.tool.organisation_editor import OrganisationEditor from blenderbim.tool.context_editor import ContextEditor from blenderbim.tool.owner import Owner +from blenderbim.tool.surveyor import Surveyor diff --git a/src/blenderbim/blenderbim/tool/ifc.py b/src/blenderbim/blenderbim/tool/ifc.py index 3386238efa..a5bf1f2889 100644 --- a/src/blenderbim/blenderbim/tool/ifc.py +++ b/src/blenderbim/blenderbim/tool/ifc.py @@ -22,7 +22,7 @@ import blenderbim.core.tool from blenderbim.bim.ifc import IfcStore -class Ifc: +class Ifc(blenderbim.core.tool.ifc.Ifc): @classmethod def run(cls, command, **kwargs): return ifcopenshell.api.run(command, IfcStore.get_file(), **kwargs) @@ -38,3 +38,13 @@ class Ifc: @classmethod def get_schema(cls): return IfcStore.get_file().schema + + @classmethod + def get_entity(cls, obj): + ifc = IfcStore.get_file() + props = getattr(obj, "BIMObjectProperties", None) + if ifc and props and props.ifc_definition_id: + try: + return IfcStore.get_file().by_id(props.ifc_definition_id) + except: + pass diff --git a/src/blenderbim/blenderbim/tool/surveyor.py b/src/blenderbim/blenderbim/tool/surveyor.py new file mode 100644 index 0000000000..b963733c42 --- /dev/null +++ b/src/blenderbim/blenderbim/tool/surveyor.py @@ -0,0 +1,25 @@ +import bpy +import ifcopenshell.api +import blenderbim.core.tool +import blenderbim.tool as tool +import numpy as np + + +class Surveyor(blenderbim.core.tool.surveyor.Surveyor): + @classmethod + def get_absolute_matrix(cls, obj): + matrix = np.array(obj.matrix_world) + props = bpy.context.scene.BIMGeoreferenceProperties + if props.has_blender_offset and obj.BIMObjectProperties.blender_offset_type == "OBJECT_PLACEMENT": + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + matrix = np.array( + ifcopenshell.util.geolocation.local2global( + matrix, + float(props.blender_eastings) * unit_scale, + float(props.blender_northings) * unit_scale, + float(props.blender_orthogonal_height) * unit_scale, + float(props.blender_x_axis_abscissa), + float(props.blender_x_axis_ordinate), + ) + ) + return matrix diff --git a/src/blenderbim/test/bim/feature/geometry.feature b/src/blenderbim/test/bim/feature/geometry.feature new file mode 100644 index 0000000000..5360165f60 --- /dev/null +++ b/src/blenderbim/test/bim/feature/geometry.feature @@ -0,0 +1,13 @@ +@geometry +Feature: Geometry + Covers geometry and coordinate manipulation + +Scenario: Edit object placement + 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 the object "IfcWall/Cube" is selected + When I press "bim.edit_object_placement" + Then nothing happens diff --git a/src/blenderbim/test/bim/test_feature.py b/src/blenderbim/test/bim/test_feature.py index 6261d35bfe..e0679dc6c0 100644 --- a/src/blenderbim/test/bim/test_feature.py +++ b/src/blenderbim/test/bim/test_feature.py @@ -42,8 +42,8 @@ def an_empty_ifc_project(): bpy.ops.bim.create_project() -@when("I add a cube") @given("I add a cube") +@when("I add a cube") def i_add_a_cube(): bpy.ops.mesh.primitive_cube_add() @@ -53,6 +53,7 @@ def i_add_a_cube_of_size_size_at_location(size, location): bpy.ops.mesh.primitive_cube_add(size=float(size), location=[float(co) for co in location.split(",")]) +@given(parsers.parse('I press "{operator}"')) @when(parsers.parse('I press "{operator}"')) def i_press_operator(operator): operator = replace_variables(operator) @@ -68,6 +69,7 @@ def i_deselect_all_objects(): bpy.ops.object.select_all(action="DESELECT") +@given(parsers.parse('the object "{name}" is selected')) @when(parsers.parse('the object "{name}" is selected')) def the_object_name_is_selected(name): i_deselect_all_objects() @@ -83,6 +85,7 @@ def additionally_the_object_name_is_selected(name): obj.select_set(True) +@given(parsers.parse('I set "{prop}" to "{value}"')) @when(parsers.parse('I set "{prop}" to "{value}"')) def i_set_prop_to_value(prop, value): try: diff --git a/src/blenderbim/test/core/bootstrap.py b/src/blenderbim/test/core/bootstrap.py index 86a13dad43..d9ed0646a9 100644 --- a/src/blenderbim/test/core/bootstrap.py +++ b/src/blenderbim/test/core/bootstrap.py @@ -77,6 +77,13 @@ def owner(): prophet.verify() +@pytest.fixture +def surveyor(): + prophet = Prophecy(blenderbim.core.tool.Surveyor) + yield prophet + prophet.verify() + + class Prophecy: def __init__(self, cls): self.subject = cls diff --git a/src/blenderbim/test/core/test_geometry.py b/src/blenderbim/test/core/test_geometry.py new file mode 100644 index 0000000000..bc24457b19 --- /dev/null +++ b/src/blenderbim/test/core/test_geometry.py @@ -0,0 +1,9 @@ +import blenderbim.core.geometry as subject +from test.core.bootstrap import ifc, surveyor + +class TestEditObjectPlacement: + def test_run(self, ifc, surveyor): + ifc.get_entity("obj").should_be_called().will_return("element") + surveyor.get_absolute_matrix("obj").should_be_called().will_return("matrix") + ifc.run("geometry.edit_object_placement", product="element", matrix="matrix").should_be_called() + subject.edit_object_placement(ifc, surveyor, obj="obj") diff --git a/src/blenderbim/test/tool/test_ifc.py b/src/blenderbim/test/tool/test_ifc.py index 791868a963..a24b15bb09 100644 --- a/src/blenderbim/test/tool/test_ifc.py +++ b/src/blenderbim/test/tool/test_ifc.py @@ -19,40 +19,72 @@ import bpy import ifcopenshell import test.bim.bootstrap +import blenderbim.core.tool.ifc from blenderbim.tool import Ifc as subject +class TestImplementsTool(test.bim.bootstrap.NewFile): + def test_run(self): + assert isinstance(subject(), blenderbim.core.tool.ifc.Ifc) + + class TestSet(test.bim.bootstrap.NewFile): def test_setting_an_ifc_data(self): ifc = ifcopenshell.file() - subject().set(ifc) - assert subject().get() == ifc + subject.set(ifc) + assert subject.get() == ifc class TestGet(test.bim.bootstrap.NewFile): def test_getting_an_ifc_dataset_from_a_ifc_spf_filepath(self): - assert subject().get() is None + assert subject.get() is None bpy.context.scene.BIMProperties.ifc_file = "test/files/basic.ifc" - result = subject().get() + result = subject.get() assert isinstance(result, ifcopenshell.file) def test_getting_the_active_ifc_dataset_regardless_of_ifc_path(self): bpy.context.scene.BIMProperties.ifc_file = "test/files/basic.ifc" ifc = ifcopenshell.file() - subject().set(ifc) - assert subject().get() == ifc + subject.set(ifc) + assert subject.get() == ifc class TestRun(test.bim.bootstrap.NewFile): def test_running_a_command_on_the_active_ifc_dataset(self): ifc = ifcopenshell.file() - subject().set(ifc) - wall = subject().run("root.create_entity", ifc_class="IfcWall") - assert subject().get().by_type("IfcWall")[0] == wall + subject.set(ifc) + wall = subject.run("root.create_entity", ifc_class="IfcWall") + assert subject.get().by_type("IfcWall")[0] == wall class TestGetSchema(test.bim.bootstrap.NewFile): def test_getting_the_schema_version_identifier(self): ifc = ifcopenshell.file(schema="IFC4") - subject().set(ifc) - assert subject().get_schema() == "IFC4" + subject.set(ifc) + assert subject.get_schema() == "IFC4" + + +class TestGetElement(test.bim.bootstrap.NewFile): + def test_run(self): + ifc = ifcopenshell.file() + subject.set(ifc) + obj = bpy.data.objects.new("Object", None) + element = ifc.createIfcWall() + obj.BIMObjectProperties.ifc_definition_id = element.id() + assert subject.get_entity(obj) == element + + def test_attempting_to_get_an_unlinked_object(self): + obj = bpy.data.objects.new("Object", None) + assert subject.get_entity(obj) is None + + def test_attempting_without_a_file(self): + obj = bpy.data.objects.new("Object", None) + obj.BIMObjectProperties.ifc_definition_id = 1 + assert subject.get_entity(obj) is None + + def test_attempting_to_get_an_invalidly_linked_object(self): + ifc = ifcopenshell.file() + subject.set(ifc) + obj = bpy.data.objects.new("Object", None) + obj.BIMObjectProperties.ifc_definition_id = 1 + assert subject.get_entity(obj) is None diff --git a/src/blenderbim/test/tool/test_surveyor.py b/src/blenderbim/test/tool/test_surveyor.py new file mode 100644 index 0000000000..01003f1788 --- /dev/null +++ b/src/blenderbim/test/tool/test_surveyor.py @@ -0,0 +1,39 @@ +import bpy +import ifcopenshell +import ifcopenshell.api +import blenderbim.tool as tool +import test.bim.bootstrap +import blenderbim.core.tool.surveyor +import numpy as np +from blenderbim.tool import Surveyor as subject + + +class TestImplementsTool(test.bim.bootstrap.NewFile): + def test_run(self): + assert isinstance(subject(), blenderbim.core.tool.surveyor.Surveyor) + + +class TestGetGlobalMatrix(test.bim.bootstrap.NewFile): + def test_getting_an_absolute_matrix_if_no_blender_offset(self): + props = bpy.context.scene.BIMGeoreferenceProperties + props.has_blender_offset = False + obj = bpy.data.objects.new("Object", None) + assert (subject.get_absolute_matrix(obj) == np.array(obj.matrix_world)).all() + + def test_applying_an_object_placement_blender_offset(self): + ifc = ifcopenshell.file() + ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") + unit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI") + ifcopenshell.api.run("unit.assign_unit", ifc, units=[unit]) + tool.Ifc.set(ifc) + props = bpy.context.scene.BIMGeoreferenceProperties + props.has_blender_offset = True + props.blender_eastings = "1000" + props.blender_northings = "2000" + props.blender_orthogonal_height = "3000" + props.blender_x_axis_abscissa = "0" + props.blender_x_axis_ordinate = "1" + obj = bpy.data.objects.new("Object", None) + obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT" + matrix = ifcopenshell.util.geolocation.local2global(np.array(obj.matrix_world), 1.0, 2.0, 3.0, 0.0, 1.0) + assert (subject.get_absolute_matrix(obj) == matrix).all() diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/add_si_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/add_si_unit.py index 3a2154bf14..7d53f02ae0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/add_si_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/add_si_unit.py @@ -1,9 +1,11 @@ class Usecase: def __init__(self, file, **settings): self.file = file - self.settings = {"unit_type": "LENGTHUNIT", "name": "METRE"} + self.settings = {"unit_type": "LENGTHUNIT", "name": "METRE", "prefix": None} for key, value in settings.items(): self.settings[key] = value def execute(self): - return self.file.create_entity("IfcSIUnit", UnitType=self.settings["unit_type"], Name=self.settings["name"]) + return self.file.create_entity( + "IfcSIUnit", UnitType=self.settings["unit_type"], Name=self.settings["name"], Prefix=self.settings["prefix"] + ) diff --git a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py index b3f3b568c6..f36c3209fd 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/geolocation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/geolocation.py @@ -53,7 +53,7 @@ def local2global(matrix, eastings, northings, orthogonal_height, x_axis_abscissa x /= np.linalg.norm(x) y = np.cross(np.array([0, 0, 1]), x) intermediate = ( - np.matrix( + np.array( [ [x[0], y[0], 0, 0], [x[1], y[1], 0, 0], @@ -81,7 +81,7 @@ def global2local(matrix, eastings, northings, orthogonal_height, x_axis_abscissa result[2, 3] = (result[2, 3] - orthogonal_height) / scale return ( np.linalg.inv( - np.matrix( + np.array( [ [x[0], y[0], 0, 0], [x[1], y[1], 0, 0], diff --git a/src/ifcopenshell-python/test/api/unit/test_add_si_unit.py b/src/ifcopenshell-python/test/api/unit/test_add_si_unit.py new file mode 100644 index 0000000000..db1bdba08a --- /dev/null +++ b/src/ifcopenshell-python/test/api/unit/test_add_si_unit.py @@ -0,0 +1,10 @@ +import test.bootstrap +import ifcopenshell.api + + +class TestAddSIUnit(test.bootstrap.IFC4): + def test_run(self): + unit = ifcopenshell.api.run("unit.add_si_unit", self.file, unit_type="LENGTHUNIT", name="METRE", prefix="MILLI") + assert unit.UnitType == "LENGTHUNIT" + assert unit.Name == "METRE" + assert unit.Prefix == "MILLI"