Fix bug where copying objects did not necessarily retain the existing context assignment

This commit is contained in:
Dion Moult
2021-10-19 19:14:05 +11:00
parent 816059eefd
commit 6478601f79
11 changed files with 316 additions and 63 deletions
@@ -27,6 +27,7 @@ import ifcopenshell.util.representation
import ifcopenshell.api
import blenderbim.core.geometry as core
import blenderbim.core.style
import blenderbim.core.root
import blenderbim.tool as tool
import blenderbim.bim.handler
from blenderbim.bim.ifc import IfcStore
@@ -421,6 +422,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
obj.select_set(False)
new_obj.select_set(True)
# This is the only difference
bpy.ops.bim.copy_class(obj=new_obj.name)
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Root, obj=new_obj)
bpy.ops.transform.translate("INVOKE_DEFAULT")
blenderbim.bim.handler.purge_module_data()
return {"FINISHED"}
@@ -27,6 +27,7 @@ import ifcopenshell.util.representation
import mathutils.geometry
import blenderbim.bim.handler
import blenderbim.core.type
import blenderbim.core.root
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
from ifcopenshell.api.pset.data import Data as PsetData
@@ -212,7 +213,7 @@ class DumbWallSplitter:
def duplicate_wall(self):
new = self.wall.copy()
self.wall.users_collection[0].objects.link(new)
bpy.ops.bim.copy_class(obj=new.name)
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Root, obj=new)
return new
def snap_end_face_to_point(self, wall, which_end):
@@ -27,11 +27,19 @@ import blenderbim.core.material
import blenderbim.core.spatial
import blenderbim.core.style
import blenderbim.core.type
import blenderbim.core.root as core
import blenderbim.tool as tool
from ifcopenshell.api.void.data import Data as VoidData
from blenderbim.bim.ifc import IfcStore
class Operator:
def execute(self, context):
IfcStore.execute_ifc_operator(self, context)
blenderbim.bim.handler.refresh_ui_data()
return {"FINISHED"}
class EnableReassignClass(bpy.types.Operator):
bl_idname = "bim.enable_reassign_class"
bl_label = "Enable Reassign IFC Class"
@@ -338,58 +346,14 @@ class UnlinkObject(bpy.types.Operator):
return {"FINISHED"}
class CopyClass(bpy.types.Operator):
class CopyClass(bpy.types.Operator, Operator):
bl_idname = "bim.copy_class"
bl_label = "Copy Class"
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
def execute(self, context):
return IfcStore.execute_ifc_operator(self, context)
def _execute(self, context):
self.file = IfcStore.get_file()
if self.obj:
objects = [bpy.data.objects.get(self.obj)]
else:
objects = context.selected_objects
objects = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
for obj in objects:
if not obj.BIMObjectProperties.ifc_definition_id:
continue
old_element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
result = ifcopenshell.api.run("root.copy_class", self.file, **{"product": old_element})
IfcStore.link_element(result, obj)
relating_type = ifcopenshell.util.element.get_type(result)
if relating_type and relating_type.RepresentationMaps:
blenderbim.core.type.assign_type(tool.Ifc, tool.Geometry, tool.Type, element=result, type=relating_type)
else:
blenderbim.core.geometry.add_representation(
tool.Ifc,
tool.Geometry,
tool.Style,
tool.Surveyor,
obj=obj,
context=tool.Ifc.get().by_id(int(context.scene.BIMProperties.contexts)),
ifc_representation_class=None,
profile_set_usage=None,
)
if result.is_a("IfcSpatialElement") or result.is_a("IfcSpatialStructureElement"):
tool.Collector.assign(obj)
elif result.is_a("IfcOpeningElement"):
self.add_opening_modifiers(result, obj)
core.copy_class(tool.Ifc, tool.Collector, tool.Root, obj=obj)
blenderbim.bim.handler.purge_module_data()
return {"FINISHED"}
def add_opening_modifiers(self, result, obj):
for rel in result.VoidsElements:
building_obj = IfcStore.get_element(rel.RelatingBuildingElement.id())
try:
modifier = next(m for m in obj.modifiers if m.type == "BOOLEAN" and m.object == obj)
except StopIteration:
modifier = building_obj.modifiers.new("IfcOpeningElement", "BOOLEAN")
modifier.object = obj
finally:
modifier.operation = "DIFFERENCE"
modifier.solver = "EXACT"
modifier.use_self = True
modifier.operand_type = "OBJECT"
@@ -21,6 +21,7 @@ import ifcopenshell.api
import ifcopenshell.util.element
import blenderbim.tool as tool
import blenderbim.core.spatial as core
import blenderbim.core.root
import blenderbim.bim.handler
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.spatial.data import SpatialData
@@ -122,9 +123,10 @@ class CopyToContainer(bpy.types.Operator):
new_obj = obj.copy()
new_obj.data = obj.data.copy()
new_obj.matrix_world = container_obj.matrix_world @ local_position
bpy.ops.bim.copy_class(obj=new_obj.name)
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Root, obj=new_obj)
core.assign_container(
tool.Ifc, tool.Collector, tool.Container, structure_obj=container_obj, element_obj=new_obj
)
blenderbim.bim.handler.purge_module_data()
obj.BIMObjectSpatialProperties.is_editing = False
return {"FINISHED"}
+33
View File
@@ -0,0 +1,33 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
def copy_class(ifc, collector, root, obj=None):
element = ifc.get_entity(obj)
if not element:
return
element = ifc.run("root.copy_class", product=element)
ifc.link(element, obj)
relating_type = root.get_element_type(element)
if relating_type and root.does_type_have_representations(relating_type):
ifc.run("type.assign_type", related_object=element, relating_type=relating_type)
else:
root.run_geometry_add_representation(obj=obj, context=root.get_object_context(obj))
collector.assign(obj)
if root.is_opening_element(element):
root.add_dynamic_opening_voids(element, obj)
+10
View File
@@ -143,6 +143,16 @@ class Owner:
def set_user(cls, user): pass
@interface
class Root:
def add_dynamic_opening_voids(cls, element, obj): pass
def does_type_have_representations(cls, element): pass
def get_element_type(cls, element): pass
def get_object_context(cls, obj): pass
def is_opening_element(cls, element): pass
def run_geometry_add_representation(cls, obj=None, context=None): pass
@interface
class Selector:
def set_active(cls, obj): pass
@@ -26,6 +26,7 @@ from blenderbim.tool.ifc import Ifc
from blenderbim.tool.material import Material
from blenderbim.tool.misc import Misc
from blenderbim.tool.owner import Owner
from blenderbim.tool.root import Root
from blenderbim.tool.style import Style
from blenderbim.tool.surveyor import Surveyor
from blenderbim.tool.type import Type
+64
View File
@@ -0,0 +1,64 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell
import blenderbim.core.tool
import blenderbim.core.geometry
import blenderbim.tool as tool
from mathutils import Vector
class Root(blenderbim.core.tool.Root):
@classmethod
def add_dynamic_opening_voids(cls, element, obj):
for rel in element.VoidsElements:
building_obj = tool.Ifc.get_object(rel.RelatingBuildingElement)
try:
modifier = next(m for m in obj.modifiers if m.type == "BOOLEAN" and m.object == obj)
except StopIteration:
modifier = building_obj.modifiers.new("IfcOpeningElement", "BOOLEAN")
modifier.object = obj
finally:
modifier.operation = "DIFFERENCE"
modifier.solver = "EXACT"
modifier.use_self = True
modifier.operand_type = "OBJECT"
@classmethod
def does_type_have_representations(cls, element):
return bool(element.RepresentationMaps)
@classmethod
def get_element_type(cls, element):
return ifcopenshell.util.element.get_type(element)
@classmethod
def get_object_context(cls, obj):
if obj.data and obj.data.BIMMeshProperties.ifc_definition_id:
return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id).ContextOfItems
@classmethod
def is_opening_element(cls, element):
return element.is_a("IfcOpeningElement")
@classmethod
def run_geometry_add_representation(cls, obj=None, context=None):
return blenderbim.core.geometry.add_representation(
tool.Ifc, tool.Geometry, tool.Style, tool.Surveyor, obj=obj, context=context
)
+20 -13
View File
@@ -42,6 +42,20 @@ def aggregate():
prophet.verify()
@pytest.fixture
def collector():
prophet = Prophecy(blenderbim.core.tool.Collector)
yield prophet
prophet.verify()
@pytest.fixture
def container():
prophet = Prophecy(blenderbim.core.tool.Container)
yield prophet
prophet.verify()
@pytest.fixture
def context():
prophet = Prophecy(blenderbim.core.tool.Context)
@@ -78,15 +92,8 @@ def owner():
@pytest.fixture
def type():
prophet = Prophecy(blenderbim.core.tool.Type)
yield prophet
prophet.verify()
@pytest.fixture
def voider():
prophet = Prophecy(blenderbim.core.tool.Voider)
def root():
prophet = Prophecy(blenderbim.core.tool.Root)
yield prophet
prophet.verify()
@@ -113,15 +120,15 @@ def surveyor():
@pytest.fixture
def collector():
prophet = Prophecy(blenderbim.core.tool.Collector)
def type():
prophet = Prophecy(blenderbim.core.tool.Type)
yield prophet
prophet.verify()
@pytest.fixture
def container():
prophet = Prophecy(blenderbim.core.tool.Container)
def voider():
prophet = Prophecy(blenderbim.core.tool.Voider)
yield prophet
prophet.verify()
+72
View File
@@ -0,0 +1,72 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import blenderbim.core.root as subject
import test.core.test_geometry
from test.core.bootstrap import ifc, collector, root
class TestCopyClass:
def test_doing_nothing_if_not_an_ifc_element(self, ifc, collector, root):
ifc.get_entity("obj").should_be_called().will_return(None)
subject.copy_class(ifc, collector, root, obj="obj")
def test_copy_with_new_geometry_derived_from_the_type(self, ifc, collector, root):
ifc.get_entity("obj").should_be_called().will_return("original_element")
ifc.run("root.copy_class", product="original_element").should_be_called().will_return("element")
ifc.link("element", "obj").should_be_called()
root.get_element_type("element").should_be_called().will_return("type")
root.does_type_have_representations("type").should_be_called().will_return(True)
ifc.run("type.assign_type", related_object="element", relating_type="type").should_be_called()
collector.assign("obj").should_be_called()
root.is_opening_element("element").should_be_called().will_return(False)
subject.copy_class(ifc, collector, root, obj="obj")
def test_copy_with_new_geometry_added_afresh_for_speed(
self,
ifc,
collector,
root,
):
ifc.get_entity("obj").should_be_called().will_return("original_element")
ifc.run("root.copy_class", product="original_element").should_be_called().will_return("element")
ifc.link("element", "obj").should_be_called()
root.get_element_type("element").should_be_called().will_return("type")
root.does_type_have_representations("type").should_be_called().will_return(False)
root.get_object_context("obj").should_be_called().will_return("context")
root.run_geometry_add_representation(obj="obj", context="context").should_be_called()
collector.assign("obj").should_be_called()
root.is_opening_element("element").should_be_called().will_return(False)
subject.copy_class(ifc, collector, root, obj="obj")
def test_copied_openings_have_dynamic_voids_added(
self,
ifc,
collector,
root,
):
ifc.get_entity("obj").should_be_called().will_return("original_element")
ifc.run("root.copy_class", product="original_element").should_be_called().will_return("element")
ifc.link("element", "obj").should_be_called()
root.get_element_type("element").should_be_called().will_return("type")
root.does_type_have_representations("type").should_be_called().will_return(True)
ifc.run("type.assign_type", related_object="element", relating_type="type").should_be_called()
collector.assign("obj").should_be_called()
root.is_opening_element("element").should_be_called().will_return(True)
root.add_dynamic_opening_voids("element", "obj").should_be_called()
subject.copy_class(ifc, collector, root, obj="obj")
+97
View File
@@ -0,0 +1,97 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
import ifcopenshell
import blenderbim.core.tool
import blenderbim.tool as tool
from test.bim.bootstrap import NewFile
from blenderbim.tool.root import Root as subject
class TestImplementsTool(NewFile):
def test_run(self):
assert isinstance(subject(), blenderbim.core.tool.Root)
class TestAddDynamicOpeningVoids(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh"))
element = ifc.createIfcOpeningElement()
tool.Ifc.link(element, obj)
wall_obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh"))
wall_element = ifc.createIfcOpeningElement()
tool.Ifc.link(wall_element, wall_obj)
ifcopenshell.api.run("void.add_opening", ifc, opening=element, element=wall_element)
subject.add_dynamic_opening_voids(element, obj)
modifier = wall_obj.modifiers[0]
assert modifier.type == "BOOLEAN"
assert modifier.name == "IfcOpeningElement"
assert modifier.operation == "DIFFERENCE"
assert modifier.object == obj
assert modifier.solver == "EXACT"
assert modifier.use_self is True
class TestDoesTypeHaveRepresentations(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
element = ifc.createIfcWallType()
assert subject.does_type_have_representations(element) is False
element.RepresentationMaps = [ifc.createIfcRepresentationMap()]
assert subject.does_type_have_representations(element) is True
class TestGetElementType(NewFile):
def test_run(self):
bpy.ops.bim.create_project()
ifc = tool.Ifc.get()
element = ifc.createIfcWall()
type = ifc.createIfcWallType()
ifcopenshell.api.run("type.assign_type", ifc, related_object=element, relating_type=type)
assert subject.get_element_type(element) == type
class TestGetElementType(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
context = ifc.createIfcGeometricRepresentationContext()
representation = ifc.createIfcShapeRepresentation(ContextOfItems=context)
obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh"))
obj.data.BIMMeshProperties.ifc_definition_id = representation.id()
assert subject.get_object_context(obj) == context
class TestIsOpeningElement(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
assert subject.is_opening_element(ifc.createIfcWall()) is False
assert subject.is_opening_element(ifc.createIfcOpeningElement()) is True
class TestRunGeometryAddRepresntation(NewFile):
def test_nothing(self):
pass