diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 677b59b9e5..dfa0a30946 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -92,7 +92,7 @@ class AddRepresentation(bpy.types.Operator, Operator): Data.load(tool.Ifc.get(), element.id()) -class SwitchRepresentation(bpy.types.Operator): +class SwitchRepresentation(bpy.types.Operator, Operator): bl_idname = "bim.switch_representation" bl_label = "Switch Representation" bl_options = {"REGISTER", "UNDO"} @@ -102,77 +102,15 @@ class SwitchRepresentation(bpy.types.Operator): disable_opening_subtractions: bpy.props.BoolProperty() should_switch_all_meshes: bpy.props.BoolProperty() - def execute(self, context): - self.element_obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.oprops = self.element_obj.BIMObjectProperties - - self.file = IfcStore.get_file() - self.context_of_items = self.file.by_id(self.ifc_definition_id).ContextOfItems - self.mesh_name = self.get_mesh_name() - - mesh = bpy.data.meshes.get(self.mesh_name) - if mesh: - self.switch_mesh(mesh) - if not mesh or self.should_reload: - self.pull_mesh_from_ifc(context) - return {"FINISHED"} - - def switch_mesh(self, mesh): - if self.should_switch_all_meshes or self.file.by_id(self.oprops.ifc_definition_id).is_a("IfcTypeProduct"): - self.element_obj.data.user_remap(mesh) - else: - self.element_obj.data = mesh - - def get_mesh_name(self): - representation = self.resolve_mapped_representation(self.file.by_id(self.ifc_definition_id)) - return "{}/{}".format(self.context_of_items.id(), representation.id()) - - def resolve_mapped_representation(self, representation): - if representation.RepresentationType == "MappedRepresentation": - return self.resolve_mapped_representation(representation.Items[0].MappingSource.MappedRepresentation) - return representation - - def pull_mesh_from_ifc(self, context): - logger = logging.getLogger("ImportIFC") - ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) - element = self.file.by_id(self.oprops.ifc_definition_id) - settings = ifcopenshell.geom.settings() - - if self.context_of_items.ContextIdentifier == "Body": - if element.is_a("IfcTypeProduct") or self.disable_opening_subtractions: - shape = ifcopenshell.geom.create_shape(settings, self.file.by_id(self.ifc_definition_id)) - else: - shape = ifcopenshell.geom.create_shape(settings, self.file.by_id(self.oprops.ifc_definition_id)) - else: - settings.set(settings.INCLUDE_CURVES, True) - shape = ifcopenshell.geom.create_shape(settings, self.file.by_id(self.ifc_definition_id)) - - ifc_importer = import_ifc.IfcImporter(ifc_import_settings) - ifc_importer.file = self.file - mesh = ifc_importer.create_mesh(element, shape) - mesh.name = self.mesh_name - mesh.BIMMeshProperties.ifc_definition_id = self.ifc_definition_id - self.switch_mesh(mesh) - material_creator = import_ifc.MaterialCreator(ifc_import_settings, ifc_importer) - material_creator.load_existing_materials() - material_creator.create(element, self.element_obj, mesh) - - if self.disable_opening_subtractions and self.context_of_items.ContextIdentifier == "Body": - if self.oprops.ifc_definition_id not in VoidData.products: - VoidData.load(self.file, self.oprops.ifc_definition_id) - for opening_id in VoidData.products[self.oprops.ifc_definition_id]: - opening = IfcStore.get_element(opening_id) - if not opening: - continue - modifier = self.element_obj.modifiers.new("IfcOpeningElement", "BOOLEAN") - modifier.operation = "DIFFERENCE" - modifier.object = opening - modifier.solver = "EXACT" - modifier.use_self = True - else: - for modifier in self.element_obj.modifiers: - if modifier.type == "BOOLEAN" and "IfcOpeningElement" in modifier.name: - self.element_obj.modifiers.remove(modifier) + def _execute(self, context): + core.switch_representation( + tool.Geometry, + obj=bpy.data.objects.get(self.obj) if self.obj else context.active_object, + representation=tool.Ifc.get().by_id(self.ifc_definition_id), + should_reload=self.should_reload, + enable_dynamic_voids=self.disable_opening_subtractions, + is_global=self.should_switch_all_meshes, + ) class RemoveRepresentation(bpy.types.Operator, Operator): @@ -324,8 +262,13 @@ class UpdateParametricRepresentation(bpy.types.Operator): parameter = props.ifc_parameters[self.index] self.file.by_id(parameter.step_id)[parameter.index] = parameter.value show_representation_parameters = bool(props.ifc_parameters) - bpy.ops.bim.switch_representation( - ifc_definition_id=props.ifc_definition_id, should_reload=True, should_switch_all_meshes=True + core.switch_representation( + tool.Geometry, + obj=obj, + representation=tool.Ifc.get().by_id(props.ifc_definition_id), + should_reload=True, + enable_dynamic_voids=False, + is_global=True, ) if show_representation_parameters: bpy.ops.bim.get_representation_ifc_parameters() diff --git a/src/blenderbim/blenderbim/bim/module/model/opening.py b/src/blenderbim/blenderbim/bim/module/model/opening.py index c075a8046f..5cddf5583d 100644 --- a/src/blenderbim/blenderbim/bim/module/model/opening.py +++ b/src/blenderbim/blenderbim/bim/module/model/opening.py @@ -21,6 +21,8 @@ import bmesh import blenderbim.bim.handler import ifcopenshell import ifcopenshell.util.representation +import blenderbim.tool as tool +import blenderbim.core.geometry from blenderbim.bim.ifc import IfcStore from math import pi from mathutils import Vector @@ -57,12 +59,13 @@ def mode_callback(obj, data): ) if not representation: continue - bpy.ops.bim.switch_representation( - obj=building_element_obj.name, - should_switch_all_meshes=True, + blenderbim.core.geometry.switch_representation( + tool.Geometry, + obj=building_element_obj, + representation=representation, should_reload=True, - ifc_definition_id=representation.id(), - disable_opening_subtractions=True, + enable_dynamic_voids=True, + is_global=True, ) IfcStore.edited_objs.add(obj) bm = bmesh.from_edit_mesh(obj.data) diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py index fc7ed03907..6504201fbb 100644 --- a/src/blenderbim/blenderbim/bim/module/model/product.py +++ b/src/blenderbim/blenderbim/bim/module/model/product.py @@ -22,6 +22,8 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.util.element import ifcopenshell.util.representation +import blenderbim.tool as tool +import blenderbim.core.geometry from . import wall, slab, profile from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.pset.data import Data as PsetData @@ -229,12 +231,13 @@ class DynamicallyVoidProduct(bpy.types.Operator): was_edit_mode = obj.mode == "EDIT" if was_edit_mode: bpy.ops.object.mode_set(mode="OBJECT") - bpy.ops.bim.switch_representation( - obj=obj.name, - should_switch_all_meshes=True, + blenderbim.core.geometry.switch_representation( + tool.Geometry, + obj=obj, + representation=representation, should_reload=True, - ifc_definition_id=representation.id(), - disable_opening_subtractions=True, + enable_dynamic_voids=True, + is_global=True, ) if was_edit_mode: bpy.ops.object.mode_set(mode="EDIT") @@ -286,8 +289,13 @@ def regenerate_profile_usage(usecase_path, ifc_file, settings): continue representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") if representation: - bpy.ops.bim.switch_representation( - obj=obj.name, ifc_definition_id=representation.id(), should_reload=True, should_switch_all_meshes=True + blenderbim.core.geometry.switch_representation( + tool.Geometry, + obj=obj, + representation=representation, + should_reload=True, + enable_dynamic_voids=True, + is_global=True, ) diff --git a/src/blenderbim/blenderbim/bim/module/model/profile.py b/src/blenderbim/blenderbim/bim/module/model/profile.py index d7ecf7dc5e..c1d513e21b 100644 --- a/src/blenderbim/blenderbim/bim/module/model/profile.py +++ b/src/blenderbim/blenderbim/bim/module/model/profile.py @@ -182,8 +182,13 @@ class DumbProfileGenerator: profile_set_usage=profile_set_usage, ) representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") - bpy.ops.bim.switch_representation( - obj=obj.name, ifc_definition_id=representation.id(), should_reload=True, should_switch_all_meshes=True + blenderbim.core.geometry.switch_representation( + tool.Geometry, + obj=obj, + representation=representation, + should_reload=True, + enable_dynamic_voids=True, + is_global=True, ) pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric") ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbProfile"}) @@ -239,8 +244,13 @@ class DumbProfileRegenerator: return representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") if representation: - bpy.ops.bim.switch_representation( - obj=obj.name, ifc_definition_id=representation.id(), should_reload=True, should_switch_all_meshes=True + blenderbim.core.geometry.switch_representation( + tool.Geometry, + obj=obj, + representation=representation, + should_reload=True, + enable_dynamic_voids=True, + is_global=True, ) def sync_object(self, element): diff --git a/src/blenderbim/blenderbim/bim/module/type/operator.py b/src/blenderbim/blenderbim/bim/module/type/operator.py index 4a95206fe9..7fe9d35817 100644 --- a/src/blenderbim/blenderbim/bim/module/type/operator.py +++ b/src/blenderbim/blenderbim/bim/module/type/operator.py @@ -20,6 +20,8 @@ import bpy import ifcopenshell.util.schema import ifcopenshell.util.type import ifcopenshell.api +import blenderbim.tool as tool +import blenderbim.core.geometry from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.type.data import Data from ifcopenshell.api.geometry.data import Data as GeometryData @@ -64,13 +66,23 @@ class AssignType(bpy.types.Operator): for representation_id in representation_ids: representation = GeometryData.representations[representation_id] if representation["ContextOfItems"]["ContextIdentifier"] == "Body": - bpy.ops.bim.switch_representation( - obj=related_object.name, ifc_definition_id=representation_id, should_switch_all_meshes=False + blenderbim.core.geometry.switch_representation( + tool.Geometry, + obj=related_object, + representation=tool.Ifc.get().by_id(representation_id), + should_reload=False, + enable_dynamic_voids=False, + is_global=False, ) has_switched = True if not has_switched and representation_ids: - bpy.ops.bim.switch_representation( - obj=related_object.name, ifc_definition_id=representation_id, should_switch_all_meshes=False + blenderbim.core.geometry.switch_representation( + tool.Geometry, + obj=related_object, + representation=tool.Ifc.get().by_id(representation_id), + should_reload=False, + enable_dynamic_voids=False, + is_global=False, ) bpy.ops.bim.disable_editing_type(obj=related_object.name) diff --git a/src/blenderbim/blenderbim/bim/module/void/operator.py b/src/blenderbim/blenderbim/bim/module/void/operator.py index 7565d33e68..589ec6cd5d 100644 --- a/src/blenderbim/blenderbim/bim/module/void/operator.py +++ b/src/blenderbim/blenderbim/bim/module/void/operator.py @@ -19,6 +19,8 @@ import bpy import ifcopenshell.api import ifcopenshell.util.representation +import blenderbim.tool as tool +import blenderbim.core.geometry from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.void.data import Data @@ -120,10 +122,13 @@ class RemoveOpening(bpy.types.Operator): ifcopenshell.api.run("void.remove_opening", self.file, **{"opening": self.file.by_id(self.opening_id)}) if not is_modifier_removed: - bpy.ops.bim.switch_representation( - ifc_definition_id=obj.data.BIMMeshProperties.ifc_definition_id, + blenderbim.core.geometry.switch_representation( + tool.Geometry, + obj=obj, + representation=tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id), should_reload=True, - should_switch_all_meshes=True, + enable_dynamic_voids=False, + is_global=True, ) Data.load(IfcStore.get_file(), obj.BIMObjectProperties.ifc_definition_id) diff --git a/src/blenderbim/blenderbim/core/geometry.py b/src/blenderbim/blenderbim/core/geometry.py index 54ea40ecea..87a61d8544 100644 --- a/src/blenderbim/blenderbim/core/geometry.py +++ b/src/blenderbim/blenderbim/core/geometry.py @@ -66,8 +66,32 @@ def add_representation( ifc.run("geometry.assign_representation", product=element, representation=representation) data = geometry.duplicate_object_data(obj) - name = geometry.get_representation_name(context, representation) - geometry.rename_object_data(data, name) + name = geometry.get_representation_name(representation) + geometry.rename_object(data, name) geometry.link(representation, data) return representation + + +def switch_representation( + geometry, + obj=None, + representation=None, + should_reload=True, + enable_dynamic_voids=True, + is_global=True, +): + representation = geometry.resolve_mapped_representation(representation) + data = geometry.get_representation_data(representation) + + if not data or should_reload: + data = geometry.import_representation(obj, representation, enable_dynamic_voids=enable_dynamic_voids) + geometry.rename_object(data, geometry.get_representation_name(representation)) + geometry.link(representation, data) + + geometry.change_object_data(obj, data, is_global=is_global) + + if enable_dynamic_voids and geometry.is_body_representation(representation): + geometry.create_dynamic_voids(obj) + else: + geometry.clear_dynamic_voids(obj) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 7c9670d601..49f8605377 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -65,15 +65,22 @@ class Context: @interface class Geometry: + def change_object_data(cls, obj, data, is_global=False): pass + def clear_dynamic_voids(cls, obj): pass + def create_dynamic_voids(cls, obj): pass def does_object_have_mesh_with_faces(cls, obj): pass def duplicate_object_data(cls, obj): pass + def get_cartesian_point_coordinate_offset(cls, obj): pass def get_object_data(cls, obj): pass def get_object_materials_without_styles(cls, obj): pass - def get_representation_name(cls, context, representation): pass - def get_cartesian_point_coordinate_offset(cls, obj): pass + def get_representation_data(cls, representation): pass + def get_representation_name(cls, representation): pass def get_total_representation_items(cls, obj): pass + def import_representation(cls, obj, representation, enable_dynamic_voids=False): pass + def is_body_representation(cls, representation): pass def link(cls, element, obj): pass - def rename_object_data(cls, data, name): pass + def rename_object(cls, obj, name): pass + def resolve_mapped_representation(cls, representation): pass def should_force_faceted_brep(cls): pass def should_force_triangulation(cls): pass def should_use_presentation_style_assignment(cls): pass diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index 6718de77ea..9c8f66beae 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -17,13 +17,42 @@ # along with BlenderBIM Add-on. If not, see . import bpy +import logging +import ifcopenshell import blenderbim.core.tool import blenderbim.tool as tool +import blenderbim.bim.import_ifc from mathutils import Vector from blenderbim.bim.ifc import IfcStore class Geometry(blenderbim.core.tool.Geometry): + @classmethod + def change_object_data(cls, obj, data, is_global=False): + if is_global: + obj.data.user_remap(data) + else: + obj.data = data + + @classmethod + def clear_dynamic_voids(cls, obj): + for modifier in obj.modifiers: + if modifier.type == "BOOLEAN" and "IfcOpeningElement" in modifier.name: + obj.modifiers.remove(modifier) + + @classmethod + def create_dynamic_voids(cls, obj): + element = tool.Ifc.get_entity(obj) + for rel in element.HasOpenings: + opening_obj = tool.Ifc.get_object(rel.RelatedOpeningElement) + if not opening_obj: + continue + modifier = obj.modifiers.new("IfcOpeningElement", "BOOLEAN") + modifier.operation = "DIFFERENCE" + modifier.object = opening_obj + modifier.solver = "EXACT" + modifier.use_self = True + @classmethod def does_object_have_mesh_with_faces(cls, obj): return bool(isinstance(obj.data, bpy.types.Mesh) and len(obj.data.polygons)) @@ -44,8 +73,12 @@ class Geometry(blenderbim.core.tool.Geometry): ] @classmethod - def get_representation_name(cls, context, representation): - return f"{context.id()}/{representation.id()}" + def get_representation_data(cls, representation): + return bpy.data.meshes.get(cls.get_representation_name(representation)) + + @classmethod + def get_representation_name(cls, representation): + return f"{representation.ContextOfItems.id()}/{representation.id()}" @classmethod def get_cartesian_point_coordinate_offset(cls, obj): @@ -63,13 +96,46 @@ class Geometry(blenderbim.core.tool.Geometry): def get_total_representation_items(cls, obj): return max(1, len(obj.material_slots)) + @classmethod + def import_representation(cls, obj, representation, enable_dynamic_voids=False): + logger = logging.getLogger("ImportIFC") + ifc_import_settings = blenderbim.bim.import_ifc.IfcImportSettings.factory(bpy.context, None, logger) + element = tool.Ifc.get_entity(obj) + settings = ifcopenshell.geom.settings() + + if representation.ContextOfItems.ContextIdentifier == "Body": + if element.is_a("IfcTypeProduct") or enable_dynamic_voids: + shape = ifcopenshell.geom.create_shape(settings, representation) + else: + shape = ifcopenshell.geom.create_shape(settings, element) + else: + settings.set(settings.INCLUDE_CURVES, True) + shape = ifcopenshell.geom.create_shape(settings, representation) + + ifc_importer = blenderbim.bim.import_ifc.IfcImporter(ifc_import_settings) + ifc_importer.file = tool.Ifc.get() + mesh = ifc_importer.create_mesh(element, shape) + ifc_importer.material_creator.load_existing_materials() + ifc_importer.material_creator.create(element, obj, mesh) + return mesh + + @classmethod + def is_body_representation(cls, representation): + return representation.ContextOfItems.ContextIdentifier == "Body" + @classmethod def link(cls, element, obj): obj.BIMMeshProperties.ifc_definition_id = element.id() @classmethod - def rename_object_data(cls, data, name): - data.name = name + def rename_object(cls, obj, name): + obj.name = name + + @classmethod + def resolve_mapped_representation(cls, representation): + if representation.RepresentationType == "MappedRepresentation": + return cls.resolve_mapped_representation(representation.Items[0].MappingSource.MappedRepresentation) + return representation @classmethod def should_force_faceted_brep(cls): diff --git a/src/blenderbim/test/bim/feature/geometry.feature b/src/blenderbim/test/bim/feature/geometry.feature index 0b57df3fae..57744dbb93 100644 --- a/src/blenderbim/test/bim/feature/geometry.feature +++ b/src/blenderbim/test/bim/feature/geometry.feature @@ -23,6 +23,16 @@ Scenario: Add representation And I press "bim.add_representation" Then nothing happens +Scenario: Switch representation + 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" + When the variable "representation" is "{ifc}.by_type('IfcShapeRepresentation')[0].id()" + And I press "bim.switch_representation(obj='IfcWall/Cube', ifc_definition_id={representation})" + Then nothing happens + Scenario: Copy representation Given an empty IFC project And I add a cube diff --git a/src/blenderbim/test/core/test_geometry.py b/src/blenderbim/test/core/test_geometry.py index 09a21b8b94..40189ec2e2 100644 --- a/src/blenderbim/test/core/test_geometry.py +++ b/src/blenderbim/test/core/test_geometry.py @@ -76,8 +76,8 @@ class TestAddRepresentation: # Update mesh geometry.duplicate_object_data("obj").should_be_called().will_return("data") - geometry.get_representation_name("context", "representation").should_be_called().will_return("name") - geometry.rename_object_data("data", "name").should_be_called() + geometry.get_representation_name("representation").should_be_called().will_return("name") + geometry.rename_object("data", "name").should_be_called() geometry.link("representation", "data").should_be_called() assert ( @@ -124,8 +124,8 @@ class TestAddRepresentation: # Update mesh geometry.duplicate_object_data("obj").should_be_called().will_return("data") - geometry.get_representation_name("context", "representation").should_be_called().will_return("name") - geometry.rename_object_data("data", "name").should_be_called() + geometry.get_representation_name("representation").should_be_called().will_return("name") + geometry.rename_object("data", "name").should_be_called() geometry.link("representation", "data").should_be_called() assert ( @@ -176,3 +176,26 @@ class TestAddRepresentation: ) is None ) + + +class TestSwitchRepresentation: + def test_run(self, ifc, geometry, style, surveyor): + geometry.resolve_mapped_representation("mapped_rep").should_be_called().will_return("representation") + geometry.get_representation_data("representation").should_be_called().will_return("data") + geometry.import_representation( + "obj", "representation", enable_dynamic_voids=True + ).should_be_called().will_return("new_data") + geometry.get_representation_name("representation").should_be_called().will_return("name") + geometry.rename_object("new_data", "name").should_be_called() + geometry.link("representation", "new_data").should_be_called() + geometry.change_object_data("obj", "new_data", is_global=True).should_be_called() + geometry.is_body_representation("representation").should_be_called().will_return(True) + geometry.create_dynamic_voids("obj").should_be_called() + subject.switch_representation( + geometry, + obj="obj", + representation="mapped_rep", + should_reload=True, + enable_dynamic_voids=True, + is_global=True, + ) diff --git a/src/blenderbim/test/files/annotation.ifc b/src/blenderbim/test/files/annotation.ifc new file mode 100644 index 0000000000..7d076c56d9 --- /dev/null +++ b/src/blenderbim/test/files/annotation.ifc @@ -0,0 +1,77 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1'); +FILE_NAME('annotation.ifc','2021-10-19T11:50:01+11:00',(),(),'IfcOpenShell v0.7.0-6cc7d796','BlenderBIM 0.0.999999','Nobody'); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCPERSON('HSeldon','Seldon','Hari',$,$,$,$,$); +#2=IFCORGANIZATION('APTR','Aperture Science',$,$,$); +#3=IFCPERSONANDORGANIZATION(#1,#2,$); +#4=IFCACTORROLE(.USERDEFINED.,'CONTRIBUTOR',$); +#5=IFCTELECOMADDRESS(.USERDEFINED.,$,'WEBPAGE',$,$,$,$,'https://ifcopenshell.org',$); +#6=IFCORGANIZATION('IfcOpenShell','IfcOpenShell','IfcOpenShell is an open source software library that helps users and software developers to work with IFC data.',(#4),(#5)); +#7=IFCAPPLICATION(#6,'0.0.999999','BlenderBIM Add-on','BlenderBIM'); +#8=IFCOWNERHISTORY(#3,#7,.READWRITE.,.ADDED.,1634604558,#3,#7,1634604558); +#9=IFCPROJECT('0NqkoFmWPBxvsDLeGa5rDc',#8,'My Project',$,$,$,$,(#18,#24),#13); +#10=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.); +#11=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.); +#12=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.); +#13=IFCUNITASSIGNMENT((#11,#12,#10)); +#14=IFCCARTESIANPOINT((0.,0.,0.)); +#15=IFCDIRECTION((0.,0.,1.)); +#16=IFCDIRECTION((1.,0.,0.)); +#17=IFCAXIS2PLACEMENT3D(#14,#15,#16); +#18=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#17,$); +#19=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#18,$,.MODEL_VIEW.,$); +#20=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Box','Model',*,*,*,*,#18,$,.MODEL_VIEW.,$); +#21=IFCCARTESIANPOINT((0.,0.,0.)); +#22=IFCDIRECTION((1.,0.,0.)); +#23=IFCAXIS2PLACEMENT2D(#21,#22); +#24=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Plan',2,1.E-05,#23,$); +#25=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Plan',*,*,*,*,#24,$,.PLAN_VIEW.,$); +#26=IFCOWNERHISTORY(#3,#7,.READWRITE.,.MODIFIED.,1634604558,#3,#7,1634604558); +#27=IFCSITE('3kJImD6Rr4NfpxM218DNpq',#26,'My Site',$,$,#53,$,$,$,$,$,$,$,$); +#33=IFCOWNERHISTORY(#3,#7,.READWRITE.,.MODIFIED.,1634604558,#3,#7,1634604558); +#34=IFCBUILDING('3afq36fJb80A_bTAB9RB0f',#33,'My Building',$,$,#60,$,$,$,$,$,$); +#40=IFCOWNERHISTORY(#3,#7,.READWRITE.,.MODIFIED.,1634604558,#3,#7,1634604558); +#41=IFCBUILDINGSTOREY('2vQTqYOy13G8THztmBknz3',#40,'My Storey',$,$,#67,$,$,$,$); +#47=IFCOWNERHISTORY(#3,#7,.READWRITE.,.ADDED.,1634604558,#3,#7,1634604558); +#48=IFCRELAGGREGATES('22R8p4a0z4jAFLUof7ha5H',#47,$,$,#9,(#27)); +#49=IFCCARTESIANPOINT((0.,0.,0.)); +#50=IFCDIRECTION((0.,0.,1.)); +#51=IFCDIRECTION((1.,0.,0.)); +#52=IFCAXIS2PLACEMENT3D(#49,#50,#51); +#53=IFCLOCALPLACEMENT($,#52); +#54=IFCOWNERHISTORY(#3,#7,.READWRITE.,.ADDED.,1634604558,#3,#7,1634604558); +#55=IFCRELAGGREGATES('1ty5GrCRX0vPrGpg4F_2JL',#54,$,$,#27,(#34)); +#56=IFCCARTESIANPOINT((0.,0.,0.)); +#57=IFCDIRECTION((0.,0.,1.)); +#58=IFCDIRECTION((1.,0.,0.)); +#59=IFCAXIS2PLACEMENT3D(#56,#57,#58); +#60=IFCLOCALPLACEMENT(#53,#59); +#61=IFCOWNERHISTORY(#3,#7,.READWRITE.,.ADDED.,1634604558,#3,#7,1634604558); +#62=IFCRELAGGREGATES('1Lbqbq3O56pg1ouvCD5WR9',#61,$,$,#34,(#41)); +#63=IFCCARTESIANPOINT((0.,0.,0.)); +#64=IFCDIRECTION((0.,0.,1.)); +#65=IFCDIRECTION((1.,0.,0.)); +#66=IFCAXIS2PLACEMENT3D(#63,#64,#65); +#67=IFCLOCALPLACEMENT(#60,#66); +#68=IFCOWNERHISTORY(#3,#7,.READWRITE.,.MODIFIED.,1634604586,#3,#7,1634604586); +#69=IFCWALL('2iJrnC5QfEH8KDFgXqLR5J',#68,'Square',$,$,#88,#81,$,.ELEMENTEDWALL.); +#75=IFCCARTESIANPOINTLIST2D(((1.,1.),(1.,-1.),(-1.,1.),(-1.,-1.))); +#76=IFCINDEXEDPOLYCURVE(#75,(IFCLINEINDEX((3,4))),$); +#77=IFCINDEXEDPOLYCURVE(#75,(IFCLINEINDEX((1,3))),$); +#78=IFCINDEXEDPOLYCURVE(#75,(IFCLINEINDEX((4,2)),IFCLINEINDEX((2,1))),$); +#79=IFCGEOMETRICCURVESET((#76,#77,#78)); +#80=IFCSHAPEREPRESENTATION(#25,'Annotation','Annotation2D',(#79)); +#81=IFCPRODUCTDEFINITIONSHAPE($,$,(#80)); +#82=IFCOWNERHISTORY(#3,#7,.READWRITE.,.ADDED.,1634604586,#3,#7,1634604586); +#83=IFCRELCONTAINEDINSPATIALSTRUCTURE('1AG29wImP3hgw6L3xSIacB',#82,$,$,(#69),#41); +#84=IFCCARTESIANPOINT((0.,0.,0.)); +#85=IFCDIRECTION((0.,0.,1.)); +#86=IFCDIRECTION((1.,0.,0.)); +#87=IFCAXIS2PLACEMENT3D(#84,#85,#86); +#88=IFCLOCALPLACEMENT(#67,#87); +ENDSEC; +END-ISO-10303-21; diff --git a/src/blenderbim/test/tool/test_geometry.py b/src/blenderbim/test/tool/test_geometry.py index d874128452..5cdef17800 100644 --- a/src/blenderbim/test/tool/test_geometry.py +++ b/src/blenderbim/test/tool/test_geometry.py @@ -33,6 +33,55 @@ class TestImplementsTool(test.bim.bootstrap.NewFile): assert isinstance(subject(), blenderbim.core.tool.Geometry) +class TestChangeObjectData(test.bim.bootstrap.NewFile): + def test_change_single_object_data(self): + data1 = bpy.data.meshes.new("Mesh") + data2 = bpy.data.meshes.new("Mesh") + obj1 = bpy.data.objects.new("Object", data1) + obj2 = bpy.data.objects.new("Object", data1) + subject.change_object_data(obj1, data2, is_global=False) + assert obj1.data == data2 + assert obj2.data == data1 + + def test_change_object_data_globally(self): + data1 = bpy.data.meshes.new("Mesh") + data2 = bpy.data.meshes.new("Mesh") + obj1 = bpy.data.objects.new("Object", data1) + obj2 = bpy.data.objects.new("Object", data1) + subject.change_object_data(obj1, data2, is_global=True) + assert obj1.data == data2 + assert obj2.data == data2 + + +class TestClearDynamicVoids(test.bim.bootstrap.NewFile): + def test_run(self): + obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) + obj.modifiers.new("IfcOpeningElement", "BOOLEAN") + subject.clear_dynamic_voids(obj) + assert len(obj.modifiers) == 0 + + +class TestCreateDynamicVoids(test.bim.bootstrap.NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + wall = ifc.createIfcWall() + opening = ifc.createIfcOpeningElement() + ifcopenshell.api.run("void.add_opening", ifc, opening=opening, element=wall) + wall_obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) + opening_obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) + tool.Ifc.link(wall, wall_obj) + tool.Ifc.link(opening, opening_obj) + subject.create_dynamic_voids(wall_obj) + modifier = wall_obj.modifiers[0] + assert modifier.type == "BOOLEAN" + assert modifier.name == "IfcOpeningElement" + assert modifier.operation == "DIFFERENCE" + assert modifier.object == opening_obj + assert modifier.solver == "EXACT" + assert modifier.use_self is True + + class TestDoesObjectHaveMeshWithFaces(test.bim.bootstrap.NewFile): def test_empties_return_false(self): obj = bpy.data.objects.new("Object", None) @@ -81,12 +130,23 @@ class TestGetObjectMaterialsWithoutStyles(test.bim.bootstrap.NewFile): assert subject.get_object_materials_without_styles(obj) == [material1, material2] +class TestGetRepresentationData(test.bim.bootstrap.NewFile): + def test_run(self): + ifc = ifcopenshell.file() + context = ifc.createIfcGeometricRepresentationContext() + representation = ifc.createIfcShapeRepresentation() + representation.ContextOfItems = context + data = bpy.data.meshes.new("1/2") + assert subject.get_representation_data(representation) == data + + class TestGetRepresentationName(test.bim.bootstrap.NewFile): def test_run(self): ifc = ifcopenshell.file() context = ifc.createIfcGeometricRepresentationContext() representation = ifc.createIfcShapeRepresentation() - assert subject.get_representation_name(context, representation) == f"{context.id()}/{representation.id()}" + representation.ContextOfItems = context + assert subject.get_representation_name(representation) == f"{context.id()}/{representation.id()}" class TestGetCartesianPointCoordinateOffset(test.bim.bootstrap.NewFile): @@ -127,6 +187,44 @@ class TestGetTotalRepresentationItems(test.bim.bootstrap.NewFile): assert subject.get_total_representation_items(obj) == 2 +class TestImportRepresentation(test.bim.bootstrap.NewFile): + def test_importing_a_normal_shape(self): + ifc = ifcopenshell.open("test/files/basic.ifc") + tool.Ifc.set(ifc) + obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) + material = bpy.data.materials.new("Material") + material.BIMMaterialProperties.ifc_style_id = 101 + element = ifc.by_type("IfcWall")[0] + tool.Ifc.link(element, obj) + representation = element.Representation.Representations[0] + mesh = subject.import_representation(obj, representation, enable_dynamic_voids=False) + assert len(mesh.polygons) == 12 + assert mesh.materials[0] == material + + def test_importing_non_body_curves(self): + ifc = ifcopenshell.open("test/files/annotation.ifc") + tool.Ifc.set(ifc) + obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) + element = ifc.by_type("IfcWall")[0] + tool.Ifc.link(element, obj) + representation = element.Representation.Representations[0] + mesh = subject.import_representation(obj, representation, enable_dynamic_voids=False) + assert len(mesh.polygons) == 0 + assert len(mesh.edges) == 4 + + +class TestIsBodyRepresentation(test.bim.bootstrap.NewFile): + def test_run(self): + ifc = ifcopenshell.file() + context = ifc.createIfcGeometricRepresentationContext() + representation = ifc.createIfcShapeRepresentation() + representation.ContextOfItems = context + context.ContextIdentifier = "Body" + assert subject.is_body_representation(representation) is True + context.ContextIdentifier = "Clearance" + assert subject.is_body_representation(representation) is False + + class TestLink(test.bim.bootstrap.NewFile): def test_run(self): ifc = ifcopenshell.file() @@ -139,10 +237,27 @@ class TestLink(test.bim.bootstrap.NewFile): class TestRenameObjectData(test.bim.bootstrap.NewFile): def test_run(self): obj = bpy.data.meshes.new("Mesh") - subject.rename_object_data(obj, "name") + subject.rename_object(obj, "name") assert obj.name == "name" +class TestResolveMappedRepresentation(test.bim.bootstrap.NewFile): + def test_run(self): + ifc = ifcopenshell.file() + mapped_representation = ifc.createIfcShapeRepresentation() + representation = ifc.createIfcShapeRepresentation() + mapped_representation.RepresentationType = "MappedRepresentation" + mapped_representation.Items = [ + ifc.createIfcMappedItem(MappingSource=ifc.createIfcRepresentationMap(MappedRepresentation=representation)) + ] + assert subject.resolve_mapped_representation(mapped_representation) == representation + + def test_return_a_representation_if_not_mapped(self): + ifc = ifcopenshell.file() + representation = ifc.createIfcShapeRepresentation() + assert subject.resolve_mapped_representation(representation) == representation + + class TestShouldForceFacetedBrep(test.bim.bootstrap.NewFile): def test_run(self): result = bpy.context.scene.BIMGeometryProperties.should_force_faceted_brep