Test edit object placement. See #1711

This commit is contained in:
Dion Moult
2021-10-04 13:14:38 +11:00
parent cd965fd4a9
commit 6e6cbcef6f
16 changed files with 192 additions and 49 deletions
@@ -24,6 +24,8 @@ import ifcopenshell.util.element
import ifcopenshell.util.representation import ifcopenshell.util.representation
import logging import logging
import ifcopenshell.api import ifcopenshell.api
import blenderbim.core.geometry as core
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from blenderbim.bim import import_ifc from blenderbim.bim import import_ifc
from ifcopenshell.api.geometry.data import Data from ifcopenshell.api.geometry.data import Data
@@ -32,46 +34,22 @@ from ifcopenshell.api.void.data import Data as VoidData
from mathutils import Vector 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_idname = "bim.edit_object_placement"
bl_label = "Edit Object Placement" bl_label = "Edit Object Placement"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty() obj: bpy.props.StringProperty()
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context): def _execute(self, context):
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects 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: for obj in objs:
if not obj.BIMObjectProperties.ifc_definition_id: core.edit_object_placement(tool.Ifc, tool.Surveyor, obj=obj)
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"}
class AddRepresentation(bpy.types.Operator): class AddRepresentation(bpy.types.Operator):
@@ -24,3 +24,4 @@ from blenderbim.core.tool.address_editor import AddressEditor
from blenderbim.core.tool.organisation_editor import OrganisationEditor from blenderbim.core.tool.organisation_editor import OrganisationEditor
from blenderbim.core.tool.context_editor import ContextEditor from blenderbim.core.tool.context_editor import ContextEditor
from blenderbim.core.tool.owner import Owner from blenderbim.core.tool.owner import Owner
from blenderbim.core.tool.surveyor import Surveyor
@@ -24,3 +24,8 @@ class Ifc(abc.ABC):
@abc.abstractmethod @abc.abstractmethod
def run(cls, command, **kwargs): def run(cls, command, **kwargs):
pass pass
@classmethod
@abc.abstractmethod
def get_entity(cls, obj):
pass
@@ -0,0 +1,8 @@
import abc
class Surveyor(abc.ABC):
@classmethod
@abc.abstractmethod
def get_absolute_matrix(cls, obj):
pass
@@ -24,3 +24,4 @@ from blenderbim.tool.address_editor import AddressEditor
from blenderbim.tool.organisation_editor import OrganisationEditor from blenderbim.tool.organisation_editor import OrganisationEditor
from blenderbim.tool.context_editor import ContextEditor from blenderbim.tool.context_editor import ContextEditor
from blenderbim.tool.owner import Owner from blenderbim.tool.owner import Owner
from blenderbim.tool.surveyor import Surveyor
+11 -1
View File
@@ -22,7 +22,7 @@ import blenderbim.core.tool
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
class Ifc: class Ifc(blenderbim.core.tool.ifc.Ifc):
@classmethod @classmethod
def run(cls, command, **kwargs): def run(cls, command, **kwargs):
return ifcopenshell.api.run(command, IfcStore.get_file(), **kwargs) return ifcopenshell.api.run(command, IfcStore.get_file(), **kwargs)
@@ -38,3 +38,13 @@ class Ifc:
@classmethod @classmethod
def get_schema(cls): def get_schema(cls):
return IfcStore.get_file().schema 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
@@ -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
@@ -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
+4 -1
View File
@@ -42,8 +42,8 @@ def an_empty_ifc_project():
bpy.ops.bim.create_project() bpy.ops.bim.create_project()
@when("I add a cube")
@given("I add a cube") @given("I add a cube")
@when("I add a cube")
def i_add_a_cube(): def i_add_a_cube():
bpy.ops.mesh.primitive_cube_add() 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(",")]) 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}"')) @when(parsers.parse('I press "{operator}"'))
def i_press_operator(operator): def i_press_operator(operator):
operator = replace_variables(operator) operator = replace_variables(operator)
@@ -68,6 +69,7 @@ def i_deselect_all_objects():
bpy.ops.object.select_all(action="DESELECT") bpy.ops.object.select_all(action="DESELECT")
@given(parsers.parse('the object "{name}" is selected'))
@when(parsers.parse('the object "{name}" is selected')) @when(parsers.parse('the object "{name}" is selected'))
def the_object_name_is_selected(name): def the_object_name_is_selected(name):
i_deselect_all_objects() i_deselect_all_objects()
@@ -83,6 +85,7 @@ def additionally_the_object_name_is_selected(name):
obj.select_set(True) obj.select_set(True)
@given(parsers.parse('I set "{prop}" to "{value}"'))
@when(parsers.parse('I set "{prop}" to "{value}"')) @when(parsers.parse('I set "{prop}" to "{value}"'))
def i_set_prop_to_value(prop, value): def i_set_prop_to_value(prop, value):
try: try:
+7
View File
@@ -77,6 +77,13 @@ def owner():
prophet.verify() prophet.verify()
@pytest.fixture
def surveyor():
prophet = Prophecy(blenderbim.core.tool.Surveyor)
yield prophet
prophet.verify()
class Prophecy: class Prophecy:
def __init__(self, cls): def __init__(self, cls):
self.subject = cls self.subject = cls
@@ -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")
+43 -11
View File
@@ -19,40 +19,72 @@
import bpy import bpy
import ifcopenshell import ifcopenshell
import test.bim.bootstrap import test.bim.bootstrap
import blenderbim.core.tool.ifc
from blenderbim.tool import Ifc as subject 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): class TestSet(test.bim.bootstrap.NewFile):
def test_setting_an_ifc_data(self): def test_setting_an_ifc_data(self):
ifc = ifcopenshell.file() ifc = ifcopenshell.file()
subject().set(ifc) subject.set(ifc)
assert subject().get() == ifc assert subject.get() == ifc
class TestGet(test.bim.bootstrap.NewFile): class TestGet(test.bim.bootstrap.NewFile):
def test_getting_an_ifc_dataset_from_a_ifc_spf_filepath(self): 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" bpy.context.scene.BIMProperties.ifc_file = "test/files/basic.ifc"
result = subject().get() result = subject.get()
assert isinstance(result, ifcopenshell.file) assert isinstance(result, ifcopenshell.file)
def test_getting_the_active_ifc_dataset_regardless_of_ifc_path(self): def test_getting_the_active_ifc_dataset_regardless_of_ifc_path(self):
bpy.context.scene.BIMProperties.ifc_file = "test/files/basic.ifc" bpy.context.scene.BIMProperties.ifc_file = "test/files/basic.ifc"
ifc = ifcopenshell.file() ifc = ifcopenshell.file()
subject().set(ifc) subject.set(ifc)
assert subject().get() == ifc assert subject.get() == ifc
class TestRun(test.bim.bootstrap.NewFile): class TestRun(test.bim.bootstrap.NewFile):
def test_running_a_command_on_the_active_ifc_dataset(self): def test_running_a_command_on_the_active_ifc_dataset(self):
ifc = ifcopenshell.file() ifc = ifcopenshell.file()
subject().set(ifc) subject.set(ifc)
wall = subject().run("root.create_entity", ifc_class="IfcWall") wall = subject.run("root.create_entity", ifc_class="IfcWall")
assert subject().get().by_type("IfcWall")[0] == wall assert subject.get().by_type("IfcWall")[0] == wall
class TestGetSchema(test.bim.bootstrap.NewFile): class TestGetSchema(test.bim.bootstrap.NewFile):
def test_getting_the_schema_version_identifier(self): def test_getting_the_schema_version_identifier(self):
ifc = ifcopenshell.file(schema="IFC4") ifc = ifcopenshell.file(schema="IFC4")
subject().set(ifc) subject.set(ifc)
assert subject().get_schema() == "IFC4" 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
+39
View File
@@ -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()
@@ -1,9 +1,11 @@
class Usecase: class Usecase:
def __init__(self, file, **settings): def __init__(self, file, **settings):
self.file = file 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(): for key, value in settings.items():
self.settings[key] = value self.settings[key] = value
def execute(self): 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"]
)
@@ -53,7 +53,7 @@ def local2global(matrix, eastings, northings, orthogonal_height, x_axis_abscissa
x /= np.linalg.norm(x) x /= np.linalg.norm(x)
y = np.cross(np.array([0, 0, 1]), x) y = np.cross(np.array([0, 0, 1]), x)
intermediate = ( intermediate = (
np.matrix( np.array(
[ [
[x[0], y[0], 0, 0], [x[0], y[0], 0, 0],
[x[1], y[1], 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 result[2, 3] = (result[2, 3] - orthogonal_height) / scale
return ( return (
np.linalg.inv( np.linalg.inv(
np.matrix( np.array(
[ [
[x[0], y[0], 0, 0], [x[0], y[0], 0, 0],
[x[1], y[1], 0, 0], [x[1], y[1], 0, 0],
@@ -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"