diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index 7b74547cab..677b59b9e5 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -66,87 +66,30 @@ class AddRepresentation(bpy.types.Operator, Operator): profile_set_usage: bpy.props.IntProperty() def _execute(self, context): + ifc_context = self.context_id or int(context.scene.BIMProperties.contexts or "0") or None + if ifc_context: + ifc_context = tool.Ifc.get().by_id(ifc_context) obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.file = IfcStore.get_file() - - core.edit_object_placement(tool.Ifc, tool.Surveyor, obj=obj) - - if not obj.data: - return {"FINISHED"} - - product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) - - context_id = self.context_id or int(context.scene.BIMProperties.contexts) - context_of_items = self.file.by_id(context_id) - - gprop = context.scene.BIMGeoreferenceProperties - coordinate_offset = None - if gprop.has_blender_offset and obj.BIMObjectProperties.blender_offset_type == "CARTESIAN_POINT": - coordinate_offset = Vector( - ( - float(gprop.blender_eastings), - float(gprop.blender_northings), - float(gprop.blender_orthogonal_height), - ) - ) - - representation_data = { - "context": context_of_items, - "blender_object": obj, - "geometry": obj.data, - "coordinate_offset": coordinate_offset, - "total_items": max(1, len(obj.material_slots)), - "should_force_faceted_brep": context.scene.BIMGeometryProperties.should_force_faceted_brep, - "should_force_triangulation": context.scene.BIMGeometryProperties.should_force_triangulation, - "ifc_representation_class": self.ifc_representation_class, - "profile_set_usage": self.file.by_id(self.profile_set_usage) if self.profile_set_usage else None, - } - - result = ifcopenshell.api.run("geometry.add_representation", self.file, **representation_data) - - if not result: - print("Failed to write shape representation") - return {"FINISHED"} - - [ - blenderbim.core.style.add_style(tool.Ifc, tool.Style, obj=s.material) - for s in obj.material_slots - if s.material and not s.material.BIMMaterialProperties.ifc_style_id - ] - - if isinstance(obj.data, bpy.types.Mesh) and len(obj.data.polygons): - ifcopenshell.api.run( - "style.assign_representation_styles", - self.file, - **{ - "shape_representation": result, - "styles": [ - self.file.by_id(s.material.BIMMaterialProperties.ifc_style_id) - for s in obj.material_slots - if s.material - ], - "should_use_presentation_style_assignment": context.scene.BIMGeometryProperties.should_use_presentation_style_assignment, - }, - ) - ifcopenshell.api.run( - "geometry.assign_representation", self.file, **{"product": product, "representation": result} + core.add_representation( + tool.Ifc, + tool.Geometry, + tool.Style, + tool.Surveyor, + obj=obj, + context=ifc_context, + ifc_representation_class=self.ifc_representation_class, + profile_set_usage=tool.Ifc.get().by_id(self.profile_set_usage) if self.profile_set_usage else None, ) - - mesh = obj.data.copy() - mesh.name = "{}/{}".format(context_id, result.id()) - mesh.BIMMeshProperties.ifc_definition_id = int(result.id()) - obj.data = mesh - Data.load(self.file, obj.BIMObjectProperties.ifc_definition_id) - - if product.is_a("IfcTypeProduct"): - if self.file.schema == "IFC2X3": - types = product.ObjectTypeOf + Data.load(tool.Ifc.get(), obj.BIMObjectProperties.ifc_definition_id) + element = tool.Ifc.get_entity(obj) + if element.is_a("IfcTypeProduct"): + if tool.Ifc.get_schema() == "IFC2X3": + types = element.ObjectTypeOf else: - types = product.Types + types = element.Types if types: for element in types[0].RelatedObjects: - Data.load(self.file, element.id()) - return {"FINISHED"} + Data.load(tool.Ifc.get(), element.id()) class SwitchRepresentation(bpy.types.Operator): @@ -430,7 +373,16 @@ class CopyRepresentation(bpy.types.Operator, Operator): continue if obj.data: bm.to_mesh(obj.data) - bpy.ops.bim.add_representation(obj=obj.name) + core.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, + ) class OverrideDelete(bpy.types.Operator): diff --git a/src/blenderbim/blenderbim/bim/module/model/profile.py b/src/blenderbim/blenderbim/bim/module/model/profile.py index cfa7ed47ce..d7ecf7dc5e 100644 --- a/src/blenderbim/blenderbim/bim/module/model/profile.py +++ b/src/blenderbim/blenderbim/bim/module/model/profile.py @@ -25,6 +25,8 @@ import ifcopenshell.util.unit import ifcopenshell.util.element import mathutils.geometry import blenderbim.bim.handler +import blenderbim.tool as tool +import blenderbim.core.geometry from blenderbim.bim.ifc import IfcStore from math import pi, degrees, inf from mathutils import Vector, Matrix @@ -169,11 +171,15 @@ class DumbProfileGenerator: element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) bpy.ops.bim.assign_type(relating_type=self.relating_type.id(), related_object=obj.name) profile_set_usage = ifcopenshell.util.element.get_material(element) - bpy.ops.bim.add_representation( - obj=obj.name, - context_id=ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW").id(), + blenderbim.core.geometry.add_representation( + tool.Ifc, + tool.Geometry, + tool.Style, + tool.Surveyor, + obj=obj, + context=ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW"), ifc_representation_class="IfcExtrudedAreaSolid/IfcMaterialProfileSetUsage", - profile_set_usage=profile_set_usage.id(), + profile_set_usage=profile_set_usage, ) representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") bpy.ops.bim.switch_representation( diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index 1c9aaccbc6..f9d2c428a7 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -32,6 +32,7 @@ import blenderbim.core.owner from blenderbim.bim.ifc import IfcStore from blenderbim.bim import import_ifc from blenderbim.bim import export_ifc +from ifcopenshell.api.context.data import Data as ContextData class CreateProject(bpy.types.Operator): @@ -76,7 +77,7 @@ class CreateProject(bpy.types.Operator): model = blenderbim.core.context.add_context( tool.Ifc, context_type="Model", context_identifier="", target_view="", parent=0 ) - blenderbim.core.context.add_context( + body_context = blenderbim.core.context.add_context( tool.Ifc, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model ) blenderbim.core.context.add_context( @@ -89,9 +90,8 @@ class CreateProject(bpy.types.Operator): tool.Ifc, context_type="Plan", context_identifier="Annotation", target_view="PLAN_VIEW", parent=plan ) - context.scene.BIMProperties.contexts = str( - ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW").id() - ) + ContextData.load(tool.Ifc.get()) + context.scene.BIMProperties.contexts = str(body_context.id()) bpy.ops.bim.assign_class(obj=site.name, ifc_class="IfcSite") bpy.ops.bim.assign_class(obj=building.name, ifc_class="IfcBuilding") diff --git a/src/blenderbim/blenderbim/bim/module/root/operator.py b/src/blenderbim/blenderbim/bim/module/root/operator.py index bcd4b380d0..95ad3c6bcf 100644 --- a/src/blenderbim/blenderbim/bim/module/root/operator.py +++ b/src/blenderbim/blenderbim/bim/module/root/operator.py @@ -24,6 +24,7 @@ import ifcopenshell.util.element import blenderbim.bim.handler import blenderbim.core.spatial import blenderbim.core.style +import blenderbim.core.geometry import blenderbim.core.material import blenderbim.tool as tool from ifcopenshell.api.void.data import Data as VoidData @@ -144,8 +145,18 @@ class AssignClass(bpy.types.Operator): IfcStore.link_element(product, obj) if self.should_add_representation: - bpy.ops.bim.add_representation( - obj=obj.name, context_id=self.context_id, ifc_representation_class=self.ifc_representation_class + ifc_context = self.context_id or int(context.scene.BIMProperties.contexts or "0") or None + if ifc_context: + ifc_context = tool.Ifc.get().by_id(ifc_context) + blenderbim.core.geometry.add_representation( + tool.Ifc, + tool.Geometry, + tool.Style, + tool.Surveyor, + obj=obj, + context=ifc_context, + ifc_representation_class=self.ifc_representation_class, + profile_set_usage=None, ) if product.is_a("IfcElementType"): @@ -351,7 +362,16 @@ class CopyClass(bpy.types.Operator): if relating_type and relating_type.RepresentationMaps: bpy.ops.bim.assign_type(relating_type=relating_type.id(), related_object=obj.name) else: - bpy.ops.bim.add_representation(obj=obj.name) + 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"): diff --git a/src/blenderbim/blenderbim/core/geometry.py b/src/blenderbim/blenderbim/core/geometry.py index 3df8ff5273..54ea40ecea 100644 --- a/src/blenderbim/blenderbim/core/geometry.py +++ b/src/blenderbim/blenderbim/core/geometry.py @@ -16,8 +16,58 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . +import blenderbim.core.style + def edit_object_placement(ifc, surveyor, obj=None): element = ifc.get_entity(obj) if element: ifc.run("geometry.edit_object_placement", product=element, matrix=surveyor.get_absolute_matrix(obj)) + + +def add_representation( + ifc, geometry, style, surveyor, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None +): + element = ifc.get_entity(obj) + if not element: + return + + edit_object_placement(ifc, surveyor, obj=obj) + data = geometry.get_object_data(obj) + + if not data: + return + + representation = ifc.run( + "geometry.add_representation", + context=context, + blender_object=obj, + geometry=data, + coordinate_offset=geometry.get_cartesian_point_coordinate_offset(obj), + total_items=geometry.get_total_representation_items(obj), + should_force_faceted_brep=geometry.should_force_faceted_brep(), + should_force_triangulation=geometry.should_force_triangulation(), + ifc_representation_class=ifc_representation_class, + profile_set_usage=profile_set_usage, + ) + + if geometry.does_object_have_mesh_with_faces(obj): + styles = [ + blenderbim.core.style.add_style(ifc, style, obj=material) + for material in geometry.get_object_materials_without_styles(obj) + ] + ifc.run( + "style.assign_representation_styles", + shape_representation=representation, + styles=styles, + should_use_presentation_style_assignment=geometry.should_use_presentation_style_assignment(), + ) + + 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) + geometry.link(representation, data) + + return representation diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 583b93b696..7c9670d601 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -63,6 +63,22 @@ class Context: def set_context(cls, context): pass +@interface +class Geometry: + def does_object_have_mesh_with_faces(cls, obj): pass + def duplicate_object_data(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_total_representation_items(cls, obj): pass + def link(cls, element, obj): pass + def rename_object_data(cls, data, name): pass + def should_force_faceted_brep(cls): pass + def should_force_triangulation(cls): pass + def should_use_presentation_style_assignment(cls): pass + + @interface class Ifc: def get(cls): pass diff --git a/src/blenderbim/blenderbim/tool/__init__.py b/src/blenderbim/blenderbim/tool/__init__.py index a6a4f85086..094198a394 100644 --- a/src/blenderbim/blenderbim/tool/__init__.py +++ b/src/blenderbim/blenderbim/tool/__init__.py @@ -21,6 +21,7 @@ from blenderbim.tool.blender import Blender from blenderbim.tool.collector import Collector from blenderbim.tool.container import Container from blenderbim.tool.context import Context +from blenderbim.tool.geometry import Geometry from blenderbim.tool.ifc import Ifc from blenderbim.tool.material import Material from blenderbim.tool.misc import Misc diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py new file mode 100644 index 0000000000..6718de77ea --- /dev/null +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -0,0 +1,84 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# 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 . + +import bpy +import blenderbim.core.tool +import blenderbim.tool as tool +from mathutils import Vector +from blenderbim.bim.ifc import IfcStore + + +class Geometry(blenderbim.core.tool.Geometry): + @classmethod + def does_object_have_mesh_with_faces(cls, obj): + return bool(isinstance(obj.data, bpy.types.Mesh) and len(obj.data.polygons)) + + @classmethod + def duplicate_object_data(cls, obj): + obj.data = obj.data.copy() + return obj.data + + @classmethod + def get_object_data(cls, obj): + return obj.data + + @classmethod + def get_object_materials_without_styles(cls, obj): + return [ + s.material for s in obj.material_slots if s.material and not s.material.BIMMaterialProperties.ifc_style_id + ] + + @classmethod + def get_representation_name(cls, context, representation): + return f"{context.id()}/{representation.id()}" + + @classmethod + def get_cartesian_point_coordinate_offset(cls, obj): + props = bpy.context.scene.BIMGeoreferenceProperties + if props.has_blender_offset and obj.BIMObjectProperties.blender_offset_type == "CARTESIAN_POINT": + return Vector( + ( + float(props.blender_eastings), + float(props.blender_northings), + float(props.blender_orthogonal_height), + ) + ) + + @classmethod + def get_total_representation_items(cls, obj): + return max(1, len(obj.material_slots)) + + @classmethod + def link(cls, element, obj): + obj.BIMMeshProperties.ifc_definition_id = element.id() + + @classmethod + def rename_object_data(cls, data, name): + data.name = name + + @classmethod + def should_force_faceted_brep(cls): + return bpy.context.scene.BIMGeometryProperties.should_force_faceted_brep + + @classmethod + def should_force_triangulation(cls): + return bpy.context.scene.BIMGeometryProperties.should_force_triangulation + + @classmethod + def should_use_presentation_style_assignment(cls): + return bpy.context.scene.BIMGeometryProperties.should_use_presentation_style_assignment diff --git a/src/blenderbim/blenderbim/tool/misc.py b/src/blenderbim/blenderbim/tool/misc.py index fe37f1080c..25f844e080 100644 --- a/src/blenderbim/blenderbim/tool/misc.py +++ b/src/blenderbim/blenderbim/tool/misc.py @@ -83,7 +83,9 @@ class Misc(blenderbim.core.tool.Misc): min_z = min([c[2] for c in absolute_bound_box]) current_absolute_height = max_z - min_z scale_factor = height / current_absolute_height - obj.matrix_world @= Matrix.Scale(scale_factor, 4, obj.matrix_world.to_quaternion() @ Vector((0, 0, 1))) + obj.matrix_world @= Matrix.Scale( + scale_factor, 4, obj.matrix_world.inverted().to_quaternion() @ Vector((0, 0, 1)) + ) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) @classmethod diff --git a/src/blenderbim/test/bim/feature/geometry.feature b/src/blenderbim/test/bim/feature/geometry.feature index 282ae30212..0b57df3fae 100644 --- a/src/blenderbim/test/bim/feature/geometry.feature +++ b/src/blenderbim/test/bim/feature/geometry.feature @@ -11,6 +11,18 @@ Scenario: Edit object placement When I press "bim.edit_object_placement" Then nothing happens +Scenario: Add 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" + And the object "IfcWall/Cube" is selected + When the variable "context" is "{ifc}.by_type('IfcGeometricRepresentationSubContext')[-1].id()" + And I set "scene.BIMProperties.contexts" to "{context}" + And I press "bim.add_representation" + Then nothing happens + Scenario: Copy representation Given an empty IFC project And I add a cube diff --git a/src/blenderbim/test/bim/test_feature.py b/src/blenderbim/test/bim/test_feature.py index 4e002a33af..bc8a918bf1 100644 --- a/src/blenderbim/test/bim/test_feature.py +++ b/src/blenderbim/test/bim/test_feature.py @@ -130,6 +130,7 @@ def additionally_the_object_name_is_selected(name): @given(parsers.parse('I set "{prop}" to "{value}"')) @when(parsers.parse('I set "{prop}" to "{value}"')) def i_set_prop_to_value(prop, value): + value = replace_variables(value) try: eval(f"bpy.context.{prop}") except: diff --git a/src/blenderbim/test/core/bootstrap.py b/src/blenderbim/test/core/bootstrap.py index 3149e3fddc..e392ad89e5 100644 --- a/src/blenderbim/test/core/bootstrap.py +++ b/src/blenderbim/test/core/bootstrap.py @@ -49,6 +49,13 @@ def context(): prophet.verify() +@pytest.fixture +def geometry(): + prophet = Prophecy(blenderbim.core.tool.Geometry) + yield prophet + prophet.verify() + + @pytest.fixture def material(): prophet = Prophecy(blenderbim.core.tool.Material) diff --git a/src/blenderbim/test/core/test_geometry.py b/src/blenderbim/test/core/test_geometry.py index 2de9749a52..09a21b8b94 100644 --- a/src/blenderbim/test/core/test_geometry.py +++ b/src/blenderbim/test/core/test_geometry.py @@ -17,12 +17,162 @@ # along with BlenderBIM Add-on. If not, see . import blenderbim.core.geometry as subject -from test.core.bootstrap import ifc, surveyor +import test.core.test_style +from test.core.bootstrap import ifc, surveyor, geometry, style class TestEditObjectPlacement: - def test_run(self, ifc, surveyor): + def predict(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() + + def test_run(self, ifc, surveyor): + self.predict(ifc, surveyor) subject.edit_object_placement(ifc, surveyor, obj="obj") + + +class TestAddRepresentation: + def test_run(self, ifc, geometry, style, surveyor): + TestEditObjectPlacement.predict(self, ifc, surveyor) + + # Add representation + geometry.get_object_data("obj").should_be_called().will_return("data") + geometry.get_cartesian_point_coordinate_offset("obj").should_be_called().will_return("coordinate_offset") + geometry.get_total_representation_items("obj").should_be_called().will_return(1) + geometry.should_force_faceted_brep().should_be_called().will_return(False) + geometry.should_force_triangulation().should_be_called().will_return(True) + ifc.run( + "geometry.add_representation", + context="context", + blender_object="obj", + geometry="data", + coordinate_offset="coordinate_offset", + total_items=1, + should_force_faceted_brep=False, + should_force_triangulation=True, + ifc_representation_class="ifc_representation_class", + profile_set_usage="profile_set_usage", + ).should_be_called().will_return("representation") + + # Styles are relevant for meshes with faces only + geometry.does_object_have_mesh_with_faces("obj").should_be_called().will_return(True) + + # Add styles + geometry.get_object_materials_without_styles("obj").should_be_called().will_return(["material"]) + test.core.test_style.TestAddStyle.predict(self, ifc, style, obj="material") + + # Link style to representation items + geometry.should_use_presentation_style_assignment().should_be_called().will_return(False) + ifc.run( + "style.assign_representation_styles", + shape_representation="representation", + styles=["style"], + should_use_presentation_style_assignment=False, + ).should_be_called() + + # Assign representation to product + ifc.run("geometry.assign_representation", product="element", representation="representation").should_be_called() + + # 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.link("representation", "data").should_be_called() + + assert ( + subject.add_representation( + ifc, + geometry, + style, + surveyor, + obj="obj", + context="context", + ifc_representation_class="ifc_representation_class", + profile_set_usage="profile_set_usage", + ) + == "representation" + ) + + def test_not_handling_styles_if_representation_has_no_faces(self, ifc, geometry, style, surveyor): + TestEditObjectPlacement.predict(self, ifc, surveyor) + + # Add representation + geometry.get_object_data("obj").should_be_called().will_return("data") + geometry.get_cartesian_point_coordinate_offset("obj").should_be_called().will_return("coordinate_offset") + geometry.get_total_representation_items("obj").should_be_called().will_return(1) + geometry.should_force_faceted_brep().should_be_called().will_return(False) + geometry.should_force_triangulation().should_be_called().will_return(True) + ifc.run( + "geometry.add_representation", + context="context", + blender_object="obj", + geometry="data", + coordinate_offset="coordinate_offset", + total_items=1, + should_force_faceted_brep=False, + should_force_triangulation=True, + ifc_representation_class="ifc_representation_class", + profile_set_usage="profile_set_usage", + ).should_be_called().will_return("representation") + + # Styles are relevant for meshes with faces only + geometry.does_object_have_mesh_with_faces("obj").should_be_called().will_return(False) + + # Assign representation to product + ifc.run("geometry.assign_representation", product="element", representation="representation").should_be_called() + + # 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.link("representation", "data").should_be_called() + + assert ( + subject.add_representation( + ifc, + geometry, + style, + surveyor, + obj="obj", + context="context", + ifc_representation_class="ifc_representation_class", + profile_set_usage="profile_set_usage", + ) + == "representation" + ) + + def test_only_updating_the_placement_if_there_is_no_object_data(self, ifc, geometry, style, surveyor): + TestEditObjectPlacement.predict(self, ifc, surveyor) + + # Add representation + geometry.get_object_data("obj").should_be_called().will_return(None) + assert ( + subject.add_representation( + ifc, + geometry, + style, + surveyor, + obj="obj", + context="context", + ifc_representation_class="ifc_representation_class", + profile_set_usage="profile_set_usage", + ) + is None + ) + + def test_doing_nothing_if_not_an_ifc_element(self, ifc, geometry, style, surveyor): + ifc.get_entity("obj").should_be_called().will_return(None) + assert ( + subject.add_representation( + ifc, + geometry, + style, + surveyor, + obj="obj", + context="context", + ifc_representation_class="ifc_representation_class", + profile_set_usage="profile_set_usage", + ) + is None + ) diff --git a/src/blenderbim/test/core/test_style.py b/src/blenderbim/test/core/test_style.py index 78a46b6019..298434335c 100644 --- a/src/blenderbim/test/core/test_style.py +++ b/src/blenderbim/test/core/test_style.py @@ -21,15 +21,18 @@ from test.core.bootstrap import ifc, style class TestAddStyle: - def test_it_adds_a_style_with_rendering_attributes(self, ifc, style): - style.get_name("obj").should_be_called().will_return("name") + def predict(self, ifc, style, obj="obj"): + style.get_name(obj).should_be_called().will_return("name") ifc.run("style.add_style", name="name").should_be_called().will_return("style") - style.link("style", "obj").should_be_called() - style.get_surface_rendering_attributes("obj").should_be_called().will_return("attributes") + style.link("style", obj).should_be_called() + style.get_surface_rendering_attributes(obj).should_be_called().will_return("attributes") ifc.run( "style.add_surface_style", style="style", ifc_class="IfcSurfaceStyleRendering", attributes="attributes" ).should_be_called() - ifc.get_entity("obj").should_be_called().will_return(None) + ifc.get_entity(obj).should_be_called().will_return(None) + + def test_it_adds_a_style_with_rendering_attributes(self, ifc, style): + self.predict(ifc, style) assert subject.add_style(ifc, style, obj="obj") == "style" def test_adding_a_style_linked_to_a_material(self, ifc, style): diff --git a/src/blenderbim/test/tool/test_geometry.py b/src/blenderbim/test/tool/test_geometry.py new file mode 100644 index 0000000000..d874128452 --- /dev/null +++ b/src/blenderbim/test/tool/test_geometry.py @@ -0,0 +1,161 @@ +# BlenderBIM Add-on - OpenBIM Blender Add-on +# Copyright (C) 2021 Dion Moult +# +# 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 . + +import bpy +import math +import numpy +import ifcopenshell +import test.bim.bootstrap +import blenderbim.core.tool +import blenderbim.tool as tool +from mathutils import Vector +from blenderbim.tool.geometry import Geometry as subject +from blenderbim.bim.ifc import IfcStore + + +class TestImplementsTool(test.bim.bootstrap.NewFile): + def test_run(self): + assert isinstance(subject(), blenderbim.core.tool.Geometry) + + +class TestDoesObjectHaveMeshWithFaces(test.bim.bootstrap.NewFile): + def test_empties_return_false(self): + obj = bpy.data.objects.new("Object", None) + assert subject.does_object_have_mesh_with_faces(obj) is False + + def test_non_meshes_return_false(self): + obj = bpy.data.objects.new("Object", bpy.data.cameras.new("Curve")) + assert subject.does_object_have_mesh_with_faces(obj) is False + + def test_meshes_without_faces_return_false(self): + obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) + assert subject.does_object_have_mesh_with_faces(obj) is False + + def test_meshes_with_faces_return_true(self): + bpy.ops.mesh.primitive_cube_add() + obj = bpy.data.objects.get("Cube") + assert subject.does_object_have_mesh_with_faces(obj) is True + + +class TestDuplicateObjectData(test.bim.bootstrap.NewFile): + def test_run(self): + data = bpy.data.meshes.new("Mesh") + obj = bpy.data.objects.new("Object", data) + assert subject.duplicate_object_data(obj) == obj.data + assert obj.data != data + assert isinstance(obj.data, bpy.types.Mesh) + + +class TestGetObjectData(test.bim.bootstrap.NewFile): + def test_run(self): + data = bpy.data.meshes.new("Mesh") + obj = bpy.data.objects.new("Object", data) + assert subject.get_object_data(obj) == obj.data + + +class TestGetObjectMaterialsWithoutStyles(test.bim.bootstrap.NewFile): + def test_run(self): + material1 = bpy.data.materials.new("Material") + material2 = bpy.data.materials.new("Material") + material3 = bpy.data.materials.new("Material") + material3.BIMMaterialProperties.ifc_style_id = 1 + obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) + obj.data.materials.append(material1) + obj.data.materials.append(material2) + obj.data.materials.append(material3) + assert subject.get_object_materials_without_styles(obj) == [material1, material2] + + +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()}" + + +class TestGetCartesianPointCoordinateOffset(test.bim.bootstrap.NewFile): + def test_run(self): + obj = bpy.data.objects.new("Object", None) + obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT" + props = bpy.context.scene.BIMGeoreferenceProperties + props.has_blender_offset = True + props.blender_eastings = "1" + props.blender_northings = "2" + props.blender_orthogonal_height = "3" + assert subject.get_cartesian_point_coordinate_offset(obj) == Vector((1.0, 2.0, 3.0)) + + def test_get_null_if_not_a_cartesian_point_offset_type(self): + obj = bpy.data.objects.new("Object", None) + props = bpy.context.scene.BIMGeoreferenceProperties + props.has_blender_offset = True + props.blender_eastings = "1" + props.blender_northings = "2" + props.blender_orthogonal_height = "3" + assert subject.get_cartesian_point_coordinate_offset(obj) is None + + def test_get_null_if_no_blender_offset(self): + obj = bpy.data.objects.new("Object", None) + obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT" + props = bpy.context.scene.BIMGeoreferenceProperties + props.has_blender_offset = False + assert subject.get_cartesian_point_coordinate_offset(obj) is None + + +class TestGetTotalRepresentationItems(test.bim.bootstrap.NewFile): + def test_run(self): + material1 = bpy.data.materials.new("Material") + material2 = bpy.data.materials.new("Material") + obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) + obj.data.materials.append(material1) + obj.data.materials.append(material2) + assert subject.get_total_representation_items(obj) == 2 + + +class TestLink(test.bim.bootstrap.NewFile): + def test_run(self): + ifc = ifcopenshell.file() + element = ifc.createIfcShapeRepresentation() + obj = bpy.data.meshes.new("Mesh") + subject.link(element, obj) + assert obj.BIMMeshProperties.ifc_definition_id == element.id() + + +class TestRenameObjectData(test.bim.bootstrap.NewFile): + def test_run(self): + obj = bpy.data.meshes.new("Mesh") + subject.rename_object_data(obj, "name") + assert obj.name == "name" + + +class TestShouldForceFacetedBrep(test.bim.bootstrap.NewFile): + def test_run(self): + result = bpy.context.scene.BIMGeometryProperties.should_force_faceted_brep + assert subject.should_force_faceted_brep() is result + + +class TestShouldForceTriangulation(test.bim.bootstrap.NewFile): + def test_run(self): + result = bpy.context.scene.BIMGeometryProperties.should_force_triangulation + assert subject.should_force_triangulation() is result + + +class TestShouldUsePresentationStyleAssignment(test.bim.bootstrap.NewFile): + def test_run(self): + result = bpy.context.scene.BIMGeometryProperties.should_use_presentation_style_assignment + assert subject.should_use_presentation_style_assignment() is result diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py b/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py index 2a1fdf1d03..fdbba11544 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py @@ -10,7 +10,6 @@ class Usecase: self.settings[key] = value def execute(self): - self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(self.file.schema) result = ifcopenshell.util.element.copy(self.file, self.settings["product"]) self.copy_direct_attributes(result) self.copy_indirect_attributes(self.settings["product"], result)