diff --git a/choco/blenderbim/check_repo_infos.py b/choco/blenderbim/check_repo_infos.py index d00a38ac72..af99f3cdea 100644 --- a/choco/blenderbim/check_repo_infos.py +++ b/choco/blenderbim/check_repo_infos.py @@ -70,7 +70,7 @@ elif sys.argv[1] == "--pyver?": found = re.findall(re_blender_version_min_maj_pat, html_txt) if found: latest_blender_version_tag = f"v{found[0]}" - re_blender_python_version_maj_min = r"SET\(PYTHON_VERSION (\d+.\d+) " + re_blender_python_version_maj_min = r"^SET\(_PYTHON_VERSION_SUPPORTED (\d+\.\d+)\)$" url = f"https://raw.githubusercontent.com/blender/blender/{latest_blender_version_tag}/build_files/cmake/Modules/FindPythonLibsUnix.cmake" resp = request_repo_info(url) html_txt = str(resp.read()) diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 9db6058d50..3f4e755006 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -95,6 +95,7 @@ classes = [ operator.OpenUpstream, operator.OpenUri, operator.SwitchTab, + operator.SetTab, operator.ReloadIfcFile, operator.RemoveIfcFile, operator.SelectDataDir, @@ -125,24 +126,37 @@ classes = [ ui.BIM_UL_generic, ui.BIM_UL_topics, ui.BIM_ADDON_preferences, - # Scene panel groups - ui.BIM_PT_root, + # Project overview ui.BIM_PT_project_info, ui.BIM_PT_project_setup, - ui.BIM_PT_collaboration, - ui.BIM_PT_selection, ui.BIM_PT_geometry, - ui.BIM_PT_services, - ui.BIM_PT_structural, - ui.BIM_PT_4D5D, - ui.BIM_PT_quality_control, - ui.BIM_PT_integrations, - # Object panel groups - ui.BIM_PT_object_metadata, - ui.BIM_PT_geometry_object, - ui.BIM_PT_services_object, - ui.BIM_PT_utilities_object, - ui.BIM_PT_misc_object, + ui.BIM_PT_selection, + # Tabs panel + ui.BIM_PT_tabs, + # Object information + ui.BIM_PT_tab_object_metadata, + ui.BIM_PT_tab_misc, + # Geometry and materials + ui.BIM_PT_tab_representations, + ui.BIM_PT_tab_geometric_relationships, + ui.BIM_PT_tab_parametric_geometry, + ui.BIM_PT_tab_materials, + ui.BIM_PT_tab_styles, + # Drawings and documents + # Services and systems + ui.BIM_PT_tab_services, + ui.BIM_PT_tab_services_object, + # Structural analysis + ui.BIM_PT_tab_structural, + # Construction scheduling + ui.BIM_PT_tab_4D5D, + # Facility management + ui.BIM_PT_tab_handover, + ui.BIM_PT_tab_operations, + # Quality and coordination + ui.BIM_PT_tab_quality_control, + ui.BIM_PT_tab_collaboration, + ui.BIM_PT_tab_integrations, ] for mod in modules.values(): @@ -170,9 +184,7 @@ def register(): for cls in classes: bpy.utils.register_class(cls) bpy.app.handlers.depsgraph_update_post.append(on_register) - bpy.app.handlers.undo_pre.append(handler.undo_pre) bpy.app.handlers.undo_post.append(handler.undo_post) - bpy.app.handlers.redo_pre.append(handler.redo_pre) bpy.app.handlers.redo_post.append(handler.redo_post) bpy.app.handlers.load_post.append(handler.load_post) bpy.app.handlers.load_post.append(handler.loadIfcStore) diff --git a/src/blenderbim/blenderbim/bim/data/assets/symbols.svg b/src/blenderbim/blenderbim/bim/data/assets/symbols.svg index a657f279c0..efcf0ddd77 100644 --- a/src/blenderbim/blenderbim/bim/data/assets/symbols.svg +++ b/src/blenderbim/blenderbim/bim/data/assets/symbols.svg @@ -41,4 +41,17 @@ + + + + + + + + + + + + + diff --git a/src/blenderbim/blenderbim/bim/handler.py b/src/blenderbim/blenderbim/bim/handler.py index b56c001690..af272745e3 100644 --- a/src/blenderbim/blenderbim/bim/handler.py +++ b/src/blenderbim/blenderbim/bim/handler.py @@ -54,10 +54,12 @@ def mode_callback(obj, data): def name_callback(obj, data): - # TODO Do we still need this, now that we are monitoring the undo redo objects? try: obj.name except: + # The object is invalid but somehow still has a callback. Clear all + # msgbus subscriptions to prevent useless further triggers. + bpy.msgbus.clear_by_owner(obj) return # In case the object RNA is gone during an undo / redo operation # Blender names are up to 63 UTF-8 bytes if len(bytes(obj.name, "utf-8")) >= 63: @@ -203,34 +205,22 @@ def loadIfcStore(scene): IfcStore.relink_all_objects() -@persistent -def undo_pre(scene): - IfcStore.track_undo_redo_stack_object_map() - - @persistent def undo_post(scene): if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction: IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction - IfcStore.undo() + IfcStore.undo(until_key=bpy.context.scene.BIMProperties.last_transaction) purge_module_data() - IfcStore.track_undo_redo_stack_selected_objects() - IfcStore.reload_undo_redo_stack_objects() - - -@persistent -def redo_pre(scene): - IfcStore.track_undo_redo_stack_object_map() + tool.Ifc.rebuild_element_maps() @persistent def redo_post(scene): if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction: IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction - IfcStore.redo() + IfcStore.redo(until_key=bpy.context.scene.BIMProperties.last_transaction) purge_module_data() - IfcStore.track_undo_redo_stack_selected_objects() - IfcStore.reload_undo_redo_stack_objects() + tool.Ifc.rebuild_element_maps() def get_application(ifc): diff --git a/src/blenderbim/blenderbim/bim/helper.py b/src/blenderbim/blenderbim/bim/helper.py index 5b73dadd2a..b062aeb552 100644 --- a/src/blenderbim/blenderbim/bim/helper.py +++ b/src/blenderbim/blenderbim/bim/helper.py @@ -17,6 +17,7 @@ # along with BlenderBIM Add-on. If not, see . # from datetime import date +import re import bpy import json import math @@ -31,6 +32,11 @@ import blenderbim.tool as tool from blenderbim.bim.ifc import IfcStore +def uncamel(s): + s = re.sub('([a-z0-9])([A-Z])', r'\1 \2', s[3:] if s.startswith("Ifc") else s) + return re.sub('([A-Z])([A-Z][a-z])', r'\1 \2', s) + + def draw_attributes(props, layout, copy_operator=None, popup_active_attribute=None): """you can set attribute active in popup with `active_attribute` meaning you will be able to type into attribute's field without having to click diff --git a/src/blenderbim/blenderbim/bim/ifc.py b/src/blenderbim/blenderbim/bim/ifc.py index 875614e1bd..4af2d0453f 100644 --- a/src/blenderbim/blenderbim/bim/ifc.py +++ b/src/blenderbim/blenderbim/bim/ifc.py @@ -44,9 +44,6 @@ class IfcStore: classification_file = None library_path = "" library_file = None - element_listeners = set() - undo_redo_stack_objects = set() - undo_redo_stack_object_names = {} current_transaction = "" last_transaction = "" history = [] @@ -161,96 +158,6 @@ class IfcStore: return return obj - @staticmethod - def add_element_listener(callback): - IfcStore.element_listeners.add(callback) - - @staticmethod - def track_undo_redo_stack_object_map(): - """Keeps track of currently mapped object names, typically during undo and redo - - When any Blender object is stored outside a Blender PointerProperty, such as - in a regular Python list, there is the likely probability that the object - will be invalidated when undo or redo occurs. Object invalidation seems to - occur whenever an object is affected during an operation. - - For example, if an operator deletes a modifier on o1, then o1 will be invalidated. - """ - for key, value in IfcStore.id_map.items(): - try: - IfcStore.undo_redo_stack_object_names[key] = value.name - except: - continue - - @staticmethod - def track_undo_redo_stack_selected_objects(): - """Keeps track of selected object names, typically during undo and redo - - When any Blender object is stored outside a Blender PointerProperty, such as - in a regular Python list, there is the likely probability that the object - will be invalidated when undo or redo occurs. Object invalidation seems to - occur for selected objects either pre/post undo/redo event, including - selected objects for consecutive undo/redos, and all children. This is - important because selected objects are often deleted from the scene. - - So if I first select o1, then o2, then o3, then press undo, o3 will be - invalidated. If instead I press undo twice, o3 and o2 will be invalidated. - """ - if bpy.context.active_object: - objects = set([o.name for o in bpy.context.selected_objects + [bpy.context.active_object]]) - objects.update([o.name for o in bpy.context.active_object.children]) - else: - objects = set([o.name for o in bpy.context.selected_objects]) - for obj in bpy.context.selected_objects: - objects.update([o.name for o in obj.children]) - IfcStore.undo_redo_stack_objects |= objects - - @staticmethod - def reload_undo_redo_stack_objects(): - """Reloads any invalidated objects after undo or redo - - After an undo or redo operation, objects may have been invalidated in - our id_map and guid_map. Invalidated objects are typically those that - have been manipulated or deleted. This checks the cache of mapped and - selected objects prior to the operation and ensures that if the object - is invalidated, they are reloaded based on the object name that was - tracked prior to the undo / redo. - """ - file = IfcStore.get_file() - if not file: - return - - # First, reload objects that were selected or active - for name in IfcStore.undo_redo_stack_objects: - obj = bpy.data.objects.get(name) - if not obj: - continue - if not obj.BIMObjectProperties.ifc_definition_id: - continue - element = file.by_id(obj.BIMObjectProperties.ifc_definition_id) - data = {"id": element.id(), "obj": obj.name} - if hasattr(element, "GlobalId"): - data["guid"] = element.GlobalId - IfcStore.commit_link_element(data) - - # Scan for any straggling invalidated objects which were indirectly affected and reload them too. - for key, value in IfcStore.id_map.items(): - try: - value.name - except: - # TODO not so sure about this obj_name check - obj_name = IfcStore.undo_redo_stack_object_names.get(key, None) - if not obj_name: - continue - obj = bpy.data.objects.get(obj_name) - if not obj or not obj.BIMObjectProperties.ifc_definition_id: - continue - element = file.by_id(obj.BIMObjectProperties.ifc_definition_id) - data = {"id": element.id(), "obj": obj.name} - if hasattr(element, "GlobalId"): - data["guid"] = element.GlobalId - IfcStore.commit_link_element(data) - @staticmethod def relink_all_objects(): if not IfcStore.get_file(): @@ -283,6 +190,13 @@ class IfcStore: @staticmethod def link_element(element, obj): + # Please use tool.Ifc.link() instead of this method. We want to + # refactor this class and deprecate usage of IfcStore in favour of + # tools. + if isinstance(obj, bpy.types.Mesh): + obj.BIMMeshProperties.ifc_definition_id = element.id() + return + existing_obj = IfcStore.id_map.get(element.id(), None) if existing_obj == obj: return @@ -309,9 +223,6 @@ class IfcStore: blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback) blenderbim.bim.handler.subscribe_to(obj, "active_material_index", blenderbim.bim.handler.active_material_index_callback) - for listener in IfcStore.element_listeners: - listener(element, obj) - if IfcStore.history: data = {"id": element.id(), "guid": getattr(element, "GlobalId", None), "obj": obj.name} IfcStore.history[-1]["operations"].append( @@ -332,10 +243,12 @@ class IfcStore: IfcStore.id_map[data["id"]] = obj if "guid" in data: IfcStore.guid_map[data["guid"]] = obj - blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback) blenderbim.bim.handler.subscribe_to(obj, "name", blenderbim.bim.handler.name_callback) if isinstance(obj, bpy.types.Material): blenderbim.bim.handler.subscribe_to(obj, "diffuse_color", blenderbim.bim.handler.color_callback) + elif isinstance(obj, bpy.types.Object): + blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback) + blenderbim.bim.handler.subscribe_to(obj, "active_material_index", blenderbim.bim.handler.active_material_index_callback) # TODO Listeners are not re-registered. Does this cause nasty problems to debug later on? # TODO We're handling id_map and guid_map, but what about edited_objs? This might cause big problems. @@ -435,8 +348,6 @@ class IfcStore: @staticmethod def begin_transaction(operator): - IfcStore.undo_redo_stack_objects = set() - IfcStore.undo_redo_stack_object_names = {} IfcStore.current_transaction = str(uuid.uuid4()) operator.transaction_key = IfcStore.current_transaction @@ -462,21 +373,35 @@ class IfcStore: IfcStore.future = [] @staticmethod - def undo(): + def undo(until_key=None): BrickStore.undo() if not IfcStore.history: return - event = IfcStore.history.pop() - for transaction in event["operations"][::-1]: - transaction["rollback"](transaction["data"]) - IfcStore.future.append(event) + + while IfcStore.history: + if IfcStore.history[-1]["key"] == until_key: + return + + event = IfcStore.history.pop() + for transaction in event["operations"][::-1]: + transaction["rollback"](transaction["data"]) + IfcStore.future.append(event) @staticmethod - def redo(): + def redo(until_key=None): BrickStore.redo() + if not IfcStore.future: return - event = IfcStore.future.pop() - for transaction in event["operations"]: - transaction["commit"](transaction["data"]) - IfcStore.history.append(event) + + has_encountered_key = False + while IfcStore.future: + if has_encountered_key and IfcStore.future[-1]["key"] != until_key: + return + elif IfcStore.future[-1]["key"] == until_key: + has_encountered_key = True + + event = IfcStore.future.pop() + for transaction in event["operations"]: + transaction["commit"](transaction["data"]) + IfcStore.history.append(event) diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index 8018f7db72..2a1bc11511 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -692,7 +692,7 @@ class IfcImporter: mesh = self.create_native_faceted_brep(element, mesh_name) elif native_data["type"] == "IfcFaceBasedSurfaceModel": mesh = self.create_native_faceted_brep(element, mesh_name) - mesh.BIMMeshProperties.ifc_definition_id = representation.id() + tool.Ifc.link(representation, mesh) mesh.name = mesh_name self.meshes[mesh_name] = mesh self.create_product(element, mesh=mesh) @@ -921,7 +921,7 @@ class IfcImporter: return result def create_pointcloud(self, product, representation): - placement_matrix = ifcopenshell.util.placement.get_local_placement(product.ObjectPlacement) + placement_matrix = self.get_element_matrix(product) vertex_list = [] for item in representation.Items: if item.is_a("IfcCartesianPointList"): @@ -937,6 +937,7 @@ class IfcImporter: mesh_name = f"{representation.ContextOfItems.id()}/{representation.id()}" mesh = bpy.data.meshes.new(mesh_name) mesh.from_pydata(vertex_list, [], []) + tool.Ifc.link(representation, mesh) obj = bpy.data.objects.new("{}/{}".format(product.is_a(), product.Name), mesh) self.set_matrix_world(obj, self.apply_blender_offset_to_matrix_world(obj, placement_matrix)) @@ -1342,12 +1343,21 @@ class IfcImporter: last_obj = obj if not last_obj: return + + # temporarily unhide types collection to make sure all objects will be cleaned + project_collection = bpy.context.view_layer.layer_collection.children[self.project["blender"].name] + types_collection = project_collection.children[self.type_collection.name] + types_collection.hide_viewport = False bpy.context.view_layer.objects.active = last_obj + context_override = {} bpy.ops.object.editmode_toggle(context_override) bpy.ops.mesh.tris_convert_to_quads(context_override) bpy.ops.mesh.normals_make_consistent(context_override) bpy.ops.object.editmode_toggle(context_override) + + types_collection.hide_viewport = True + bpy.context.view_layer.objects.active = last_obj IfcStore.edited_objs.clear() def load_file(self): diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py index 50bc8007f6..6d5404c057 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py @@ -22,13 +22,13 @@ from blenderbim.bim.ifc import IfcStore class BIM_PT_aggregate(Panel): - bl_label = "IFC Aggregates" + bl_label = "Aggregates" bl_idname = "BIM_PT_aggregate" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" - bl_parent_id = "BIM_PT_object_metadata" + bl_parent_id = "BIM_PT_tab_object_metadata" @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/attribute/ui.py b/src/blenderbim/blenderbim/bim/module/attribute/ui.py index 510b2c537e..7ffd360c19 100644 --- a/src/blenderbim/blenderbim/bim/module/attribute/ui.py +++ b/src/blenderbim/blenderbim/bim/module/attribute/ui.py @@ -54,12 +54,12 @@ def draw_ui(context, layout, obj_type, attributes): class BIM_PT_object_attributes(Panel): - bl_label = "IFC Attributes" + bl_label = "Attributes" bl_idname = "BIM_PT_object_attributes" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" - bl_parent_id = "BIM_PT_object_metadata" + bl_parent_id = "BIM_PT_tab_object_metadata" @classmethod def poll(cls, context): @@ -76,7 +76,7 @@ class BIM_PT_object_attributes(Panel): class BIM_PT_material_attributes(Panel): - bl_label = "IFC Material Attributes" + bl_label = "Material Attributes" bl_idname = "BIM_PT_material_attributes" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" diff --git a/src/blenderbim/blenderbim/bim/module/augin/ui.py b/src/blenderbim/blenderbim/bim/module/augin/ui.py index 896a8e5386..4daed80129 100644 --- a/src/blenderbim/blenderbim/bim/module/augin/ui.py +++ b/src/blenderbim/blenderbim/bim/module/augin/ui.py @@ -26,7 +26,7 @@ class BIM_PT_augin(bpy.types.Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_integrations" + bl_parent_id = "BIM_PT_tab_integrations" def draw(self, context): layout = self.layout diff --git a/src/blenderbim/blenderbim/bim/module/bcf/ui.py b/src/blenderbim/blenderbim/bim/module/bcf/ui.py index 821d028dbb..58f3c0a858 100644 --- a/src/blenderbim/blenderbim/bim/module/bcf/ui.py +++ b/src/blenderbim/blenderbim/bim/module/bcf/ui.py @@ -29,7 +29,7 @@ class BIM_PT_bcf(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_collaboration" + bl_parent_id = "BIM_PT_tab_collaboration" def draw(self, context): layout = self.layout diff --git a/src/blenderbim/blenderbim/bim/module/bimtester/ui.py b/src/blenderbim/blenderbim/bim/module/bimtester/ui.py index 9b58f29b12..5c0843d618 100644 --- a/src/blenderbim/blenderbim/bim/module/bimtester/ui.py +++ b/src/blenderbim/blenderbim/bim/module/bimtester/ui.py @@ -27,7 +27,7 @@ class BIM_PT_qa(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_quality_control" + bl_parent_id = "BIM_PT_tab_quality_control" def draw(self, context): self.layout.use_property_split = True diff --git a/src/blenderbim/blenderbim/bim/module/boundary/ui.py b/src/blenderbim/blenderbim/bim/module/boundary/ui.py index 6abad5b6e6..1e1068077f 100644 --- a/src/blenderbim/blenderbim/bim/module/boundary/ui.py +++ b/src/blenderbim/blenderbim/bim/module/boundary/ui.py @@ -25,7 +25,7 @@ from blenderbim.bim.module.boundary.data import SpaceBoundariesData class BIM_PT_SceneBoundaries(Panel): - bl_label = "IFC Space Boundaries" + bl_label = "Space Boundaries" bl_id_name = "BIM_PT_scene_boundaries" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -45,12 +45,12 @@ class BIM_PT_SceneBoundaries(Panel): class BIM_PT_Boundary(Panel): - bl_label = "IFC Space Boundary" + bl_label = "Space Boundary" bl_idname = "BIM_PT_Boundary" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" - bl_parent_id = "BIM_PT_geometry_object" + bl_parent_id = "BIM_PT_tab_geometric_relationships" @classmethod def poll(cls, context): @@ -117,14 +117,14 @@ class BIM_PT_Boundary(Panel): class BIM_PT_SpaceBoundaries(Panel): - bl_label = "IFC Space Boundaries" + bl_label = "Space Boundaries" bl_idname = "BIM_PT_SpaceBoundaries" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" bl_order = 1 - bl_parent_id = "BIM_PT_geometry_object" + bl_parent_id = "BIM_PT_tab_geometric_relationships" @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/brick/ui.py b/src/blenderbim/blenderbim/bim/module/brick/ui.py index b769057d37..2303ac6498 100644 --- a/src/blenderbim/blenderbim/bim/module/brick/ui.py +++ b/src/blenderbim/blenderbim/bim/module/brick/ui.py @@ -26,14 +26,10 @@ from blenderbim.tool.brick import BrickStore class BIM_PT_brickschema(Panel): bl_label = "Brickschema Project" bl_idname = "BIM_PT_brickschema" - bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - - @classmethod - def poll(cls, context): - return tool.Blender.is_tab(context, "OTHER") + bl_parent_id = "BIM_PT_tab_operations" def draw(self, context): if not BrickschemaData.is_loaded: @@ -108,7 +104,7 @@ class BIM_PT_brickschema(Panel): class BIM_PT_ifc_brickschema_references(Panel): - bl_label = "IFC Brickschema References" + bl_label = "Brickschema References" bl_idname = "BIM_PT_ifc_brickschema_references" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" diff --git a/src/blenderbim/blenderbim/bim/module/clash/ui.py b/src/blenderbim/blenderbim/bim/module/clash/ui.py index 21e6547e57..26c32321c4 100644 --- a/src/blenderbim/blenderbim/bim/module/clash/ui.py +++ b/src/blenderbim/blenderbim/bim/module/clash/ui.py @@ -21,13 +21,13 @@ from bpy.types import Panel class BIM_PT_ifcclash(Panel): - bl_label = "IFC Clash Sets" + bl_label = "Clash Sets" bl_idname = "BIM_PT_ifcclash" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_quality_control" + bl_parent_id = "BIM_PT_tab_quality_control" def draw(self, context): layout = self.layout diff --git a/src/blenderbim/blenderbim/bim/module/classification/ui.py b/src/blenderbim/blenderbim/bim/module/classification/ui.py index 8bf5f660c7..5a4de6bf65 100644 --- a/src/blenderbim/blenderbim/bim/module/classification/ui.py +++ b/src/blenderbim/blenderbim/bim/module/classification/ui.py @@ -30,7 +30,7 @@ from blenderbim.bim.module.classification.data import ( class BIM_PT_classifications(Panel): - bl_label = "IFC Classifications" + bl_label = "Classifications" bl_idname = "BIM_PT_classifications" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -156,13 +156,13 @@ class ReferenceUI: class BIM_PT_classification_references(Panel, ReferenceUI): - bl_label = "IFC Classification References" + bl_label = "Classification References" bl_idname = "BIM_PT_classification_references" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" - bl_parent_id = "BIM_PT_object_metadata" + bl_parent_id = "BIM_PT_tab_object_metadata" @classmethod def poll(cls, context): @@ -180,7 +180,7 @@ class BIM_PT_classification_references(Panel, ReferenceUI): class BIM_PT_material_classifications(Panel, ReferenceUI): - bl_label = "IFC Material Classifications" + bl_label = "Material Classifications" bl_idname = "BIM_PT_material_classifications" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -206,7 +206,7 @@ class BIM_PT_material_classifications(Panel, ReferenceUI): class BIM_PT_cost_classifications(Panel, ReferenceUI): - bl_label = "IFC Cost Classifications" + bl_label = "Cost Classifications" bl_idname = "BIM_PT_cost_classifications" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" diff --git a/src/blenderbim/blenderbim/bim/module/cobie/ui.py b/src/blenderbim/blenderbim/bim/module/cobie/ui.py index 075dba6bba..a78cff2b62 100644 --- a/src/blenderbim/blenderbim/bim/module/cobie/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cobie/ui.py @@ -16,18 +16,18 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . +import blenderbim.tool as tool from bpy.types import Panel from blenderbim.bim.ifc import IfcStore class BIM_PT_cobie(Panel): - bl_label = "IFC COBie" + bl_label = "COBie" bl_idname = "BIM_PT_cobie" - bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_collaboration" + bl_parent_id = "BIM_PT_tab_handover" def draw(self, context): layout = self.layout diff --git a/src/blenderbim/blenderbim/bim/module/constraint/ui.py b/src/blenderbim/blenderbim/bim/module/constraint/ui.py index 38bd07bfd3..2b017e89c3 100644 --- a/src/blenderbim/blenderbim/bim/module/constraint/ui.py +++ b/src/blenderbim/blenderbim/bim/module/constraint/ui.py @@ -23,7 +23,7 @@ from blenderbim.bim.module.constraint.data import ConstraintsData, ObjectConstra class BIM_PT_constraints(Panel): - bl_label = "IFC Constraints" + bl_label = "Constraints" bl_idname = "BIM_PT_constraints" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -68,14 +68,14 @@ class BIM_PT_constraints(Panel): class BIM_PT_object_constraints(Panel): - bl_label = "IFC Constraints" + bl_label = "Constraints" bl_idname = "BIM_PT_object_constraints" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" bl_order = 1 - bl_parent_id = "BIM_PT_misc_object" + bl_parent_id = "BIM_PT_tab_misc" @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/context/ui.py b/src/blenderbim/blenderbim/bim/module/context/ui.py index 58449f6d45..c8fe010625 100644 --- a/src/blenderbim/blenderbim/bim/module/context/ui.py +++ b/src/blenderbim/blenderbim/bim/module/context/ui.py @@ -23,7 +23,7 @@ from blenderbim.bim.module.context.data import ContextData class BIM_PT_context(bpy.types.Panel): - bl_label = "IFC Geometric Representation Contexts" + bl_label = "Geometric Representation Contexts" bl_idname = "BIM_PT_context" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 3e92163157..c083331c76 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -24,13 +24,13 @@ from blenderbim.bim.module.cost.data import CostSchedulesData class BIM_PT_cost_schedules(Panel): - bl_label = "IFC Cost Schedules" + bl_label = "Cost Schedules" bl_idname = "BIM_PT_cost_schedules" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_4D5D" + bl_parent_id = "BIM_PT_tab_4D5D" @classmethod def poll(cls, context): @@ -267,7 +267,7 @@ class BIM_PT_cost_schedules(Panel): class BIM_PT_cost_item_types(Panel): - bl_label = "IFC Cost Item Types" + bl_label = "Cost Item Types" bl_idname = "BIM_PT_cost_item_types" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -353,7 +353,7 @@ class BIM_PT_cost_item_types(Panel): class BIM_PT_cost_item_quantities(Panel): - bl_label = "IFC Cost Item Quantities" + bl_label = "Cost Item Quantities" bl_idname = "BIM_PT_cost_item_quantities" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -525,7 +525,7 @@ class BIM_PT_cost_item_quantities(Panel): class BIM_PT_cost_item_rates(Panel): - bl_label = "IFC Cost Item Rates" + bl_label = "Cost Item Rates" bl_idname = "BIM_PT_cost_item_rates" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" diff --git a/src/blenderbim/blenderbim/bim/module/covetool/ui.py b/src/blenderbim/blenderbim/bim/module/covetool/ui.py index f74aa1a97b..4c055db03e 100644 --- a/src/blenderbim/blenderbim/bim/module/covetool/ui.py +++ b/src/blenderbim/blenderbim/bim/module/covetool/ui.py @@ -26,7 +26,7 @@ class BIM_PT_covetool(bpy.types.Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_integrations" + bl_parent_id = "BIM_PT_tab_integrations" def draw(self, context): layout = self.layout diff --git a/src/blenderbim/blenderbim/bim/module/csv/ui.py b/src/blenderbim/blenderbim/bim/module/csv/ui.py index 7c1df7a4d2..9f690a762a 100644 --- a/src/blenderbim/blenderbim/bim/module/csv/ui.py +++ b/src/blenderbim/blenderbim/bim/module/csv/ui.py @@ -21,13 +21,13 @@ from blenderbim.bim.ifc import IfcStore class BIM_PT_ifccsv(Panel): - bl_label = "IFC CSV Import/Export" + bl_label = "CSV Import/Export" bl_idname = "BIM_PT_ifccsv" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_collaboration" + bl_parent_id = "BIM_PT_tab_collaboration" def draw(self, context): layout = self.layout diff --git a/src/blenderbim/blenderbim/bim/module/debug/ui.py b/src/blenderbim/blenderbim/bim/module/debug/ui.py index 7e06e2fb19..1ca48978be 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/ui.py +++ b/src/blenderbim/blenderbim/bim/module/debug/ui.py @@ -21,13 +21,13 @@ from bpy.types import Panel class BIM_PT_debug(Panel): - bl_label = "IFC Debug" + bl_label = "Debug" bl_idname = "BIM_PT_debug" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_quality_control" + bl_parent_id = "BIM_PT_tab_quality_control" def draw(self, context): layout = self.layout diff --git a/src/blenderbim/blenderbim/bim/module/diff/ui.py b/src/blenderbim/blenderbim/bim/module/diff/ui.py index 520a26ae39..a0343e2042 100644 --- a/src/blenderbim/blenderbim/bim/module/diff/ui.py +++ b/src/blenderbim/blenderbim/bim/module/diff/ui.py @@ -24,13 +24,13 @@ import json class BIM_PT_diff(Panel): - bl_label = "IFC Diff" + bl_label = "Diff" bl_idname = "BIM_PT_diff" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_quality_control" + bl_parent_id = "BIM_PT_tab_quality_control" def draw(self, context): if not DiffData.is_loaded: diff --git a/src/blenderbim/blenderbim/bim/module/document/ui.py b/src/blenderbim/blenderbim/bim/module/document/ui.py index b20f0319e9..a656f4f576 100644 --- a/src/blenderbim/blenderbim/bim/module/document/ui.py +++ b/src/blenderbim/blenderbim/bim/module/document/ui.py @@ -23,7 +23,7 @@ from blenderbim.bim.module.document.data import DocumentData, ObjectDocumentData class BIM_PT_documents(Panel): - bl_label = "IFC Documents" + bl_label = "Documents" bl_idname = "BIM_PT_documents" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -76,14 +76,14 @@ class BIM_PT_documents(Panel): class BIM_PT_object_documents(Panel): - bl_label = "IFC Documents" + bl_label = "Documents" bl_idname = "BIM_PT_object_documents" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" bl_order = 1 - bl_parent_id = "BIM_PT_misc_object" + bl_parent_id = "BIM_PT_tab_misc" @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py index f5ce7c2840..4f3764df3c 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/decoration.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/decoration.py @@ -748,7 +748,7 @@ class AngleDecorator(BaseDecorator): arcs_color = None edges_color = UNSPECIAL_ELEMENT_COLOR - if context.object == obj and obj.data.is_editmode: + if context.active_object == obj and obj.data.is_editmode: arcs_color = context.preferences.addons["blenderbim"].preferences.decorator_color_special edges_color = None diff --git a/src/blenderbim/blenderbim/bim/module/drawing/helper.py b/src/blenderbim/blenderbim/bim/module/drawing/helper.py index 100880fb26..95bd35b77b 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/helper.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/helper.py @@ -192,6 +192,8 @@ def format_distance( tx_dist += str(feet) + "'" if feet and add_inches: tx_dist += " - " + if not feet and value < 0: + tx_dist += "-" if add_inches: tx_dist += str(inches) if add_inches and frac: diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index 6ef6f9bbc0..8c5bdfd3ee 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -23,6 +23,7 @@ import json import time import bmesh import shutil +import hashlib import shapely import subprocess import webbrowser @@ -365,7 +366,6 @@ class CreateDrawing(bpy.types.Operator): return svg_path def generate_linework(self, context): - global ifcopenshell if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"): return svg_path = self.get_svg_path(cache_type="linework") @@ -395,10 +395,6 @@ class CreateDrawing(bpy.types.Operator): } cached_linework -= edited_guids - # This is a work in progress. See #1153 and #1564. - import hashlib - import ifcopenshell.draw - files = {context.scene.BIMProperties.ifc_file: tool.Ifc.get()} for ifc_path, ifc in files.items(): @@ -569,7 +565,7 @@ class CreateDrawing(bpy.types.Operator): for projection in projections: boundary_lines = [] for path in projection.findall("./{http://www.w3.org/2000/svg}path"): - start, end = [co[1:].split(",") for co in path.attrib["d"].split()] + start, end = [[round(float(o), 1) for o in co[1:].split(",")] for co in path.attrib["d"].split()] boundary_lines.append(shapely.LineString([start, end])) unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines)) closed_polygons = shapely.polygonize(unioned_boundaries.geoms) @@ -896,6 +892,42 @@ class CreateDrawing(bpy.types.Operator): classes.append("cut") el.set("class", " ".join(classes)) + # An element group will contain a bunch of paths representing the + # cut of that element. However IfcOpenShell may not correctly + # create closed paths. We post-process all paths with shapely to + # ensure things that should be closed (i.e. + # shapely.polygonize_full) are, and things which aren't are left + # alone (e.g. dangles, cuts, invalids). See #3421. + line_strings = [] + old_paths = [] + for path in el.findall("{http://www.w3.org/2000/svg}path"): + for subpath in path.attrib["d"].split("M")[1:]: + subpath = "M" + subpath.strip() + coords = [[round(float(o), 1) for o in co[1:].split(",")] for co in subpath.split()] + line_strings.append(shapely.LineString(coords)) + old_paths.append(path) + unioned_line_strings = shapely.union_all(shapely.GeometryCollection(line_strings)) + if hasattr(unioned_line_strings, "geoms"): + results = shapely.polygonize_full(unioned_line_strings.geoms) + else: + results = [] + + # If we succeeded in generating new path geometry, remove all the + # old paths and add new ones. + if results: + for path in old_paths: + path.getparent().remove(path) + for result in results: + for geom in result.geoms: + path = etree.SubElement(el, "path") + if isinstance(geom, shapely.Polygon): + d = "M" + " L".join([",".join([str(o) for o in co]) for co in geom.exterior.coords[0:-1]]) + " Z" + for interior in geom.interiors: + d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z" + elif isinstance(geom, shapely.LineString): + d = "M" + " L".join([",".join([str(o) for o in co]) for co in geom.coords]) + " Z" + path.attrib["d"] = d + # Architectural convention only merges these objects. E.g. pipe segments and fittings shouldn't merge. if not element.is_a("IfcWall") and not element.is_a("IfcSlab"): continue diff --git a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py index bb65b52f67..625bb8af47 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py @@ -332,9 +332,10 @@ class SvgWriter: return classes = self.get_attribute_classes(obj) - if len(obj.data.polygons) == 0: - self.draw_edge_annotation(obj, classes) - return + if len(obj.data.vertices) and not len(obj.data.edges): + return self.draw_point_annotation(obj, classes) + elif len(obj.data.polygons) == 0: + return self.draw_edge_annotation(obj, classes) bm = bmesh.new() bm.from_mesh(obj.data) @@ -501,9 +502,9 @@ class SvgWriter: return pattern def get_scale(size, direction): - vector = direction * size - shrinked_vector = size // segment_width * segment_width * direction - scale = [1 if vector[i] == 0 else vector[i] / shrinked_vector[i] for i in range(2)] + original_edge = direction * size + current_svg_segments = ceil(size / segment_width) * segment_width * direction + scale = [1 if original_edge[i] == 0 else original_edge[i] / current_svg_segments[i] for i in range(2)] return "scale(%f, %f)" % (scale[0], scale[1]) def poly_to_edges(poly): @@ -557,11 +558,16 @@ class SvgWriter: pattern_dir = pattern_edge.normalized() pattern_length = pattern_edge.length - segments = int(pattern_length // segment_width) + segments = ceil(pattern_length / segment_width) pattern_dir_step = pattern_dir * segment_width - points = [pattern_dir_step * i for i in range(segments)] + # it takes atleast 2 points to preserve the edge direction + # if there is just 1 segment then we still add second point and then hide the "marker-end" + n_points = max(segments, 2) + points = [pattern_dir_step * i for i in range(n_points)] - polyline_style = f"marker: url(#{marker_id}); stroke: none;" + polyline_style = f"marker: url(#{marker_id}); stroke: none; " + if segments == 1: + polyline_style += "marker-end: none; " polyline_transform = f"translate({start_svg.x}, {start_svg.y}) {get_scale(pattern_length, pattern_dir)}" polyline = self.svg.polyline( points=points, class_=" ".join(classes), style=polyline_style, transform=polyline_transform @@ -837,6 +843,26 @@ class SvgWriter: self.svg.add(tag) line_number += len(tag.elements) + def draw_point_annotation(self, obj, classes): + x_offset = self.raw_width / 2 + y_offset = self.raw_height / 2 + + matrix_world = obj.matrix_world + projected_points = [self.project_point_onto_camera(matrix_world @ v.co) for v in obj.data.vertices] + + element = tool.Ifc.get_entity(obj) + svg_id = str(ifcopenshell.util.element.get_predefined_type(element)) + + # EPset_AnnotationSurveyArea is not standard! See bSI-4.3 proposal #660. + point_type = ifcopenshell.util.element.get_pset(element, "EPset_AnnotationSurveyArea", "PointType") + if point_type: + svg_id += f"-{point_type}" + + for symbol_position in projected_points: + symbol_position = Vector(((x_offset + symbol_position.x), (y_offset - symbol_position.y))) + symbol_position_svg = symbol_position * self.svg_scale + self.svg.add(self.svg.use(f"#{svg_id}", insert=symbol_position_svg)) + def draw_break_annotations(self, obj): x_offset = self.raw_width / 2 y_offset = self.raw_height / 2 @@ -880,8 +906,13 @@ class SvgWriter: d = "M{}".format(d[1:]) path = self.svg.add(self.svg.path(d=d, class_=" ".join(classes))) text_position = projected_points_svg[0] - Vector((0, base_offset_y)) - vector = projected_points_svg[1] - projected_points_svg[0] - angle = math.degrees(vector.angle_signed(Vector((1, 0)))) + text_dir = projected_points_svg[1] - projected_points_svg[0] + if text_dir.x < 0: + box_alignment = "bottom-right" + text_dir *= -1 + else: + box_alignment = "bottom-left" + angle = math.degrees(text_dir.angle_signed(Vector((1, 0)))) # TODO: allow metric to be configurable def get_text(): @@ -895,7 +926,6 @@ class SvgWriter: text = "{}{}".format("" if z < 0 else "+", rl) return text - box_alignment = "bottom-left" if projected_points[0].x <= projected_points[-1].x else "bottom-right" self.draw_dimension_text( get_text, description, @@ -931,7 +961,7 @@ class SvgWriter: p0 = points_2d[1] + dir0 * angle_radius p2 = points_2d[1] + dir1 * angle_radius points_chunk = [view3d_utils.region_2d_to_origin_3d(region, region_3d, p) for p in [p0, p3, p2]] - # points = [p.co.xyz for p in bpy.context.object.data.splines[0].points[:3]] + # points = [p.co.xyz for p in bpy.context.active_object.data.splines[0].points[:3]] bm = bmesh.new() bm.verts.index_update() diff --git a/src/blenderbim/blenderbim/bim/module/drawing/ui.py b/src/blenderbim/blenderbim/bim/module/drawing/ui.py index dad42d1ca6..a0a461a8dd 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/ui.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/ui.py @@ -387,7 +387,7 @@ class BIM_PT_sheets(Panel): class BIM_PT_product_assignments(Panel): - bl_label = "IFC Product Assignments" + bl_label = "Product Assignments" bl_idname = "BIM_PT_product_assignments" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -422,7 +422,7 @@ class BIM_PT_product_assignments(Panel): class BIM_PT_text(Panel): - bl_label = "IFC Text" + bl_label = "Text" bl_idname = "BIM_PT_text" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" diff --git a/src/blenderbim/blenderbim/bim/module/drawing/workspace.py b/src/blenderbim/blenderbim/bim/module/drawing/workspace.py index 3193a23271..73704e32e7 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/workspace.py @@ -101,6 +101,8 @@ class LaunchAnnotationTypeManager(bpy.types.Operator): op = row.operator("bim.select_type", icon="OBJECT_DATA") op.relating_type = relating_type["id"] + op = row.operator("bim.rename_type", icon="GREASEPENCIL", text="") + op.element = relating_type["id"] op = row.operator("bim.duplicate_type", icon="DUPLICATE", text="") op.element = relating_type["id"] op = row.operator("bim.remove_type", icon="X", text="") @@ -227,7 +229,7 @@ class AnnotationToolUI: @classmethod def draw_edit_object_interface(cls, context): - if DecoratorData.get_ifc_text_data(bpy.context.object): + if DecoratorData.get_ifc_text_data(bpy.context.active_object): add_layout_hotkey_operator(cls.layout, "Edit Text", "S_E", "") @classmethod @@ -246,16 +248,16 @@ class AnnotationToolUI: add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation") - if object_type in ("TEXT", "STAIR_ARROW"): + if object_type in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP: add_layout_hotkey_operator( cls.layout, "Bulk Tag", "S_T", "Create new annotations and automatically adjust them to the selected objects", ) - add_layout_hotkey_operator( - cls.layout, "Readjust", "S_G", "Readjust tags based on the products they are assigned to" - ) + add_layout_hotkey_operator( + cls.layout, "Readjust", "S_G", "Readjust tags based on the products they are assigned to" + ) class Hotkey(bpy.types.Operator, Operator): @@ -287,23 +289,27 @@ class Hotkey(bpy.types.Operator, Operator): def hotkey_S_T(self): props = bpy.context.scene.BIMAnnotationProperties - object_type = props.object_type + annotation_type = props.object_type + + if annotation_type not in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP: + self.report({"ERROR"}, f"Annotation type {annotation_type} is not supported for tagging.") + return related_objects = bpy.context.selected_objects for related_object in related_objects: create_annotation() obj = bpy.context.active_object bpy.ops.object.mode_set(mode="OBJECT") - tool.Drawing.setup_annotation_object(obj, object_type, related_object) + tool.Drawing.setup_annotation_object(obj, annotation_type, related_object) def hotkey_S_A(self): create_annotation() def hotkey_S_E(self): - if not bpy.context.object: + if not bpy.context.active_object: return - if DecoratorData.get_ifc_text_data(bpy.context.object): + if DecoratorData.get_ifc_text_data(bpy.context.active_object): bpy.ops.bim.edit_text_popup() def hotkey_S_G(self): @@ -312,9 +318,15 @@ class Hotkey(bpy.types.Operator, Operator): if not element or not element.is_a("IfcAnnotation"): continue + annotation_type = element.ObjectType + if annotation_type not in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP: + self.report({"ERROR"}, f"Annotation type {annotation_type} is not supported for readjustment.") + continue + related_product = tool.Drawing.get_assigned_product(element) if not related_product: + self.report({"ERROR"}, "Selected annotation has no product assigned.") continue related_object = tool.Ifc.get_object(related_product) - tool.Drawing.setup_annotation_object(obj, element.ObjectType, related_object) + tool.Drawing.setup_annotation_object(obj, annotation_type, related_object) diff --git a/src/blenderbim/blenderbim/bim/module/geometry/operator.py b/src/blenderbim/blenderbim/bim/module/geometry/operator.py index a6f29b3354..6d1f671d2d 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/operator.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/operator.py @@ -598,6 +598,9 @@ class OverrideDuplicateMove(bpy.types.Operator): relationships = tool.Root.get_decomposition_relationships(context.selected_objects) old_to_new = {} for obj in context.selected_objects: + element = tool.Ifc.get_entity(obj) + if element and element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING": + continue # For now, don't copy drawings until we stabilise a bit more. It's tricky. new_obj = obj.copy() if obj.data: new_obj.data = obj.data.copy() @@ -1017,7 +1020,6 @@ class RefreshAggregate(bpy.types.Operator): return parents def duplicate_children(entity): - pset = ifcopenshell.util.element.get_pset(entity, "BBIM_Aggregate_Data") pset_data = json.loads(pset["Data"])[0] instance_of = pset_data["instance_of"][0] @@ -1236,6 +1238,7 @@ class OverridePasteBuffer(bpy.types.Operator): class OverrideModeSetEdit(bpy.types.Operator): + bl_description = "Switch from Object mode to Edit mode" bl_idname = "bim.override_mode_set_edit" bl_label = "IFC Mode Set Edit" bl_options = {"REGISTER", "UNDO"} @@ -1244,7 +1247,7 @@ class OverrideModeSetEdit(bpy.types.Operator): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - objs = context.selected_objects or ([context.active_object] if context.active_object else []) + selected_objs = context.selected_objects or ([context.active_object] if context.active_object else []) active_obj = context.active_object if context.active_object: @@ -1254,19 +1257,15 @@ class OverrideModeSetEdit(bpy.types.Operator): if element and element.is_a("IfcRelSpaceBoundary"): return bpy.ops.bim.enable_editing_boundary_geometry() - for obj in objs: + for obj in selected_objs: if not obj: continue - if not obj.data: obj.select_set(False) continue - element = tool.Ifc.get_entity(obj) if not element: continue - - # We are switching from OBJECT to EDIT mode. usage_type = tool.Model.get_usage_type(element) if usage_type is not None and usage_type not in ("PROFILE", "LAYER3"): # Parametric objects shall not be edited as meshes as they @@ -1274,57 +1273,37 @@ class OverrideModeSetEdit(bpy.types.Operator): # constraints. obj.select_set(False) continue - representation = tool.Geometry.get_active_representation(obj) if not representation: continue - - if ( - tool.Pset.get_element_pset(element, "BBIM_Door") - or tool.Pset.get_element_pset(element, "BBIM_Window") - or tool.Pset.get_element_pset(element, "BBIM_Stair") - ): + if tool.Blender.Modifier.is_modifier_with_non_editable_path(element): obj.select_set(False) continue + is_profile = True if usage_type == "PROFILE": - if len(context.selected_objects) == 1: - bpy.ops.bim.hotkey(hotkey="A_E", description="") - return {"FINISHED"} - else: - self.report({"INFO"}, "Only a single profile-based representation can be edited at a time.") - obj.select_set(False) - continue - - # TODO: refactor repetitive code - if tool.Pset.get_element_pset(element, "BBIM_Roof"): - if len(context.selected_objects) == 1: - bpy.ops.bim.enable_editing_roof_path() - return {"FINISHED"} - else: - self.report({"INFO"}, "Only a single profile-based representation can be edited at a time.") - obj.select_set(False) - continue - - if tool.Pset.get_element_pset(element, "BBIM_Railing"): - if len(context.selected_objects) == 1: - if obj.BIMRailingProperties.is_editing == 1: - self.report({"INFO"}, "Can't edit path while the modifier parameters are being modified") - return {"FINISHED"} - bpy.ops.bim.enable_editing_railing_path() - return {"FINISHED"} - else: - self.report({"INFO"}, "Only a single profile-based representation can be edited at a time.") - obj.select_set(False) - continue - - if ( + operator = lambda: bpy.ops.bim.hotkey(hotkey="A_E") + elif ( tool.Geometry.is_profile_based(obj.data) or usage_type == "LAYER3" or tool.Geometry.is_swept_profile(representation) ): - if len(context.selected_objects) == 1: - bpy.ops.bim.hotkey(hotkey="S_E", description="") + operator = lambda: bpy.ops.bim.hotkey(hotkey="S_E") + elif tool.Blender.Modifier.is_editing_parameters(obj): + # This should go BEFORE the modifiers + self.report({"INFO"}, "Can't edit path while the modifier parameters are being modified") + obj.select_set(False) + continue + elif tool.Blender.Modifier.is_roof(element): + operator = lambda: bpy.ops.bim.enable_editing_roof_path() + elif tool.Blender.Modifier.is_railing(element): + operator = lambda: bpy.ops.bim.enable_editing_railing_path() + else: + is_profile = False + if is_profile: + if len(context.selected_objects) == 1 and context.active_object == context.selected_objects[0]: + tool.Blender.select_and_activate_single_object(context, obj) + operator() return {"FINISHED"} else: self.report({"INFO"}, "Only a single profile-based representation can be edited at a time.") @@ -1350,17 +1329,18 @@ class OverrideModeSetEdit(bpy.types.Operator): obj.select_set(False) continue - if not context.selected_objects or len(context.selected_objects) != len(objs): + if not context.selected_objects or len(context.selected_objects) != len(selected_objs): # We are trying to edit at least one non-mesh-like object : Display a hint to the user - self.report({"INFO"}, "Only mesh-compatible representations may be edited in edit mode.") + self.report({"INFO"}, "Only mesh-compatible representations may be edited concurrently in edit mode.") if context.active_object not in context.selected_objects: # The active object is non-mesh-like. Set a valid object (or None) as active context.view_layer.objects.active = context.selected_objects[0] if context.selected_objects else None if context.active_object: return tool.Blender.toggle_edit_mode(context) + # Restore the selection if nothing worked - for obj in objs: + for obj in selected_objs: obj.select_set(True) context.view_layer.objects.active = active_obj return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/geometry/ui.py b/src/blenderbim/blenderbim/bim/module/geometry/ui.py index 26d1a9ddc6..4725373914 100644 --- a/src/blenderbim/blenderbim/bim/module/geometry/ui.py +++ b/src/blenderbim/blenderbim/bim/module/geometry/ui.py @@ -17,6 +17,7 @@ # along with BlenderBIM Add-on. If not, see . import bpy +import blenderbim.tool as tool from bpy.types import Panel from blenderbim.bim.ifc import IfcStore from blenderbim.bim.helper import prop_with_search @@ -29,19 +30,21 @@ def object_menu(self, context): self.layout.operator("bim.override_object_delete", icon="PLUGIN") self.layout.operator("bim.override_paste_buffer", icon="PLUGIN") + def outliner_menu(self, context): self.layout.separator() - self.layout.operator("bim.override_outliner_delete", icon='X') + self.layout.operator("bim.override_outliner_delete", icon="X") + class BIM_PT_representations(Panel): - bl_label = "IFC Representations" + bl_label = "Representations" bl_idname = "BIM_PT_representations" - bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" - bl_context = "object" + bl_context = "scene" bl_order = 1 - bl_parent_id = "BIM_PT_geometry_object" + bl_parent_id = "BIM_PT_tab_representations" + bl_options = {"HIDE_HEADER"} @classmethod def poll(cls, context): @@ -58,20 +61,24 @@ class BIM_PT_representations(Panel): layout = self.layout props = context.active_object.BIMObjectProperties - if not RepresentationsData.data["representations"]: - layout.label(text="No representations found") - row = layout.row(align=True) prop_with_search(row, context.active_object.BIMGeometryProperties, "contexts", text="") row.operator("bim.add_representation", icon="ADD", text="") + if not RepresentationsData.data["representations"]: + layout.label(text="No Representations Found") + for representation in RepresentationsData.data["representations"]: row = self.layout.row(align=True) row.label(text=representation["ContextType"]) row.label(text=representation["ContextIdentifier"]) row.label(text=representation["TargetView"]) row.label(text=representation["RepresentationType"]) - op = row.operator("bim.switch_representation", icon="FILE_REFRESH" if representation["is_active"] else "OUTLINER_DATA_MESH", text="") + op = row.operator( + "bim.switch_representation", + icon="FILE_REFRESH" if representation["is_active"] else "OUTLINER_DATA_MESH", + text="", + ) op.should_switch_all_meshes = True op.should_reload = True op.ifc_definition_id = representation["id"] @@ -80,14 +87,14 @@ class BIM_PT_representations(Panel): class BIM_PT_connections(Panel): - bl_label = "IFC Connections" + bl_label = "Connections" bl_idname = "BIM_PT_connections" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" bl_order = 1 - bl_parent_id = "BIM_PT_geometry_object" + bl_parent_id = "BIM_PT_tab_geometric_relationships" @classmethod def poll(cls, context): @@ -118,7 +125,7 @@ class BIM_PT_connections(Panel): class BIM_PT_mesh(Panel): - bl_label = "IFC Representation" + bl_label = "Representation" bl_idname = "BIM_PT_mesh" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -185,7 +192,7 @@ def BIM_PT_transform(self, context): class BIM_PT_derived_placements(Panel): - bl_label = "IFC Derived Placements" + bl_label = "Derived Placements" bl_idname = "BIM_PT_derived_placements" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -220,7 +227,7 @@ class BIM_PT_derived_placements(Panel): class BIM_PT_workarounds(Panel): - bl_label = "IFC Vendor Workarounds" + bl_label = "Vendor Workarounds" bl_idname = "BIM_PT_workarounds" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" diff --git a/src/blenderbim/blenderbim/bim/module/georeference/ui.py b/src/blenderbim/blenderbim/bim/module/georeference/ui.py index d7c3131a0a..bb75f06f5e 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/ui.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/ui.py @@ -23,7 +23,7 @@ from blenderbim.bim.module.georeference.data import GeoreferenceData class BIM_PT_gis(Panel): - bl_label = "IFC Georeferencing" + bl_label = "Georeferencing" bl_idname = "BIM_PT_gis" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" diff --git a/src/blenderbim/blenderbim/bim/module/gis/ui.py b/src/blenderbim/blenderbim/bim/module/gis/ui.py index 1ccc930928..a57b8d2c1c 100644 --- a/src/blenderbim/blenderbim/bim/module/gis/ui.py +++ b/src/blenderbim/blenderbim/bim/module/gis/ui.py @@ -20,13 +20,13 @@ from bpy.types import Panel class BIM_PT_cityjson_converter(Panel): - bl_label = "IFC CityJSON" + bl_label = "CityJSON" bl_idname = "BIM_PT_ifccityjson" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_collaboration" + bl_parent_id = "BIM_PT_tab_collaboration" def draw(self, context): layout = self.layout diff --git a/src/blenderbim/blenderbim/bim/module/group/ui.py b/src/blenderbim/blenderbim/bim/module/group/ui.py index 874d239bf2..a2d6d8a362 100644 --- a/src/blenderbim/blenderbim/bim/module/group/ui.py +++ b/src/blenderbim/blenderbim/bim/module/group/ui.py @@ -22,7 +22,7 @@ from blenderbim.bim.module.group.data import GroupsData, ObjectGroupsData class BIM_PT_groups(Panel): - bl_label = "IFC Groups" + bl_label = "Groups" bl_idname = "BIM_PT_groups" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -69,14 +69,14 @@ class BIM_PT_groups(Panel): class BIM_PT_object_groups(Panel): - bl_label = "IFC Groups" + bl_label = "Groups" bl_idname = "BIM_PT_object_groups" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" bl_order = 1 - bl_parent_id = "BIM_PT_utilities_object" + bl_parent_id = "BIM_PT_tab_misc" @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/ifcgit/ui.py b/src/blenderbim/blenderbim/bim/module/ifcgit/ui.py index 286e900adc..d54591a509 100644 --- a/src/blenderbim/blenderbim/bim/module/ifcgit/ui.py +++ b/src/blenderbim/blenderbim/bim/module/ifcgit/ui.py @@ -6,7 +6,7 @@ from blenderbim.bim.module.ifcgit.data import IfcGitData class IFCGIT_PT_panel(bpy.types.Panel): """Scene Properties panel to interact with IFC repository data""" - bl_label = "IFC Git" + bl_label = "Git" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" diff --git a/src/blenderbim/blenderbim/bim/module/layer/ui.py b/src/blenderbim/blenderbim/bim/module/layer/ui.py index 179ea568a2..218fd4046d 100644 --- a/src/blenderbim/blenderbim/bim/module/layer/ui.py +++ b/src/blenderbim/blenderbim/bim/module/layer/ui.py @@ -22,13 +22,13 @@ from blenderbim.bim.module.layer.data import LayersData class BIM_PT_layers(Panel): - bl_label = "IFC Presentation Layers" + bl_label = "Presentation Layers" bl_idname = "BIM_PT_layers" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_geometry_object" + bl_parent_id = "BIM_PT_tab_geometric_relationships" @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/lca/ui.py b/src/blenderbim/blenderbim/bim/module/lca/ui.py index a76b38d8f8..5e06fd0098 100644 --- a/src/blenderbim/blenderbim/bim/module/lca/ui.py +++ b/src/blenderbim/blenderbim/bim/module/lca/ui.py @@ -27,7 +27,7 @@ class BIM_PT_lca(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_integrations" + bl_parent_id = "BIM_PT_tab_integrations" def draw(self, context): props = context.scene.BIMLCAProperties diff --git a/src/blenderbim/blenderbim/bim/module/library/ui.py b/src/blenderbim/blenderbim/bim/module/library/ui.py index 2057e45a98..c963203cd7 100644 --- a/src/blenderbim/blenderbim/bim/module/library/ui.py +++ b/src/blenderbim/blenderbim/bim/module/library/ui.py @@ -23,7 +23,7 @@ from blenderbim.bim.module.library.data import LibrariesData, LibraryReferencesD class BIM_PT_libraries(Panel): - bl_label = "IFC Libraries" + bl_label = "Libraries" bl_idname = "BIM_PT_libraries" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -94,14 +94,14 @@ class BIM_PT_libraries(Panel): class BIM_PT_library_references(Panel): - bl_label = "IFC Library References" + bl_label = "Library References" bl_idname = "BIM_PT_library_references" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" bl_order = 1 - bl_parent_id = "BIM_PT_misc_object" + bl_parent_id = "BIM_PT_tab_misc" @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/material/ui.py b/src/blenderbim/blenderbim/bim/module/material/ui.py index da1490aae1..773a3e14db 100644 --- a/src/blenderbim/blenderbim/bim/module/material/ui.py +++ b/src/blenderbim/blenderbim/bim/module/material/ui.py @@ -17,6 +17,7 @@ # along with BlenderBIM Add-on. If not, see . import blenderbim.bim.helper +import blenderbim.tool as tool from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore from blenderbim.bim.helper import draw_attributes @@ -25,7 +26,7 @@ from blenderbim.bim.module.material.data import MaterialsData, ObjectMaterialDat class BIM_PT_materials(Panel): - bl_label = "IFC Materials" + bl_label = "Materials" bl_idname = "BIM_PT_materials" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -88,7 +89,7 @@ class BIM_PT_materials(Panel): blenderbim.bim.helper.draw_attributes(self.props.material_attributes, self.layout) class BIM_PT_material(Panel): - bl_label = "IFC Material" + bl_label = "Material" bl_idname = "BIM_PT_material" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -111,15 +112,17 @@ class BIM_PT_material(Panel): class BIM_PT_object_material(Panel): - bl_label = "IFC Object Material" + bl_label = "Object Material" bl_idname = "BIM_PT_object_material" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" - bl_context = "object" - bl_parent_id = "BIM_PT_object_metadata" + bl_context = "scene" + bl_parent_id = "BIM_PT_tab_materials" @classmethod def poll(cls, context): + if not tool.Blender.is_tab(context, "GEOMETRY"): + return False if not context.active_object: return False props = context.active_object.BIMObjectProperties diff --git a/src/blenderbim/blenderbim/bim/module/model/data.py b/src/blenderbim/blenderbim/bim/module/model/data.py index 66a4ca1cf5..e9a18c02af 100644 --- a/src/blenderbim/blenderbim/bim/module/model/data.py +++ b/src/blenderbim/blenderbim/bim/module/model/data.py @@ -67,6 +67,7 @@ class AuthoringData: cls.data["active_material_usage"] = cls.active_material_usage() cls.data["active_representation_type"] = cls.active_representation_type() cls.data["boundary_class"] = cls.boundary_class() + cls.data["selected_material_usages"] = cls.selected_material_usages() @classmethod def boundary_class(cls): @@ -238,6 +239,23 @@ class AuthoringData: return [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in results] return [] + @classmethod + def selected_material_usages(cls): + selected_usages = {} + for obj in bpy.context.selected_objects: + element = tool.Ifc.get_entity(obj) + if not element: + continue + usage = tool.Model.get_usage_type(element) + if not usage: + representation = tool.Geometry.get_active_representation(obj) + if representation and representation.RepresentationType == "SweptSolid": + usage = "SWEPTSOLID" + else: + continue + selected_usages.setdefault(usage, []).append(obj) + return selected_usages + class ArrayData: data = {} @@ -272,12 +290,27 @@ class StairData: @classmethod def load(cls): cls.is_loaded = True - cls.data = {"pset_data": cls.pset_data()} + cls.data = {} + cls.data["pset_data"] = cls.pset_data() + if not cls.data["pset_data"]: + return + cls.data["general_params"] = cls.general_params() @classmethod def pset_data(cls): return tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Stair") + @classmethod + def general_params(cls): + props = bpy.context.active_object.BIMStairProperties + data = cls.data["pset_data"]["data_dict"] + general_params = {} + general_props = props.get_props_kwargs(stair_type=data["stair_type"]) + for prop_name in general_props: + prop_readable_name, prop_value = get_prop_from_data(props, data, prop_name) + general_params[prop_readable_name] = prop_value + return general_params + class SverchokData: data = {} diff --git a/src/blenderbim/blenderbim/bim/module/model/door.py b/src/blenderbim/blenderbim/bim/module/model/door.py index 838e42bf9a..6150f472c6 100644 --- a/src/blenderbim/blenderbim/bim/module/model/door.py +++ b/src/blenderbim/blenderbim/bim/module/model/door.py @@ -276,7 +276,7 @@ def create_bm_door_lining(bm, size: Vector, thickness: list, position: Vector = def update_door_modifier_bmesh(context): - obj = context.object + obj = context.active_object props = obj.BIMDoorProperties overall_width = props.overall_width @@ -456,7 +456,7 @@ def update_door_modifier_bmesh(context): bmesh.ops.translate(bm, vec=V(0, lining_offset, 0), verts=lining_offset_verts) bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) - if bpy.context.object.mode == "EDIT": + if bpy.context.active_object.mode == "EDIT": bmesh.update_edit_mesh(obj.data) else: bm.to_mesh(obj.data) @@ -475,9 +475,9 @@ class BIM_OT_add_door(bpy.types.Operator, tool.Ifc.Operator): self.report({"ERROR"}, "You need to start IFC project first to create a door.") return {"CANCELLED"} - if context.object is not None: - spawn_location = context.object.location.copy() - context.object.select_set(False) + if context.active_object is not None: + spawn_location = context.active_object.location.copy() + context.active_object.select_set(False) else: spawn_location = bpy.context.scene.cursor.location.copy() diff --git a/src/blenderbim/blenderbim/bim/module/model/grid.py b/src/blenderbim/blenderbim/bim/module/model/grid.py index adb4a240c4..3159f03c3b 100644 --- a/src/blenderbim/blenderbim/bim/module/model/grid.py +++ b/src/blenderbim/blenderbim/bim/module/model/grid.py @@ -98,7 +98,7 @@ def add_object(self, context): class BIM_OT_add_object(Operator, tool.Ifc.Operator): bl_idname = "mesh.add_grid" - bl_label = "IFC Grid" + bl_label = "Grid" bl_options = {"REGISTER", "UNDO"} u_spacing: FloatProperty(name="U Spacing", default=10) diff --git a/src/blenderbim/blenderbim/bim/module/model/opening.py b/src/blenderbim/blenderbim/bim/module/model/opening.py index ef7ea63bc8..c527348eab 100644 --- a/src/blenderbim/blenderbim/bim/module/model/opening.py +++ b/src/blenderbim/blenderbim/bim/module/model/opening.py @@ -498,10 +498,12 @@ class AddBoolean(Operator, tool.Ifc.Operator): bl_label = "Add Boolean" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return len(context.selected_objects) == 2 + def _execute(self, context): props = context.scene.BIMModelProperties - if len(context.selected_objects) != 2: - return {"FINISHED"} obj1, obj2 = context.selected_objects element1 = tool.Ifc.get_entity(obj1) element2 = tool.Ifc.get_entity(obj2) @@ -546,14 +548,19 @@ class ShowBooleans(Operator, tool.Ifc.Operator, AddObjectHelper): bl_label = "Show Booleans" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + obj = context.active_object + return ( + obj is not None + and obj.data + and hasattr(obj.data, "BIMMeshProperties") + and obj.data.BIMMeshProperties.ifc_definition_id + ) + + def _execute(self, context): obj = context.active_object - if ( - not obj.data - or not hasattr(obj.data, "BIMMeshProperties") - or not obj.data.BIMMeshProperties.ifc_definition_id - ): - return {"FINISHED"} unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) booleans = [] diff --git a/src/blenderbim/blenderbim/bim/module/model/pie.py b/src/blenderbim/blenderbim/bim/module/model/pie.py index f29e286af9..f43af88132 100644 --- a/src/blenderbim/blenderbim/bim/module/model/pie.py +++ b/src/blenderbim/blenderbim/bim/module/model/pie.py @@ -92,7 +92,7 @@ class VIEW3D_MT_PIE_bim(bpy.types.Menu): class VIEW3D_MT_PIE_bim_class(bpy.types.Menu): - bl_label = "IFC Class" + bl_label = "Class" def draw(self, context): pie = self.layout.menu_pie() diff --git a/src/blenderbim/blenderbim/bim/module/model/prop.py b/src/blenderbim/blenderbim/bim/module/model/prop.py index 10b8473e9d..90adc41ec5 100644 --- a/src/blenderbim/blenderbim/bim/module/model/prop.py +++ b/src/blenderbim/blenderbim/bim/module/model/prop.py @@ -99,7 +99,7 @@ class BIMModelProperties(PropertyGroup): ) occurrence_name_function: bpy.props.StringProperty(name="Occurrence Name Function") getter_enum = {"ifc_class": get_ifc_class, "relating_type": get_relating_type_id} - extrusion_depth: bpy.props.FloatProperty(default=42.0, subtype="DISTANCE") + extrusion_depth: bpy.props.FloatProperty(min=0.001, default=42.0, subtype="DISTANCE") cardinal_point: bpy.props.EnumProperty( items=( # TODO: complain to buildingSMART @@ -201,16 +201,18 @@ class BIMStairProperties(PropertyGroup): has_top_nib: bpy.props.BoolProperty(name="Has top nib", default=True) stair_type: bpy.props.EnumProperty(name="Stair type", items=stair_types, default="CONCRETE") - def get_props_kwargs(self, convert_to_project_units=False): + def get_props_kwargs(self, convert_to_project_units=False, stair_type=None): + if not stair_type: + stair_type = self.stair_type stair_kwargs = { - "stair_type": self.stair_type, + "stair_type": stair_type, "width": self.width, "height": self.height, "number_of_treads": self.number_of_treads, "tread_run": self.tread_run, } - if self.stair_type == "CONCRETE": + if stair_type == "CONCRETE": concrete_props = { "base_slab_depth": self.base_slab_depth, "top_slab_depth": self.top_slab_depth, @@ -219,13 +221,13 @@ class BIMStairProperties(PropertyGroup): } stair_kwargs.update(concrete_props) - elif self.stair_type == "WOOD/STEEL": + elif stair_type == "WOOD/STEEL": wood_steel_props = { "tread_depth": self.tread_depth, } stair_kwargs.update(wood_steel_props) - elif self.stair_type == "GENERIC": + elif stair_type == "GENERIC": pass if not convert_to_project_units: diff --git a/src/blenderbim/blenderbim/bim/module/model/railing.py b/src/blenderbim/blenderbim/bim/module/model/railing.py index 71c07a986b..245a963f61 100644 --- a/src/blenderbim/blenderbim/bim/module/model/railing.py +++ b/src/blenderbim/blenderbim/bim/module/model/railing.py @@ -120,7 +120,7 @@ def update_railing_modifier_bmesh(context): """before using should make sure that Data contains up-to-date information. If BBIM Pset just changed should call refresh() before updating bmesh """ - obj = context.object + obj = context.active_object props = obj.BIMRailingProperties # NOTE: using Data since bmesh update will hapen very often @@ -274,9 +274,9 @@ class BIM_OT_add_railing(bpy.types.Operator, tool.Ifc.Operator): self.report({"ERROR"}, "You need to start IFC project first to create a railing.") return {"CANCELLED"} - if context.object is not None: - spawn_location = context.object.location.copy() - context.object.select_set(False) + if context.active_object is not None: + spawn_location = context.active_object.location.copy() + context.active_object.select_set(False) else: spawn_location = bpy.context.scene.cursor.location.copy() @@ -348,8 +348,7 @@ class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object props = obj.BIMRailingProperties - element = tool.Ifc.get_entity(obj) - data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Railing", "Data")) + data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Railing")["data_dict"] data["path_data"] = json.dumps(data["path_data"]) # required since we could load pset from .ifc and BIMRailingProperties won't be set @@ -366,14 +365,13 @@ class CancelEditingRailing(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - element = tool.Ifc.get_entity(obj) - data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Railing", "Data")) + data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Railing")["data_dict"] props = obj.BIMRailingProperties # restore previous settings since editing was canceled props.set_props_kwargs_from_ifc_data(data) update_railing_modifier_bmesh(context) - + props.is_editing = False return {"FINISHED"} @@ -438,11 +436,14 @@ class EnableEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object props = obj.BIMRailingProperties + data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Railing")["data_dict"] + # required since we could load pset from .ifc and BIMRoofProperties won't be set + props.set_props_kwargs_from_ifc_data(data) props.is_editing_path = True update_railing_modifier_bmesh(context) - if bpy.context.object.mode != "EDIT": + if bpy.context.active_object.mode != "EDIT": bpy.ops.object.mode_set(mode="EDIT") bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool") ProfileDecorator.install(context, exit_edit_mode_callback=lambda: cancel_editing_railing_path(context)) @@ -456,9 +457,24 @@ def cancel_editing_railing_path(context): ProfileDecorator.uninstall() props.is_editing_path = False - update_railing_modifier_bmesh(context) - if bpy.context.object.mode == "EDIT": + if bpy.context.active_object.mode == "EDIT": bpy.ops.object.mode_set(mode="OBJECT") + + if props.railing_type == "FRAMELESS_PANEL": + update_railing_modifier_bmesh(context) + else: + element = tool.Ifc.get_entity(obj) + body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + blenderbim.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=body, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + ) + return {"FINISHED"} @@ -492,7 +508,7 @@ class FinishEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator): # since we know that BBIM_Railing could have changed refresh() update_railing_modifier_bmesh(context) - if bpy.context.object.mode == "EDIT": + if bpy.context.active_object.mode == "EDIT": bpy.ops.object.mode_set(mode="OBJECT") update_railing_modifier_ifc_data(context) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/roof.py b/src/blenderbim/blenderbim/bim/module/model/roof.py index f3d90c0c73..13d4b402c6 100644 --- a/src/blenderbim/blenderbim/bim/module/model/roof.py +++ b/src/blenderbim/blenderbim/bim/module/model/roof.py @@ -424,7 +424,7 @@ def update_roof_modifier_bmesh(context): """before using should make sure that Data contains up-to-date information. If BBIM Pset just changed should call refresh() before updating bmesh """ - obj = context.object + obj = context.active_object props = obj.BIMRoofProperties # NOTE: using Data since bmesh update will hapen very often @@ -506,9 +506,9 @@ class BIM_OT_add_roof(bpy.types.Operator, tool.Ifc.Operator): self.report({"ERROR"}, "You need to start IFC project first to create a roof.") return {"CANCELLED"} - if context.object is not None: - spawn_location = context.object.location.copy() - context.object.select_set(False) + if context.active_object is not None: + spawn_location = context.active_object.location.copy() + context.active_object.select_set(False) else: spawn_location = bpy.context.scene.cursor.location.copy() @@ -593,8 +593,7 @@ class EnableEditingRoof(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object props = obj.BIMRoofProperties - element = tool.Ifc.get_entity(obj) - data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Roof", "Data")) + data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Roof")["data_dict"] # required since we could load pset from .ifc and BIMRoofProperties won't be set props.set_props_kwargs_from_ifc_data(data) props.is_editing = True @@ -608,8 +607,7 @@ class CancelEditingRoof(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - element = tool.Ifc.get_entity(obj) - data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Roof", "Data")) + data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Roof")["data_dict"] props = obj.BIMRoofProperties # restore previous settings since editing was canceled @@ -650,11 +648,14 @@ class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object props = obj.BIMRoofProperties + data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Roof")["data_dict"] + # required since we could load pset from .ifc and BIMRoofProperties won't be set + props.set_props_kwargs_from_ifc_data(data) props.is_editing_path = True update_roof_modifier_bmesh(context) - if bpy.context.object.mode != "EDIT": + if bpy.context.active_object.mode != "EDIT": bpy.ops.object.mode_set(mode="EDIT") bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool") @@ -706,7 +707,7 @@ def cancel_editing_roof_path(context): props.is_editing_path = False update_roof_modifier_bmesh(context) - if bpy.context.object.mode == "EDIT": + if bpy.context.active_object.mode == "EDIT": bpy.ops.object.mode_set(mode="OBJECT") return {"FINISHED"} @@ -747,7 +748,7 @@ class FinishEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator): update_roof_modifier_bmesh(context) update_roof_modifier_ifc_data(context) - if bpy.context.object.mode == "EDIT": + if bpy.context.active_object.mode == "EDIT": bpy.ops.object.mode_set(mode="OBJECT") update_roof_modifier_ifc_data(context) return {"FINISHED"} @@ -789,7 +790,7 @@ class SetGableRoofEdgeAngle(bpy.types.Operator): # tried to avoid bmesh with foreach_get and foreach_set # but in EDIT mode it's only possible to change attributes by working with bmesh - me = context.object.data + me = context.active_object.data bm = tool.Blender.get_bmesh_for_mesh(me) # check if attribute exists or create one diff --git a/src/blenderbim/blenderbim/bim/module/model/stair.py b/src/blenderbim/blenderbim/bim/module/model/stair.py index 331173f381..dbf24d3ae7 100644 --- a/src/blenderbim/blenderbim/bim/module/model/stair.py +++ b/src/blenderbim/blenderbim/bim/module/model/stair.py @@ -215,7 +215,7 @@ def update_stair_modifier(context): props_kwargs = obj.BIMStairProperties.get_props_kwargs() vertices, edges, faces = generate_stair_2d_profile(**props_kwargs) - obj = context.object + obj = context.active_object bm = bmesh.new() bm.verts.index_update() bm.edges.index_update() @@ -234,7 +234,7 @@ def update_stair_modifier(context): translate_verts = [v for v in extruded["geom"] if isinstance(v, BMVert)] bmesh.ops.translate(bm, vec=extrusion_vector, verts=translate_verts) - if context.object.mode == "EDIT": + if context.active_object.mode == "EDIT": bmesh.update_edit_mesh(obj.data) else: bm.to_mesh(obj.data) @@ -316,9 +316,9 @@ class BIM_OT_add_clever_stair(bpy.types.Operator, tool.Ifc.Operator): self.report({"ERROR"}, "You need to start IFC project first to create a stair.") return {"CANCELLED"} - if context.object is not None: - spawn_location = context.object.location.copy() - context.object.select_set(False) + if context.active_object is not None: + spawn_location = context.active_object.location.copy() + context.active_object.select_set(False) else: spawn_location = bpy.context.scene.cursor.location.copy() diff --git a/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py b/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py index 4444870fe9..7c6428a4eb 100644 --- a/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py +++ b/src/blenderbim/blenderbim/bim/module/model/sverchok_modifier.py @@ -51,7 +51,7 @@ def update_sverchok_modifier(context): bm.edges.index_update() bm.faces.index_update() - if context.object.mode == "EDIT": + if context.active_object.mode == "EDIT": bmesh.update_edit_mesh(obj.data) else: bm.to_mesh(obj.data) diff --git a/src/blenderbim/blenderbim/bim/module/model/ui.py b/src/blenderbim/blenderbim/bim/module/model/ui.py index 12aa343c9b..75837229cb 100644 --- a/src/blenderbim/blenderbim/bim/module/model/ui.py +++ b/src/blenderbim/blenderbim/bim/module/model/ui.py @@ -41,7 +41,7 @@ from blenderbim.bim.helper import prop_with_search class LaunchTypeManager(bpy.types.Operator): bl_idname = "bim.launch_type_manager" bl_label = "Launch Type Manager" - bl_options = {"REGISTER"} + bl_options = {"REGISTER", "UNDO"} bl_description = "Display all available Construction Types to add new instances" def execute(self, context): @@ -127,6 +127,8 @@ class LaunchTypeManager(bpy.types.Operator): op.ifc_class = relating_type["ifc_class"] op.relating_type_id = relating_type["id"] + op = row.operator("bim.rename_type", icon="GREASEPENCIL", text="") + op.element = relating_type["id"] op = row.operator("bim.select_type", icon="OBJECT_DATA", text="") op.relating_type = relating_type["id"] op = row.operator("bim.duplicate_type", icon="DUPLICATE", text="") @@ -166,12 +168,13 @@ class BIM_PT_Grids(Panel): class BIM_PT_array(bpy.types.Panel): - bl_label = "IFC Array" + bl_label = "Array" bl_idname = "BIM_PT_array" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" - bl_context = "modifier" + bl_context = "scene" bl_options = {"DEFAULT_CLOSED"} + bl_parent_id = "BIM_PT_tab_parametric_geometry" @classmethod def poll(cls, context): @@ -234,12 +237,13 @@ class BIM_PT_array(bpy.types.Panel): class BIM_PT_stair(bpy.types.Panel): - bl_label = "IFC Stair" + bl_label = "Stair" bl_idname = "BIM_PT_stair" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" - bl_context = "modifier" + bl_context = "scene" bl_options = {"DEFAULT_CLOSED"} + bl_parent_id = "BIM_PT_tab_parametric_geometry" @classmethod def poll(cls, context): @@ -269,11 +273,9 @@ class BIM_PT_stair(bpy.types.Panel): row.operator("bim.enable_editing_stair", icon="GREASEPENCIL", text="") row.operator("bim.remove_stair", icon="X", text="") row = self.layout.row(align=True) - for prop in props.get_props_kwargs(): - prop_value = stair_data[prop] - prop_value = round(prop_value, 5) if type(prop_value) is float else prop_value + for prop_name, prop_value in StairData.data["general_params"].items(): row = self.layout.row(align=True) - row.label(text=f"{props.bl_rna.properties[prop].name}") + row.label(text=prop_name) row.label(text=str(prop_value)) # calculated properties @@ -294,12 +296,13 @@ class BIM_PT_stair(bpy.types.Panel): class BIM_PT_sverchok(bpy.types.Panel): - bl_label = "IFC Sverchok" + bl_label = "Sverchok" bl_idname = "BIM_PT_sverchok" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" - bl_context = "modifier" + bl_context = "scene" bl_options = {"DEFAULT_CLOSED"} + bl_parent_id = "BIM_PT_tab_parametric_geometry" @classmethod def poll(cls, context): @@ -332,12 +335,13 @@ class BIM_PT_sverchok(bpy.types.Panel): class BIM_PT_window(bpy.types.Panel): - bl_label = "IFC Window" + bl_label = "Window" bl_idname = "BIM_PT_window" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" - bl_context = "modifier" + bl_context = "scene" bl_options = {"DEFAULT_CLOSED"} + bl_parent_id = "BIM_PT_tab_parametric_geometry" @classmethod def poll(cls, context): @@ -441,12 +445,13 @@ class BIM_PT_window(bpy.types.Panel): class BIM_PT_door(bpy.types.Panel): - bl_label = "IFC Door" + bl_label = "Door" bl_idname = "BIM_PT_door" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" - bl_context = "modifier" + bl_context = "scene" bl_options = {"DEFAULT_CLOSED"} + bl_parent_id = "BIM_PT_tab_parametric_geometry" @classmethod def poll(cls, context): @@ -514,12 +519,13 @@ class BIM_PT_door(bpy.types.Panel): class BIM_PT_railing(bpy.types.Panel): - bl_label = "IFC Railing" + bl_label = "Railing" bl_idname = "BIM_PT_railing" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" - bl_context = "modifier" + bl_context = "scene" bl_options = {"DEFAULT_CLOSED"} + bl_parent_id = "BIM_PT_tab_parametric_geometry" @classmethod def poll(cls, context): @@ -576,12 +582,13 @@ class BIM_PT_railing(bpy.types.Panel): class BIM_PT_roof(bpy.types.Panel): - bl_label = "IFC Roof" + bl_label = "Roof" bl_idname = "BIM_PT_roof" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" - bl_context = "modifier" + bl_context = "scene" bl_options = {"DEFAULT_CLOSED"} + bl_parent_id = "BIM_PT_tab_parametric_geometry" @classmethod def poll(cls, context): @@ -631,7 +638,7 @@ class BIM_PT_roof(bpy.types.Panel): class BIM_MT_model(Menu): bl_idname = "BIM_MT_model" - bl_label = "IFC Objects" + bl_label = "Objects" def draw(self, context): layout = self.layout diff --git a/src/blenderbim/blenderbim/bim/module/model/wall.py b/src/blenderbim/blenderbim/bim/module/model/wall.py index aa94348596..7494146aee 100644 --- a/src/blenderbim/blenderbim/bim/module/model/wall.py +++ b/src/blenderbim/blenderbim/bim/module/model/wall.py @@ -60,11 +60,11 @@ class JoinWall(bpy.types.Operator, tool.Ifc.Operator): for obj in selected_objs: joiner.unjoin(obj) return {"FINISHED"} - + if not context.active_object or not context.active_object.BIMObjectProperties.ifc_definition_id: self.report({"ERROR"}, f"No active object selected") return {"CANCELLED"} - + for obj in selected_objs: tool.Geometry.clear_scale(obj) @@ -75,7 +75,7 @@ class JoinWall(bpy.types.Operator, tool.Ifc.Operator): if len(selected_objs) == 1: joiner.join_E(context.active_object, context.scene.cursor.location) return {"FINISHED"} - + if self.join_type in ("L", "V"): if len(selected_objs) != 2: self.report({"ERROR"}, f"It requires 2 selected objects to do join of type {self.join_type}") @@ -86,7 +86,7 @@ class JoinWall(bpy.types.Operator, tool.Ifc.Operator): elif self.join_type == "V": joiner.join_V(another_selected_object, context.active_object) return {"FINISHED"} - + if self.join_type == "T": elements = [tool.Ifc.get_entity(o) for o in context.selected_objects] layer2_elements = [] @@ -560,7 +560,7 @@ class DumbWallGenerator: rotation = math.atan2(normal[1], normal[0]) rotated_y_axis = Matrix.Rotation(-rotation, 4, "Z")[1].xyz - + # since wall thickness goes by local Y+ axis # we find best position for the next wall # by finding the face of another wall that will be very close to the some test point. @@ -1137,7 +1137,7 @@ class DumbWallJoiner: previous_matrix = obj.matrix_world.copy() previous_origin = previous_matrix.col[3].to_2d() - obj.matrix_world[0][3], obj.matrix_world[1][3] = self.body[0] + obj.matrix_world.col[3].xy = self.body[0] bpy.context.view_layer.update() for rel in element.ConnectedFrom: @@ -1432,6 +1432,7 @@ class DumbWallJoiner: return True def clip(self, wall1, slab2): + """returns height of the clipped wall, adds clipping plane to `clippings`""" element1 = tool.Ifc.get_entity(wall1) element2 = tool.Ifc.get_entity(slab2) @@ -1441,19 +1442,15 @@ class DumbWallJoiner: bases = [axis1["base"][0].to_3d(), axis1["base"][1].to_3d(), axis1["side"][0].to_3d(), axis1["side"][1].to_3d()] extrusion = self.get_extrusion_data(tool.Ifc.get().by_id(wall1.data.BIMMeshProperties.ifc_definition_id)) - d = wall1.matrix_world.to_quaternion() @ extrusion["direction"] + wall_dir = wall1.matrix_world.to_quaternion() @ extrusion["direction"] slab_pt = slab2.matrix_world @ Vector((0, 0, 0)) slab_dir = slab2.matrix_world.to_quaternion() @ Vector((0, 0, -1)) - tops = [mathutils.geometry.intersect_line_plane(b, b + d, slab_pt, slab_dir) for b in bases] - - i_bottom = None - i_top = None - for i, co in enumerate(tops): - if i_top is None or co[2] > i_top[2]: - i_top = co - i_bottom = bases[i] + tops = [mathutils.geometry.intersect_line_plane(b, b + wall_dir, slab_pt, slab_dir) for b in bases] + top_index = max(range(4), key=lambda i: tops[i].z) + i_top = tops[top_index] + i_bottom = bases[top_index] quaternion = slab2.matrix_world.to_quaternion() x_axis = quaternion @ Vector((1, 0, 0)) diff --git a/src/blenderbim/blenderbim/bim/module/model/window.py b/src/blenderbim/blenderbim/bim/module/model/window.py index d19763b445..ea5458da4d 100644 --- a/src/blenderbim/blenderbim/bim/module/model/window.py +++ b/src/blenderbim/blenderbim/bim/module/model/window.py @@ -292,7 +292,7 @@ def create_bm_window( def update_window_modifier_bmesh(context): - obj = context.object + obj = context.active_object props = obj.BIMWindowProperties panel_schema = DEFAULT_PANEL_SCHEMAS[props.window_type] accumulated_height = [0] * len(panel_schema[0]) @@ -425,7 +425,7 @@ def update_window_modifier_bmesh(context): bmesh.ops.translate(bm, vec=V(0, lining_offset, 0), verts=bm.verts) bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) - if bpy.context.object.mode == "EDIT": + if bpy.context.active_object.mode == "EDIT": bmesh.update_edit_mesh(obj.data) else: bm.to_mesh(obj.data) @@ -444,9 +444,9 @@ class BIM_OT_add_window(bpy.types.Operator, tool.Ifc.Operator): self.report({"ERROR"}, "You need to start IFC project first to create a window.") return {"CANCELLED"} - if context.object is not None: - spawn_location = context.object.location.copy() - context.object.select_set(False) + if context.active_object is not None: + spawn_location = context.active_object.location.copy() + context.active_object.select_set(False) else: spawn_location = bpy.context.scene.cursor.location.copy() diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 6fefce75b1..26f5631947 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -176,7 +176,11 @@ class BimToolUI: row.operator("bim.join_wall", icon="X", text="").join_type = "" elif AuthoringData.data["active_material_usage"] == "LAYER3": - add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "") + + if len(context.selected_objects) == 1: + add_layout_hotkey_operator(cls.layout, "Edit Profile", "S_E", "") + elif "LAYER2" in AuthoringData.data["selected_material_usages"]: + add_layout_hotkey_operator(cls.layout, "Extend Wall To Slab", "S_E", "") row = cls.layout.row(align=True) row.prop(data=cls.props, property="x_angle", text="X Angle") @@ -457,7 +461,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): if len(bpy.context.selected_objects) == 1: if self.active_material_usage == "LAYER3": - # Edit LAYER2 profile + # Edit LAYER3 profile if bpy.context.active_object and bpy.context.active_object.mode == "OBJECT": bpy.ops.bim.enable_editing_extrusion_profile() elif self.active_material_usage == "LAYER2": @@ -469,6 +473,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): else: # Edit SWEPTSOLID profile (assuming single profile for now) bpy.ops.bim.enable_editing_extrusion_profile() + elif self.active_material_usage == "LAYER2" and selected_usages.get("PROFILE", []): # Extend PROFILEs to LAYER2 [o.select_set(False) for o in selected_usages.get("LAYER3", [])] diff --git a/src/blenderbim/blenderbim/bim/module/owner/ui.py b/src/blenderbim/blenderbim/bim/module/owner/ui.py index a9a5404e4e..622808c6f0 100644 --- a/src/blenderbim/blenderbim/bim/module/owner/ui.py +++ b/src/blenderbim/blenderbim/bim/module/owner/ui.py @@ -77,7 +77,7 @@ def draw_addresses(box, parent): class BIM_PT_people(bpy.types.Panel): - bl_label = "IFC People" + bl_label = "People" bl_idname = "BIM_PT_people" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -136,7 +136,7 @@ class BIM_PT_people(bpy.types.Panel): class BIM_PT_organisations(bpy.types.Panel): - bl_label = "IFC Organisations" + bl_label = "Organisations" bl_idname = "BIM_PT_organisations" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -183,7 +183,7 @@ class BIM_PT_organisations(bpy.types.Panel): class BIM_PT_owner(bpy.types.Panel): - bl_label = "IFC Owner History" + bl_label = "Owner History" bl_idname = "BIM_PT_owner" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -234,7 +234,7 @@ class BIM_PT_owner(bpy.types.Panel): class BIM_PT_actor(bpy.types.Panel): - bl_label = "IFC Actor" + bl_label = "Actor" bl_idname = "BIM_PT_actor" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -284,14 +284,14 @@ class BIM_PT_actor(bpy.types.Panel): class BIM_PT_object_actor(bpy.types.Panel): - bl_label = "IFC Actor" + bl_label = "Actor" bl_idname = "BIM_PT_object_actor" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" bl_order = 1 - bl_parent_id = "BIM_PT_misc_object" + bl_parent_id = "BIM_PT_tab_misc" @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/patch/ui.py b/src/blenderbim/blenderbim/bim/module/patch/ui.py index 7b0416bd14..c5013c31ff 100644 --- a/src/blenderbim/blenderbim/bim/module/patch/ui.py +++ b/src/blenderbim/blenderbim/bim/module/patch/ui.py @@ -23,13 +23,13 @@ from blenderbim.bim.helper import draw_attributes class BIM_PT_patch(bpy.types.Panel): - bl_label = "IFC Patch" + bl_label = "Patch" bl_idname = "BIM_PT_patch" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_quality_control" + bl_parent_id = "BIM_PT_tab_quality_control" def draw(self, context): layout = self.layout diff --git a/src/blenderbim/blenderbim/bim/module/profile/ui.py b/src/blenderbim/blenderbim/bim/module/profile/ui.py index 4caf801b6f..5f4b998c98 100644 --- a/src/blenderbim/blenderbim/bim/module/profile/ui.py +++ b/src/blenderbim/blenderbim/bim/module/profile/ui.py @@ -25,7 +25,7 @@ from blenderbim.bim.module.profile.prop import generate_thumbnail_for_active_pro class BIM_PT_profiles(Panel): - bl_label = "IFC Profiles" + bl_label = "Profiles" bl_idname = "BIM_PT_profiles" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index 3cd930fe0c..369d8d5c97 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -582,34 +582,34 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): @persistent def load_handler(*args): bpy.app.handlers.load_post.remove(load_handler) - if tool.Blender.is_default_scene(): - for obj in bpy.data.objects: - bpy.data.objects.remove(obj) self.finish_loading_project(context) if self.should_start_fresh_session: - # WARNING: wm.read_homefile clears context - # which could lead to some operators to fail: + # WARNING: wm.read_homefile clears context which could lead to some + # operators to fail: # https://blender.stackexchange.com/a/282558/135166 - # So we continue using the load_post handler - # thats triggered when context is already restored + # So we continue using the load_post handler thats triggered when + # context is already restored bpy.app.handlers.load_post.append(load_handler) bpy.ops.wm.read_homefile() return {"FINISHED"} else: return self.finish_loading_project(context) - def finish_loading_project(self, context): if not self.is_existing_ifc_file(): return {"FINISHED"} + + if tool.Blender.is_default_scene(): + for obj in bpy.data.objects: + bpy.data.objects.remove(obj) + context.scene.BIMProperties.ifc_file = self.get_filepath() context.scene.BIMProjectProperties.is_loading = True context.scene.BIMProjectProperties.total_elements = len(tool.Ifc.get().by_type("IfcElement")) if not self.is_advanced: bpy.ops.bim.load_project_elements() - return {"FINISHED"} - + return {"FINISHED"} def invoke(self, context, event): context.window_manager.fileselect_add(self) @@ -987,7 +987,7 @@ class ExportIFC(bpy.types.Operator): if bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath: bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath) blenderbim.bim.handler.purge_module_data() - self.report({"INFO"}, f"IFC Project \"{os.path.basename(output_file)}\" Saved") + self.report({"INFO"}, f'IFC Project "{os.path.basename(output_file)}" Saved') if bpy.data.is_saved: bpy.ops.wm.save_mainfile("INVOKE_DEFAULT") diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index 882211e716..34fb692b07 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -41,6 +41,12 @@ class BIM_MT_new_project(Menu): bl_label = "New Project" def draw(self, context): + self.layout.operator_context = "INVOKE_DEFAULT" + op = self.layout.operator("bim.load_project", text="Open IFC Project", icon="FILEBROWSER") + op.should_start_fresh_session = True + # Do we need to set it back to exec default? + # self.layout.operator_context = "EXEC_DEFAULT" + self.layout.separator() self.layout.label(text="New IFC Project", icon_value=blenderbim.bim.icons["IFC"].icon_id) self.layout.operator("bim.new_project", text="Metric (m) Project").preset = "metric_m" self.layout.operator("bim.new_project", text="Metric (mm) Project").preset = "metric_mm" @@ -70,7 +76,7 @@ def file_menu(self, context): class BIM_PT_project(Panel): - bl_label = "IFC Project" + bl_label = "Current Project" bl_idname = "BIM_PT_project" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -209,7 +215,6 @@ class BIM_PT_project(Panel): row = self.layout.row() row.label(text="File Not Saved", icon="ERROR") - def draw_create_project_ui(self, context): props = context.scene.BIMProperties pprops = context.scene.BIMProjectProperties @@ -226,11 +231,11 @@ class BIM_PT_project(Panel): row = self.layout.row(align=True) row.operator("bim.create_project") - row.operator("bim.load_project").should_start_fresh_session = True + row.operator("bim.load_project").should_start_fresh_session = False class BIM_PT_project_library(Panel): - bl_label = "IFC Project Library" + bl_label = "Project Library" bl_idname = "BIM_PT_project_library" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -272,7 +277,7 @@ class BIM_PT_project_library(Panel): class BIM_PT_links(Panel): - bl_label = "IFC Links" + bl_label = "Links" bl_idname = "BIM_PT_links" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" diff --git a/src/blenderbim/blenderbim/bim/module/pset/ui.py b/src/blenderbim/blenderbim/bim/module/pset/ui.py index 6eb1ab8419..56ff428e00 100644 --- a/src/blenderbim/blenderbim/bim/module/pset/ui.py +++ b/src/blenderbim/blenderbim/bim/module/pset/ui.py @@ -170,12 +170,12 @@ def draw_psetqto_editable_ui(box, props, prop): class BIM_PT_object_psets(Panel): - bl_label = "IFC Object Property Sets" + bl_label = "Object Property Sets" bl_idname = "BIM_PT_object_psets" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" - bl_parent_id = "BIM_PT_object_metadata" + bl_parent_id = "BIM_PT_tab_object_metadata" @classmethod def poll(cls, context): @@ -212,12 +212,12 @@ class BIM_PT_object_psets(Panel): class BIM_PT_object_qtos(Panel): - bl_label = "IFC Object Quantity Sets" + bl_label = "Object Quantity Sets" bl_idname = "BIM_PT_object_qtos" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" - bl_parent_id = "BIM_PT_object_metadata" + bl_parent_id = "BIM_PT_tab_object_metadata" @classmethod def poll(cls, context): @@ -249,7 +249,7 @@ class BIM_PT_object_qtos(Panel): class BIM_PT_material_psets(Panel): - bl_label = "IFC Material Property Sets" + bl_label = "Material Property Sets" bl_idname = "BIM_PT_material_psets" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -293,7 +293,7 @@ class BIM_PT_material_psets(Panel): class BIM_PT_material_set_psets(Panel): - bl_label = "IFC Material Set Property Sets" + bl_label = "Material Set Property Sets" bl_idname = "BIM_PT_material_set_psets" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -330,7 +330,7 @@ class BIM_PT_material_set_psets(Panel): class BIM_PT_material_set_item_psets(Panel): - bl_label = "IFC Material Set Item Property Sets" + bl_label = "Material Set Item Property Sets" bl_idname = "BIM_PT_material_set_item_psets" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -367,7 +367,7 @@ class BIM_PT_material_set_item_psets(Panel): class BIM_PT_task_qtos(Panel): - bl_label = "IFC Task Quantity Sets" + bl_label = "Task Quantity Sets" bl_idname = "BIM_PT_task_qtos" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -403,7 +403,7 @@ class BIM_PT_task_qtos(Panel): class BIM_PT_resource_qtos(Panel): - bl_label = "IFC Resource Quantity Sets" + bl_label = "Resource Quantity Sets" bl_idname = "BIM_PT_resource_qtos" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -437,7 +437,7 @@ class BIM_PT_resource_qtos(Panel): class BIM_PT_resource_psets(Panel): - bl_label = "IFC Resource Property Sets" + bl_label = "Resource Property Sets" bl_idname = "BIM_PT_resource_psets" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -471,7 +471,7 @@ class BIM_PT_resource_psets(Panel): class BIM_PT_profile_psets(Panel): - bl_label = "IFC Profile Property Sets" + bl_label = "Profile Property Sets" bl_idname = "BIM_PT_profile_psets" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -507,7 +507,7 @@ class BIM_PT_profile_psets(Panel): class BIM_PT_work_schedule_psets(Panel): - bl_label = "IFC Work Schedule Property Sets" + bl_label = "Work Schedule Property Sets" bl_idname = "BIM_PT_work_schedule_psets" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -539,12 +539,12 @@ class BIM_PT_work_schedule_psets(Panel): class BIM_PT_bulk_property_editor(Panel): - bl_label = "IFC Bulk Property Editor" + bl_label = "Bulk Property Editor" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" bl_options = {"DEFAULT_CLOSED"} - bl_parent_id = "BIM_PT_utilities_object" + bl_parent_id = "BIM_PT_tab_misc" def draw(self, context): pass diff --git a/src/blenderbim/blenderbim/bim/module/pset_template/ui.py b/src/blenderbim/blenderbim/bim/module/pset_template/ui.py index 324aee459d..969a355ed3 100644 --- a/src/blenderbim/blenderbim/bim/module/pset_template/ui.py +++ b/src/blenderbim/blenderbim/bim/module/pset_template/ui.py @@ -23,7 +23,7 @@ from blenderbim.bim.module.pset_template.data import PsetTemplatesData class BIM_PT_pset_template(Panel): - bl_label = "IFC Property Set Templates" + bl_label = "Property Set Templates" bl_idname = "BIM_PT_pset_template" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" diff --git a/src/blenderbim/blenderbim/bim/module/resource/ui.py b/src/blenderbim/blenderbim/bim/module/resource/ui.py index 433c0e73e1..4ce36e12bf 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/ui.py +++ b/src/blenderbim/blenderbim/bim/module/resource/ui.py @@ -23,13 +23,13 @@ from blenderbim.bim.module.resource.data import ResourceData class BIM_PT_resources(Panel): - bl_label = "IFC Resources" + bl_label = "Resources" bl_idname = "BIM_PT_resources" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_4D5D" + bl_parent_id = "BIM_PT_tab_4D5D" @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/root/data.py b/src/blenderbim/blenderbim/bim/module/root/data.py index d77664249b..4bd5cecab7 100644 --- a/src/blenderbim/blenderbim/bim/module/root/data.py +++ b/src/blenderbim/blenderbim/bim/module/root/data.py @@ -21,6 +21,7 @@ import bpy import ifcopenshell.util.element from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc import blenderbim.tool as tool +import blenderbim.bim.helper from blenderbim.bim.ifc import IfcStore @@ -70,7 +71,7 @@ class IfcClassData: "IfcAnnotation", "IfcRelSpaceBoundary", ] - return [(e, e, (get_entity_doc(version, e) or {}).get("description", "")) for e in products] + return [(e, blenderbim.bim.helper.uncamel(e), (get_entity_doc(version, e) or {}).get("description", "")) for e in products] @classmethod def ifc_classes(cls): @@ -86,7 +87,7 @@ class IfcClassData: # Yeah, weird isn't it. names.remove("IfcOpeningStandardCase") version = tool.Ifc.get_schema() - return [(c, c, (get_entity_doc(version, c) or {}).get("description", "")) for c in sorted(names)] + return [(c, blenderbim.bim.helper.uncamel(c), (get_entity_doc(version, c) or {}).get("description", "")) for c in sorted(names)] @classmethod def ifc_predefined_types(cls): @@ -163,9 +164,10 @@ class IfcClassData: if not element: return name = element.is_a() + name = blenderbim.bim.helper.uncamel(name) predefined_type = ifcopenshell.util.element.get_predefined_type(element) if predefined_type: - name += f"[{predefined_type}]" + name += f" [{predefined_type}]" return name @classmethod diff --git a/src/blenderbim/blenderbim/bim/module/root/ui.py b/src/blenderbim/blenderbim/bim/module/root/ui.py index 2004f7ed08..76f5546594 100644 --- a/src/blenderbim/blenderbim/bim/module/root/ui.py +++ b/src/blenderbim/blenderbim/bim/module/root/ui.py @@ -25,12 +25,13 @@ from blenderbim.bim.module.root.data import IfcClassData class BIM_PT_class(Panel): - bl_label = "IFC Class" + bl_label = "Class" bl_idname = "BIM_PT_class" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" - bl_parent_id = "BIM_PT_object_metadata" + bl_parent_id = "BIM_PT_tab_object_metadata" + bl_options = {"HIDE_HEADER"} @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/search/ui.py b/src/blenderbim/blenderbim/bim/module/search/ui.py index b84c821cfa..fc9b016012 100644 --- a/src/blenderbim/blenderbim/bim/module/search/ui.py +++ b/src/blenderbim/blenderbim/bim/module/search/ui.py @@ -22,7 +22,7 @@ from blenderbim.bim.ifc import IfcStore class BIM_PT_search(Panel): - bl_label = "IFC Search" + bl_label = "Search" bl_idname = "BIM_PT_search" # bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -109,7 +109,7 @@ class BIM_UL_ifc_building_storey_filter(bpy.types.UIList): class BIM_PT_IFCSelector(Panel): - bl_label = "IFC Selector" + bl_label = "Selector" bl_idname = "BIM_PT_ifc_selector" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" diff --git a/src/blenderbim/blenderbim/bim/module/sequence/ui.py b/src/blenderbim/blenderbim/bim/module/sequence/ui.py index 79e7191b6a..eb0ccc477d 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/ui.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/ui.py @@ -25,13 +25,13 @@ from blenderbim.bim.module.sequence.data import WorkPlansData, WorkScheduleData, class BIM_PT_work_plans(Panel): - bl_label = "IFC Work Plans" + bl_label = "Work Plans" bl_idname = "BIM_PT_work_plans" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_4D5D" + bl_parent_id = "BIM_PT_tab_4D5D" @classmethod def poll(cls, context): @@ -97,13 +97,13 @@ class BIM_PT_work_plans(Panel): class BIM_PT_work_schedules(Panel): - bl_label = "IFC Work Schedules" + bl_label = "Work Schedules" bl_idname = "BIM_PT_work_schedules" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_4D5D" + bl_parent_id = "BIM_PT_tab_4D5D" @classmethod def poll(cls, context): @@ -526,7 +526,7 @@ class BIM_PT_work_schedules(Panel): class BIM_PT_task_icom(Panel): - bl_label = "IFC Task ICOM" + bl_label = "Task ICOM" bl_idname = "BIM_PT_task_icom" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -811,13 +811,13 @@ class BIM_UL_tasks(UIList): class BIM_PT_work_calendars(Panel): - bl_label = "IFC Work Calendars" + bl_label = "Work Calendars" bl_idname = "BIM_PT_work_calendars" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_4D5D" + bl_parent_id = "BIM_PT_tab_4D5D" @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/spatial/data.py b/src/blenderbim/blenderbim/bim/module/spatial/data.py index 9623045536..930a19b84a 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/data.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/data.py @@ -43,7 +43,11 @@ class SpatialData: @classmethod def containers(cls): results = {} - for container in tool.Ifc.get().by_type("IfcSpatialElement"): + if tool.Ifc.get_schema() == "IFC2X3": + spatial_elements = tool.Ifc.get().by_type("IfcSpatialStructureElement") + else: + spatial_elements = tool.Ifc.get().by_type("IfcSpatialElement") + for container in spatial_elements: results[container.id()] = { "type": container.is_a(), "id": container.id(), diff --git a/src/blenderbim/blenderbim/bim/module/spatial/ui.py b/src/blenderbim/blenderbim/bim/module/spatial/ui.py index e6d0c62432..8f73b18a4f 100644 --- a/src/blenderbim/blenderbim/bim/module/spatial/ui.py +++ b/src/blenderbim/blenderbim/bim/module/spatial/ui.py @@ -23,12 +23,12 @@ import blenderbim.tool as tool class BIM_PT_spatial(Panel): - bl_label = "IFC Spatial Container" + bl_label = "Spatial Container" bl_idname = "BIM_PT_spatial" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" - bl_parent_id = "BIM_PT_object_metadata" + bl_parent_id = "BIM_PT_tab_object_metadata" @classmethod def poll(cls, context): @@ -72,7 +72,7 @@ class BIM_PT_spatial(Panel): if SpatialData.data["is_directly_contained"]: row.operator("bim.remove_container", icon="X", text="") else: - row.label(text="This object is not spatially contained") + row.label(text="No Spatial Container") row.operator("bim.enable_editing_container", icon="GREASEPENCIL", text="") for reference in SpatialData.data["references"]: row = self.layout.row() @@ -97,7 +97,7 @@ class BIM_UL_containers(UIList): class BIM_PT_SpatialManager(Panel): - bl_label = "IFC Spatial Manager" + bl_label = "Spatial Manager" bl_idname = "BIM_PT_SpatialManager" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" diff --git a/src/blenderbim/blenderbim/bim/module/structural/ui.py b/src/blenderbim/blenderbim/bim/module/structural/ui.py index bee2b9f25e..b828c124f4 100644 --- a/src/blenderbim/blenderbim/bim/module/structural/ui.py +++ b/src/blenderbim/blenderbim/bim/module/structural/ui.py @@ -86,14 +86,14 @@ def draw_boundary_condition_read_only_ui(layout, boundary_condition): class BIM_PT_structural_boundary_conditions(Panel): - bl_label = "IFC Structural Boundary Conditions" + bl_label = "Structural Boundary Conditions" bl_idname = "BIM_PT_structural_boundary_conditions" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" bl_order = 1 - bl_parent_id = "BIM_PT_misc_object" + bl_parent_id = "BIM_PT_tab_misc" @classmethod def poll(cls, context): @@ -121,14 +121,14 @@ class BIM_PT_structural_boundary_conditions(Panel): class BIM_PT_connected_structural_members(Panel): - bl_label = "IFC Connected Structural Members" + bl_label = "Connected Structural Members" bl_idname = "BIM_PT_connected_structural_members" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" bl_order = 1 - bl_parent_id = "BIM_PT_misc_object" + bl_parent_id = "BIM_PT_tab_misc" @classmethod def poll(cls, context): @@ -174,14 +174,14 @@ class BIM_PT_connected_structural_members(Panel): class BIM_PT_structural_member(Panel): - bl_label = "IFC Structural Member" + bl_label = "Structural Member" bl_idname = "BIM_PT_structural_member" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" bl_order = 1 - bl_parent_id = "BIM_PT_misc_object" + bl_parent_id = "BIM_PT_tab_misc" @classmethod def poll(cls, context): @@ -217,14 +217,14 @@ class BIM_PT_structural_member(Panel): class BIM_PT_structural_connection(Panel): - bl_label = "IFC Structural Connection" + bl_label = "Structural Connection" bl_idname = "BIM_PT_structural_connection" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" bl_order = 1 - bl_parent_id = "BIM_PT_misc_object" + bl_parent_id = "BIM_PT_tab_misc" @classmethod def poll(cls, context): @@ -277,13 +277,13 @@ class BIM_PT_structural_connection(Panel): class BIM_PT_structural_analysis_models(Panel): - bl_label = "IFC Structural Analysis Models" + bl_label = "Structural Analysis Models" bl_idname = "BIM_PT_structural_analysis_models" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_structural" + bl_parent_id = "BIM_PT_tab_structural" @classmethod def poll(cls, context): @@ -352,13 +352,13 @@ class BIM_UL_structural_analysis_models(UIList): class BIM_PT_structural_load_cases(Panel): - bl_label = "IFC Structural Load Cases" + bl_label = "Structural Load Cases" bl_idname = "BIM_PT_structural_load_cases" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_structural" + bl_parent_id = "BIM_PT_tab_structural" @classmethod def poll(cls, context): @@ -448,13 +448,13 @@ class BIM_UL_structural_activities(UIList): class BIM_PT_structural_loads(Panel): - bl_label = "IFC Structural Loads" + bl_label = "Structural Loads" bl_idname = "BIM_PT_structural_loads" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_structural" + bl_parent_id = "BIM_PT_tab_structural" @classmethod def poll(cls, context): @@ -518,13 +518,13 @@ class BIM_UL_structural_loads(UIList): class BIM_PT_boundary_conditions(Panel): - bl_label = "IFC Boundary Conditions" + bl_label = "Boundary Conditions" bl_idname = "BIM_PT_boundary_conditions" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_structural" + bl_parent_id = "BIM_PT_tab_structural" @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/style/ui.py b/src/blenderbim/blenderbim/bim/module/style/ui.py index 3ebbe20810..bb266d9542 100644 --- a/src/blenderbim/blenderbim/bim/module/style/ui.py +++ b/src/blenderbim/blenderbim/bim/module/style/ui.py @@ -26,7 +26,7 @@ from blenderbim.tool.style import TEXTURE_MAPS_BY_METHODS, STYLE_TEXTURE_PROPS_M class BIM_PT_styles(Panel): - bl_label = "IFC Styles" + bl_label = "Styles" bl_idname = "BIM_PT_styles" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" @@ -86,7 +86,7 @@ def draw_style_ui(self, context): class BIM_PT_style(MaterialButtonsPanel, Panel): - bl_label = "IFC Style" + bl_label = "Style" bl_idname = "BIM_PT_style" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -111,7 +111,7 @@ class BIM_PT_style(MaterialButtonsPanel, Panel): class BIM_PT_style_attributes(Panel): - bl_label = "IFC Style Attributes" + bl_label = "Style Attributes" bl_idname = "BIM_PT_style_attributes" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -159,7 +159,7 @@ class BIM_PT_style_attributes(Panel): class BIM_PT_external_style_attributes(Panel): - bl_label = "IFC External Surface Style" + bl_label = "External Surface Style" bl_idname = "BIM_PT_external_style_attributes" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -226,7 +226,7 @@ class BIM_UL_styles(UIList): class BIM_PT_STYLE_GRAPH(Panel): bl_idname = "BIM_PT_style_graph" bl_space_type = "NODE_EDITOR" - bl_label = "IFC Style Graph Settings" + bl_label = "Style Graph Settings" bl_region_type = "UI" bl_category = "BBIM" diff --git a/src/blenderbim/blenderbim/bim/module/system/ui.py b/src/blenderbim/blenderbim/bim/module/system/ui.py index 5db712324f..9aea14b027 100644 --- a/src/blenderbim/blenderbim/bim/module/system/ui.py +++ b/src/blenderbim/blenderbim/bim/module/system/ui.py @@ -24,13 +24,13 @@ from blenderbim.bim.module.system.data import SystemData, ObjectSystemData, Port class BIM_PT_systems(Panel): - bl_label = "IFC Systems" + bl_label = "Systems" bl_idname = "BIM_PT_systems" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_services" + bl_parent_id = "BIM_PT_tab_services" @classmethod def poll(cls, context): @@ -74,14 +74,14 @@ class BIM_PT_systems(Panel): class BIM_PT_object_systems(Panel): - bl_label = "IFC Systems" + bl_label = "Systems" bl_idname = "BIM_PT_object_systems" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" bl_order = 1 - bl_parent_id = "BIM_PT_services_object" + bl_parent_id = "BIM_PT_tab_services_object" @classmethod def poll(cls, context): @@ -130,14 +130,14 @@ class BIM_PT_object_systems(Panel): class BIM_PT_ports(Panel): - bl_label = "IFC Ports" + bl_label = "Ports" bl_idname = "BIM_PT_ports" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" bl_order = 1 - bl_parent_id = "BIM_PT_services_object" + bl_parent_id = "BIM_PT_tab_services_object" @classmethod def poll(cls, context): @@ -163,14 +163,14 @@ class BIM_PT_ports(Panel): class BIM_PT_port(Panel): - bl_label = "IFC Port" + bl_label = "Port" bl_idname = "BIM_PT_port" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" bl_order = 1 - bl_parent_id = "BIM_PT_services_object" + bl_parent_id = "BIM_PT_tab_services_object" @classmethod def poll(cls, context): diff --git a/src/blenderbim/blenderbim/bim/module/tester/ui.py b/src/blenderbim/blenderbim/bim/module/tester/ui.py index 1708774393..feaffa30aa 100644 --- a/src/blenderbim/blenderbim/bim/module/tester/ui.py +++ b/src/blenderbim/blenderbim/bim/module/tester/ui.py @@ -28,7 +28,7 @@ class BIM_PT_tester(Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_parent_id = "BIM_PT_quality_control" + bl_parent_id = "BIM_PT_tab_quality_control" def draw(self, context): self.layout.use_property_split = True diff --git a/src/blenderbim/blenderbim/bim/module/type/__init__.py b/src/blenderbim/blenderbim/bim/module/type/__init__.py index 50accd37ea..d52d470062 100644 --- a/src/blenderbim/blenderbim/bim/module/type/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/type/__init__.py @@ -27,6 +27,7 @@ classes = ( operator.EnableEditingType, operator.PurgeUnusedTypes, operator.RemoveType, + operator.RenameType, operator.SelectSimilarType, operator.SelectType, operator.SelectTypeObjects, diff --git a/src/blenderbim/blenderbim/bim/module/type/data.py b/src/blenderbim/blenderbim/bim/module/type/data.py index 1d23c3363f..27a9ef5c13 100644 --- a/src/blenderbim/blenderbim/bim/module/type/data.py +++ b/src/blenderbim/blenderbim/bim/module/type/data.py @@ -56,6 +56,8 @@ class TypeData: return [] version = tool.Ifc.get_schema() types = ifcopenshell.util.type.get_applicable_types(element.is_a(), schema=version) + if element.is_a("IfcAnnotation"): + types.append("IfcTypeProduct") results.extend((t, t, get_entity_doc(version, t).get("description", "")) for t in types) return results diff --git a/src/blenderbim/blenderbim/bim/module/type/operator.py b/src/blenderbim/blenderbim/bim/module/type/operator.py index fda53d560a..4a0e9e06cd 100644 --- a/src/blenderbim/blenderbim/bim/module/type/operator.py +++ b/src/blenderbim/blenderbim/bim/module/type/operator.py @@ -436,7 +436,7 @@ class AddType(bpy.types.Operator, tool.Ifc.Operator): class RemoveType(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_type" bl_label = "Remove Type" - bl_options = {"REGISTER"} + bl_options = {"REGISTER", "UNDO"} element: bpy.props.IntProperty() def _execute(self, context): @@ -446,13 +446,35 @@ class RemoveType(bpy.types.Operator, tool.Ifc.Operator): if obj: tool.Ifc.unlink(obj=obj) bpy.data.objects.remove(obj) - return {"FINISHED"} + + +class RenameType(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.rename_type" + bl_label = "Rename Type" + bl_options = {"REGISTER", "UNDO"} + element: bpy.props.IntProperty() + name: bpy.props.StringProperty(name="Name") + + def _execute(self, context): + element = tool.Ifc.get().by_id(self.element) + obj = tool.Ifc.get_object(element) + element.Name = self.name + if obj: + tool.Root.set_object_name(obj, element) + + def invoke(self, context, event): + element = tool.Ifc.get().by_id(self.element) + self.name = element.Name or "Unnamed" + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + self.layout.prop(self, "name") class DuplicateType(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.duplicate_type" bl_label = "Duplicate Type" - bl_options = {"REGISTER"} + bl_options = {"REGISTER", "UNDO"} element: bpy.props.IntProperty() def _execute(self, context): @@ -478,7 +500,7 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator): class PurgeUnusedTypes(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.purge_unused_types" bl_label = "Purge Unused Types" - bl_options = {"REGISTER"} + bl_options = {"REGISTER", "UNDO"} def _execute(self, context): core.purge_unused_types(tool.Ifc, tool.Type) diff --git a/src/blenderbim/blenderbim/bim/module/type/ui.py b/src/blenderbim/blenderbim/bim/module/type/ui.py index d7feaf34bb..7be2c50897 100644 --- a/src/blenderbim/blenderbim/bim/module/type/ui.py +++ b/src/blenderbim/blenderbim/bim/module/type/ui.py @@ -25,12 +25,12 @@ from blenderbim.bim.module.type.data import TypeData class BIM_PT_type(Panel): - bl_label = "IFC Type" + bl_label = "Type" bl_idname = "BIM_PT_type" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" - bl_parent_id = "BIM_PT_object_metadata" + bl_parent_id = "BIM_PT_tab_object_metadata" @classmethod def poll(cls, context): @@ -92,7 +92,7 @@ class BIM_PT_type(Panel): row.operator("bim.enable_editing_type", icon="GREASEPENCIL", text="") row.operator("bim.unassign_type", icon="X", text="") else: - row.label(text="This object has no type") + row.label(text="No Relating Type") row.operator("bim.enable_editing_type", icon="GREASEPENCIL", text="") diff --git a/src/blenderbim/blenderbim/bim/module/unit/ui.py b/src/blenderbim/blenderbim/bim/module/unit/ui.py index 10a3682296..19d45eb825 100644 --- a/src/blenderbim/blenderbim/bim/module/unit/ui.py +++ b/src/blenderbim/blenderbim/bim/module/unit/ui.py @@ -24,7 +24,7 @@ from blenderbim.bim.module.unit.data import UnitsData class BIM_PT_units(Panel): - bl_label = "IFC Units" + bl_label = "Units" bl_idname = "BIM_PT_units" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" diff --git a/src/blenderbim/blenderbim/bim/module/void/ui.py b/src/blenderbim/blenderbim/bim/module/void/ui.py index fd1020fac9..4197189a61 100644 --- a/src/blenderbim/blenderbim/bim/module/void/ui.py +++ b/src/blenderbim/blenderbim/bim/module/void/ui.py @@ -23,14 +23,14 @@ from blenderbim.bim.module.void.data import BooleansData, VoidsData class BIM_PT_voids(Panel): - bl_label = "IFC Voids" + bl_label = "Voids" bl_idname = "BIM_PT_voids" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" bl_order = 1 - bl_parent_id = "BIM_PT_geometry_object" + bl_parent_id = "BIM_PT_tab_geometric_relationships" @classmethod def poll(cls, context): @@ -79,7 +79,7 @@ class BIM_PT_voids(Panel): class BIM_PT_booleans(Panel): - bl_label = "IFC Booleans" + bl_label = "Booleans" bl_idname = "BIM_PT_booleans" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -109,8 +109,10 @@ class BIM_PT_booleans(Panel): if context.active_object.data.BIMMeshProperties.ifc_definition_id: row = layout.row(align=True) row.label(text=f"{BooleansData.data['total_booleans']} Booleans Found") - row.operator("bim.add_boolean", text="", icon="ADD") - row.operator("bim.show_booleans", text="", icon="HIDE_OFF") + row.operator("bim.add_boolean", text="Apply Boolean", icon="ADD") + show_boolean_button = row.row(align=True) + show_boolean_button.operator("bim.show_booleans", text="", icon="HIDE_OFF") + show_boolean_button.enabled = BooleansData.data['total_booleans'] > 0 row.operator("bim.hide_booleans", text="", icon="HIDE_ON") elif context.active_object.data.BIMMeshProperties.ifc_boolean_id: row = layout.row() diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index 75a8875c5e..5e2fbe31ac 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -40,6 +40,21 @@ from mathutils import Vector, Matrix, Euler from math import radians +class SetTab(bpy.types.Operator): + bl_idname = "bim.set_tab" + bl_label = "Set Tab" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Sets the current property tab" + tab: bpy.props.StringProperty() + + def execute(self, context): + if context.area.spaces.active.search_filter: + return {"FINISHED"} + aprops = context.screen.BIMAreaProperties[context.screen.areas[:].index(context.area)] + aprops.tab = self.tab + return {"FINISHED"} + + class SwitchTab(bpy.types.Operator): bl_idname = "bim.switch_tab" bl_label = "Switch Tab" diff --git a/src/blenderbim/blenderbim/bim/prop.py b/src/blenderbim/blenderbim/bim/prop.py index 9ca579413b..cf8c806b38 100644 --- a/src/blenderbim/blenderbim/bim/prop.py +++ b/src/blenderbim/blenderbim/bim/prop.py @@ -345,13 +345,13 @@ def get_tab(self, context): return [ ("PROJECT", "Project Overview", "", blenderbim.bim.icons["IFC"].icon_id, 0), ("OBJECT", "Object Information", "", "FILE_3D", 1), - ("MATERIALS", "Materials and Styles", "", "MATERIAL", 2), + ("GEOMETRY", "Geometry and Materials", "", "MATERIAL", 2), ("DRAWINGS", "Drawings and Documents", "", "DOCUMENTS", 3), ("SERVICES", "Services and Systems", "", "NETWORK_DRIVE", 4), ("STRUCTURE", "Structural Analysis", "", "EDITMODE_HLT", 5), ("SCHEDULING", "Construction Scheduling", "", "NLA", 6), ("FM", "Facility Management", "", "PACKAGE", 7), - ("OTHER", "Other Utilities", "", "COLLAPSEMENU", 8), + ("QUALITY", "Quality and Coordination", "", "COMMUNITY", 8), ("BLENDER", "Blender Properties", "", "BLENDER", 9), ] @@ -359,7 +359,9 @@ def get_tab(self, context): class BIMAreaProperties(PropertyGroup): tab: EnumProperty(default=0, items=get_tab, name="Tab", update=update_tab) previous_tab: StringProperty(default="PROJECT", name="Previous Tab") - alt_tab: StringProperty(default="PROJECT", name="Alt Tab") + alt_tab: StringProperty(default="OBJECT", name="Alt Tab") + active_tab: BoolProperty(default=True, name="Active Tab") + inactive_tab: BoolProperty(default=False, name="Inactive Tab") class BIMProperties(PropertyGroup): diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index 3b6f3f2b45..f320450ed5 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -270,7 +270,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): # Scene panel groups -class BIM_PT_root(Panel): +class BIM_PT_tabs(Panel): bl_label = "BlenderBIM Add-on" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -281,15 +281,52 @@ class BIM_PT_root(Panel): def draw(self, context): try: aprops = context.screen.BIMAreaProperties[context.screen.areas[:].index(context.area)] + + row = self.layout.row() + row.operator( + "bim.set_tab", text="", emboss=False, icon_value=blenderbim.bim.icons["IFC"].icon_id + ).tab = "PROJECT" + row.operator("bim.set_tab", text="", emboss=False, icon="FILE_3D").tab = "OBJECT" + row.operator("bim.set_tab", text="", emboss=False, icon="MATERIAL").tab = "GEOMETRY" + row.operator("bim.set_tab", text="", emboss=False, icon="DOCUMENTS").tab = "DRAWINGS" + row.operator("bim.set_tab", text="", emboss=False, icon="NETWORK_DRIVE").tab = "SERVICES" + row.operator("bim.set_tab", text="", emboss=False, icon="EDITMODE_HLT").tab = "STRUCTURE" + row.operator("bim.set_tab", text="", emboss=False, icon="NLA").tab = "SCHEDULING" + row.operator("bim.set_tab", text="", emboss=False, icon="PACKAGE").tab = "FM" + row.operator("bim.set_tab", text="", emboss=False, icon="COMMUNITY").tab = "QUALITY" + row.operator("bim.set_tab", text="", emboss=False, icon="BLENDER").tab = "BLENDER" + row.operator("bim.switch_tab", text="", emboss=False, icon="UV_SYNC_SELECT") + + # Yes, that's right. + row = self.layout.row() + row.scale_y = 0.2 + for tab in [ + "PROJECT", + "OBJECT", + "GEOMETRY", + "DRAWINGS", + "SERVICES", + "STRUCTURE", + "SCHEDULING", + "FM", + "QUALITY", + "BLENDER", + "SWITCH", + ]: + if aprops.tab == tab: + row.prop(aprops, "active_tab", text="", icon="BLANK1") + else: + row.prop(aprops, "inactive_tab", text="", icon="BLANK1", emboss=False) + + aprops = context.screen.BIMAreaProperties[context.screen.areas[:].index(context.area)] row = self.layout.row(align=True) row.prop(aprops, "tab", text="") - row.operator("bim.switch_tab", text="", icon="UV_SYNC_SELECT") except: pass # Prior to load_post, we may not have any area properties setup class BIM_PT_project_info(Panel): - bl_label = "IFC Project Info" + bl_label = "Project Info" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" @@ -303,7 +340,7 @@ class BIM_PT_project_info(Panel): class BIM_PT_project_setup(Panel): - bl_label = "IFC Project Setup" + bl_label = "Project Setup" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" @@ -317,23 +354,22 @@ class BIM_PT_project_setup(Panel): pass -class BIM_PT_collaboration(Panel): - bl_label = "IFC Collaboration" +class BIM_PT_tab_collaboration(Panel): + bl_label = "Collaboration" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "OTHER") + return tool.Blender.is_tab(context, "QUALITY") def draw(self, context): pass class BIM_PT_selection(Panel): - bl_label = "IFC Selection" + bl_label = "Selection" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" @@ -348,11 +384,10 @@ class BIM_PT_selection(Panel): class BIM_PT_geometry(Panel): - bl_label = "IFC Geometry" + bl_label = "Geometry" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): @@ -362,12 +397,11 @@ class BIM_PT_geometry(Panel): pass -class BIM_PT_4D5D(Panel): - bl_label = "IFC Costing and Scheduling" +class BIM_PT_tab_4D5D(Panel): + bl_label = "Costing and Scheduling" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): @@ -377,12 +411,11 @@ class BIM_PT_4D5D(Panel): pass -class BIM_PT_structural(Panel): - bl_label = "IFC Structural" +class BIM_PT_tab_structural(Panel): + bl_label = "Structural" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): @@ -392,12 +425,11 @@ class BIM_PT_structural(Panel): pass -class BIM_PT_services(Panel): - bl_label = "IFC Services" +class BIM_PT_tab_services(Panel): + bl_label = "Services" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): @@ -407,22 +439,21 @@ class BIM_PT_services(Panel): pass -class BIM_PT_quality_control(Panel): - bl_label = "IFC Quality Control" +class BIM_PT_tab_quality_control(Panel): + bl_label = "Quality Control" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" - bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "OTHER") + return tool.Blender.is_tab(context, "QUALITY") def draw(self, context): pass -class BIM_PT_integrations(Panel): +class BIM_PT_tab_integrations(Panel): bl_label = "BIM Integrations" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -431,87 +462,161 @@ class BIM_PT_integrations(Panel): @classmethod def poll(cls, context): - return tool.Blender.is_tab(context, "OTHER") + return tool.Blender.is_tab(context, "QUALITY") def draw(self, context): pass # Object panel groups -class BIM_PT_object_metadata(Panel): - bl_label = "IFC Object Metadata" +class BIM_PT_tab_object_metadata(Panel): + bl_label = "Object Metadata" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" - bl_context = "object" + bl_context = "scene" bl_order = 1 @classmethod def poll(cls, context): - return tool.Ifc.get() + return tool.Blender.is_tab(context, "OBJECT") and tool.Ifc.get() def draw(self, context): pass -class BIM_PT_geometry_object(Panel): - bl_label = "IFC Geometry" +class BIM_PT_tab_representations(Panel): + bl_label = "Representations" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" - bl_context = "object" + bl_context = "scene" bl_order = 1 - bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): - return tool.Ifc.get() + return tool.Blender.is_tab(context, "GEOMETRY") and tool.Ifc.get() def draw(self, context): pass -class BIM_PT_services_object(Panel): - bl_label = "IFC Services" +class BIM_PT_tab_geometric_relationships(Panel): + bl_label = "Geometric Relationships" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" - bl_context = "object" + bl_context = "scene" bl_order = 1 bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): - return tool.Ifc.get() + return tool.Blender.is_tab(context, "GEOMETRY") and tool.Ifc.get() def draw(self, context): pass -class BIM_PT_utilities_object(Panel): - bl_label = "IFC Utilities" +class BIM_PT_tab_parametric_geometry(Panel): + bl_label = "Parametric Geometry" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" - bl_context = "object" + bl_context = "scene" bl_order = 1 bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): - return tool.Ifc.get() + return tool.Blender.is_tab(context, "GEOMETRY") and tool.Ifc.get() def draw(self, context): pass -class BIM_PT_misc_object(Panel): - bl_label = "IFC Misc." +class BIM_PT_tab_materials(Panel): + bl_label = "Materials" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" - bl_context = "object" + bl_context = "scene" + bl_order = 1 + + @classmethod + def poll(cls, context): + return tool.Blender.is_tab(context, "GEOMETRY") and tool.Ifc.get() + + def draw(self, context): + pass + + +class BIM_PT_tab_styles(Panel): + bl_label = "Styles" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_order = 1 + + @classmethod + def poll(cls, context): + return tool.Blender.is_tab(context, "GEOMETRY") and tool.Ifc.get() + + def draw(self, context): + pass + + +class BIM_PT_tab_services_object(Panel): + bl_label = "Services" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_order = 1 + + @classmethod + def poll(cls, context): + return tool.Blender.is_tab(context, "SERVICES") and tool.Ifc.get() + + def draw(self, context): + pass + + +class BIM_PT_tab_misc(Panel): + bl_label = "Misc." + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" bl_order = 1 bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): - return tool.Ifc.get() + return tool.Blender.is_tab(context, "OBJECT") and tool.Ifc.get() + + def draw(self, context): + pass + + +class BIM_PT_tab_handover(Panel): + bl_label = "Commissioning and Handover" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_order = 1 + + @classmethod + def poll(cls, context): + return tool.Blender.is_tab(context, "FM") and tool.Ifc.get() + + def draw(self, context): + pass + + +class BIM_PT_tab_operations(Panel): + bl_label = "Operations and Maintenance" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + bl_order = 2 + + @classmethod + def poll(cls, context): + return tool.Blender.is_tab(context, "FM") and tool.Ifc.get() def draw(self, context): pass diff --git a/src/blenderbim/blenderbim/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py index ca6f2b9b33..12bfcf0950 100644 --- a/src/blenderbim/blenderbim/core/drawing.py +++ b/src/blenderbim/blenderbim/core/drawing.py @@ -339,6 +339,9 @@ def update_drawing_name(ifc, drawing_tool, drawing=None, name=None): if drawing_tool.does_file_exist(uri): drawing_tool.update_embedded_svg_location(uri, old_location, new_location) + if drawing_tool.is_editing_sheets(): + drawing_tool.import_sheets() + def add_annotation(ifc, collector, drawing_tool, drawing=None, object_type=None): context = drawing_tool.get_annotation_context( diff --git a/src/blenderbim/blenderbim/core/geometry.py b/src/blenderbim/blenderbim/core/geometry.py index 01dcde0555..b25ec4703a 100644 --- a/src/blenderbim/blenderbim/core/geometry.py +++ b/src/blenderbim/blenderbim/core/geometry.py @@ -88,7 +88,15 @@ def switch_representation( should_sync_changes_first=False, apply_openings=True, ): - """Function can switch to representation that wasn't yet assigned to that object. See #2766.""" + """Function can switch to representation that wasn't yet assigned to that object. See #2766. + + `should_sync_changes_first` - sync ifc representation with current state of `obj.data`; + + `should_reload` - reload `obj.data` from ifc representation; + + `is_global` - replace mesh data for all users of `obj.data`, not just `obj`; + + """ if should_sync_changes_first and geometry.is_edited(obj) and not geometry.is_box_representation(representation): representation_id = geometry.get_representation_id(representation) geometry.run_geometry_update_representation(obj=obj) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 25fa289a5d..7520251c62 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -252,16 +252,16 @@ class Drawing: def delete_object(cls, obj): pass def disable_editing_assigned_product(cls, obj): pass def disable_editing_drawings(cls): pass - def disable_editing_schedules(cls): pass def disable_editing_references(cls): pass + def disable_editing_schedules(cls): pass def disable_editing_sheets(cls): pass def disable_editing_text(cls, obj): pass def does_file_exist(cls, uri): pass def enable_editing(cls, obj): pass def enable_editing_assigned_product(cls, obj): pass def enable_editing_drawings(cls): pass - def enable_editing_schedules(cls): pass def enable_editing_references(cls): pass + def enable_editing_schedules(cls): pass def enable_editing_sheets(cls): pass def enable_editing_text(cls, obj): pass def ensure_unique_drawing_name(cls, name): pass @@ -297,13 +297,14 @@ class Drawing: def get_text_literal(cls, obj): pass def get_unit_system(cls): pass def import_assigned_product(cls, obj): pass - def import_drawings(cls): pass def import_documents(cls, document_type): pass + def import_drawings(cls): pass def import_sheets(cls): pass def import_text_attributes(cls, obj): pass def is_active_drawing(cls, drawing): pass def is_camera_orthographic(cls): pass def is_drawing_active(cls): pass + def is_editing_sheets(cls): pass def move_file(cls, src, dest): pass def open_spreadsheet(cls, uri): pass def open_svg(cls, filepath): pass diff --git a/src/blenderbim/blenderbim/tool/blender.py b/src/blenderbim/blenderbim/tool/blender.py index e315341eaa..c6f08fe548 100644 --- a/src/blenderbim/blenderbim/tool/blender.py +++ b/src/blenderbim/blenderbim/tool/blender.py @@ -398,12 +398,12 @@ class Blender: ) bmesh.update_edit_mesh(mesh) if not obj: - if not bpy.context.object or bpy.context.object.data != mesh: + if not bpy.context.active_object or bpy.context.active_object.data != mesh: raise Exception( "Error applying bmesh in EDIT object - object is " "not provided and can't be acquired from the context. " ) - obj = bpy.context.object + obj = bpy.context.active_object obj.update_from_editmode() else: bm.to_mesh(mesh) @@ -456,3 +456,32 @@ class Blender: return bpy.ops.object.mode_set(mode="EDIT_GPENCIL", toggle=True) else: return {"CANCELLED"} + + class Modifier: + @classmethod + def is_railing(cls, element): + return tool.Pset.get_element_pset(element, "BBIM_Railing") + + @classmethod + def is_roof(cls, element): + return tool.Pset.get_element_pset(element, "BBIM_Roof") + + @classmethod + def is_window(cls, element): + return tool.Pset.get_element_pset(element, "BBIM_Window") + + @classmethod + def is_door(cls, element): + return tool.Pset.get_element_pset(element, "BBIM_Door") + + @classmethod + def is_stair(cls, element): + return tool.Pset.get_element_pset(element, "BBIM_Stair") + + @classmethod + def is_editing_parameters(cls, obj): + return obj.BIMRailingProperties.is_editing or obj.BIMRoofProperties.is_editing + + @classmethod + def is_modifier_with_non_editable_path(cls, element): + return cls.is_stair(element) or cls.is_door(element) or cls.is_window(element) diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 0f179915d6..9d3bb0f884 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -24,6 +24,8 @@ import lark import bmesh import shutil import logging +import shapely +from shapely.ops import unary_union import mathutils import webbrowser import subprocess @@ -97,6 +99,8 @@ class Drawing(blenderbim.core.tool.Drawing): current_pos = camera.matrix_world @ current_pos obj.location = current_pos + ANNOTATION_TYPES_SUPPORT_SETUP = ("STAIR_ARROW", "TEXT", "REVISION_CLOUD", "FILL_AREA") + @classmethod def setup_annotation_object(cls, obj, object_type, related_object=None): """Finish object's adjustments after both object and entity are created""" @@ -135,6 +139,30 @@ class Drawing(blenderbim.core.tool.Drawing): cls.ensure_annotation_in_drawing_plane(obj) assign_product = True + elif object_type == "REVISION_CLOUD": + revised_object, cloud = related_object, obj + + verts = [np.array(revised_object.matrix_world @ v.co) for v in revised_object.data.vertices] + verts = [(np.around(v[[0, 1]], decimals=3)).tolist() for v in verts] + edges = [e.vertices for e in revised_object.data.edges] + + # shapely magic + boundary_lines = [shapely.LineString([verts[v] for v in e]) for e in edges] + unioned_boundaries = shapely.union_all(shapely.GeometryCollection(boundary_lines)) + all_polygons = shapely.polygonize(unioned_boundaries.geoms).geoms + outer_shell = unary_union(all_polygons) + + bm = tool.Blender.get_bmesh_for_mesh(obj.data, clean=True) + new_verts = list(outer_shell.exterior.coords) + bm_verts = [bm.verts.new(v + (0,)) for v in new_verts] + bm_edges = [bm.edges.new([bm_verts[i], bm_verts[i + 1]]) for i in range(len(new_verts) - 1)] + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) + + tool.Blender.apply_bmesh(obj.data, bm, obj) + cloud.location = Vector((0, 0, 0)) + cls.ensure_annotation_in_drawing_plane(cloud) + assign_product = True + if assign_product and not cls.get_assigned_product(obj_entity): tool.Ifc.run("drawing.assign_product", relating_product=related_entity, related_object=obj_entity) @@ -466,6 +494,10 @@ class Drawing(blenderbim.core.tool.Drawing): return items return items[0] + @classmethod + def is_editing_sheets(cls): + return bpy.context.scene.DocProperties.is_editing_sheets + @classmethod def remove_literal_from_annotation(cls, obj, literal): element = tool.Ifc.get_entity(obj) diff --git a/src/blenderbim/blenderbim/tool/geometry.py b/src/blenderbim/blenderbim/tool/geometry.py index 5e13ab5dd2..5369741dbb 100644 --- a/src/blenderbim/blenderbim/tool/geometry.py +++ b/src/blenderbim/blenderbim/tool/geometry.py @@ -35,6 +35,8 @@ class Geometry(blenderbim.core.tool.Geometry): @classmethod def change_object_data(cls, obj, data, is_global=False): if is_global: + if obj.mode == "EDIT": + raise Exception("user_remap is not supported in EDIT mode") obj.data.user_remap(data) else: obj.data = data @@ -75,6 +77,7 @@ class Geometry(blenderbim.core.tool.Geometry): if element.is_a("IfcRelSpaceBoundary"): ifcopenshell.api.run("boundary.remove_boundary", tool.Ifc.get(), boundary=element) return bpy.data.objects.remove(obj) + collection = obj.BIMObjectProperties.collection if collection: parent = ifcopenshell.util.element.get_aggregate(element) @@ -98,7 +101,7 @@ class Geometry(blenderbim.core.tool.Geometry): if element.VoidsElements: bpy.ops.bim.remove_opening(opening_id=element.id()) else: - is_spatial = element.is_a("IfcSpatialElement") + is_spatial = element.is_a() in ("IfcSpatialElement", "IfcSpatialStructureElement") if getattr(element, "HasOpenings", None): for rel in element.HasOpenings: bpy.ops.bim.remove_opening(opening_id=rel.RelatedOpeningElement.id()) diff --git a/src/blenderbim/blenderbim/tool/ifc.py b/src/blenderbim/blenderbim/tool/ifc.py index 36ab2f22c5..c6050492e3 100644 --- a/src/blenderbim/blenderbim/tool/ifc.py +++ b/src/blenderbim/blenderbim/tool/ifc.py @@ -21,6 +21,8 @@ import bpy import numpy as np import ifcopenshell.api import blenderbim.core.tool +import blenderbim.bim.handler +import blenderbim.tool as tool from blenderbim.bim.ifc import IfcStore @@ -88,6 +90,54 @@ class Ifc(blenderbim.core.tool.Ifc): def get_object(cls, element): return IfcStore.get_element(element.id()) + @classmethod + def rebuild_element_maps(cls): + """Rebuilds the id_map and guid_map + + When any Blender object is stored outside a Blender PointerProperty, + such as in a regular Python list, there is the likely probability that + the object will be invalidated when undo or redo occurs. Object + invalidation seems to occur whenever an object is affected during an + operation, or selected, or has a related modifier, and so on ... to + cover all bases, this completely rebuilds the element maps. + """ + + IfcStore.id_map = {} + IfcStore.guid_map = {} + + if not cls.get(): + return + + for obj in bpy.data.objects: + bpy.msgbus.clear_by_owner(obj) + + element = cls.get_entity(obj) + if not element: + continue + IfcStore.id_map[element.id()] = obj + global_id = getattr(element, "GlobalId", None) + if global_id: + IfcStore.guid_map[global_id] = obj + + blenderbim.bim.handler.subscribe_to(obj, "name", blenderbim.bim.handler.name_callback) + blenderbim.bim.handler.subscribe_to(obj, "mode", blenderbim.bim.handler.mode_callback) + blenderbim.bim.handler.subscribe_to( + obj, "active_material_index", blenderbim.bim.handler.active_material_index_callback + ) + + for obj in bpy.data.materials: + bpy.msgbus.clear_by_owner(obj) + + material = cls.get_entity(obj) + style = tool.Style.get_style(obj) + if material: + IfcStore.id_map[material.id()] = obj + if style: + IfcStore.id_map[style.id()] = obj + + blenderbim.bim.handler.subscribe_to(obj, "name", blenderbim.bim.handler.name_callback) + blenderbim.bim.handler.subscribe_to(obj, "diffuse_color", blenderbim.bim.handler.color_callback) + @classmethod def link(cls, element, obj): IfcStore.link_element(element, obj) diff --git a/src/blenderbim/blenderbim/tool/root.py b/src/blenderbim/blenderbim/tool/root.py index fb7eaa4e51..b7424ff3bc 100644 --- a/src/blenderbim/blenderbim/tool/root.py +++ b/src/blenderbim/blenderbim/tool/root.py @@ -111,8 +111,12 @@ class Root(blenderbim.core.tool.Root): if obj.data and obj.data.BIMMeshProperties.ifc_definition_id: return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) element = tool.Ifc.get_entity(obj) - if not obj.data and getattr(element, "ObjectType", None) == "TEXT": - return element.Representation.Representations[0] + if element.is_a("IfcTypeProduct"): + if element.RepresentationMaps: + return element.RepresentationMaps[0].MappedRepresentation + elif element.is_a("IfcProduct"): + if element.Representation: + return element.Representation.Representations[0] @classmethod def get_representation_context(cls, representation): @@ -157,8 +161,5 @@ class Root(blenderbim.core.tool.Root): @classmethod def set_object_name(cls, obj, element): - name = obj.name - if "/" in name and name.split("/")[0][0:3] == "Ifc": - name = "/".join(name.split("/")[1:]) - name = "{}/{}".format(element.is_a(), name) - obj.name = name + name = getattr(element, "Name", getattr(element, "AxisTag", None)) + obj.name = "{}/{}".format(element.is_a(), name or "Unnamed") diff --git a/src/blenderbim/blenderbim/tool/spatial.py b/src/blenderbim/blenderbim/tool/spatial.py index 95369338fd..75f5f20110 100644 --- a/src/blenderbim/blenderbim/tool/spatial.py +++ b/src/blenderbim/blenderbim/tool/spatial.py @@ -52,7 +52,7 @@ class Spatial(blenderbim.core.tool.Spatial): if not structure.is_a("IfcSpatialStructureElement"): return False else: - if not structure.is_a("IfcSpatialElement"): + if not structure.is_a() in ("IfcSpatialElement", "IfcSpatialStructureElement"): return False if not hasattr(element, "ReferencedInStructures"): return False @@ -204,7 +204,7 @@ class Spatial(blenderbim.core.tool.Spatial): parent = tool.Ifc.get().by_type("IfcProject")[0] for object in ifcopenshell.util.element.get_parts(parent) or []: - if object.is_a("IfcSpatialElement"): + if object.is_a() in ("IfcSpatialElement", "IfcSpatialStructureElement"): cls.create_new_storey_li(object, 0) cls.props.is_container_update_enabled = True @@ -223,7 +223,7 @@ class Spatial(blenderbim.core.tool.Spatial): new.has_children = True if new.is_expanded: for related_object in ifcopenshell.util.element.get_parts(element) or []: - if related_object.is_a("IfcSpatialElement"): + if related_object.is_a() in ("IfcSpatialElement", "IfcSpatialStructureElement"): cls.create_new_storey_li(related_object, level_index + 1) @classmethod diff --git a/src/blenderbim/docs/devs/writing_docs.rst b/src/blenderbim/docs/devs/writing_docs.rst index 87374de599..c5be24d5b2 100644 --- a/src/blenderbim/docs/devs/writing_docs.rst +++ b/src/blenderbim/docs/devs/writing_docs.rst @@ -15,7 +15,48 @@ All documentation is written in ReStructured Text and is available in the The following colours and annotation styles should be used for annotating images. All stroke widths are 3px with a corner radius of 3px. Horizontal -underlines are 5px with a corner radius of 2px. The dark green is **39b54a** and -the light green is **d9e021**. +underlines are 5px with a corner radius of 2px. The dark green is ``39b54a`` and +the light green is ``d9e021``. .. image:: documentation-style.png + +Special keywords such as **Technical Terminology** that the user should be +aware of should be bolded, titlecased, and used consistently. You *may* +use italics to emphasize words or phrases. Inline code must be ``quoted`` and +longer code snippets may use code blocks. + +.. code-block:: console + + $ cd /path/to/blenderbim + $ ls + +Be sure to specify the language to enable syntax highlighting. + +.. code-block:: python + + print("Hello, world!") + +A button may be used to point users to a critical sample file or +download. + +.. container:: blockbutton + + `Visit critical link `__ + +.. note:: + + Instead of writing "Note that XYZ ..." you should use notes sparingly to + highlight "gotchas". + +.. tip:: + + Tips may be used to add a useful but optional suggestion. + +.. warning:: + + Warnings may be used to highlight common mistakes. + +.. seealso:: + + See also blocks should be used to reference `further reading + `__ links. diff --git a/src/blenderbim/scripts/geonodes_modifier_prototype.py b/src/blenderbim/scripts/geonodes_modifier_prototype.py index f536b046f1..77b7b660a6 100644 --- a/src/blenderbim/scripts/geonodes_modifier_prototype.py +++ b/src/blenderbim/scripts/geonodes_modifier_prototype.py @@ -106,7 +106,7 @@ def update_geonodes_modifier(): bm.edges.index_update() bm.faces.index_update() - if bpy.context.object.mode == "EDIT": + if bpy.context.active_object.mode == "EDIT": bmesh.update_edit_mesh(obj.data) else: bm.to_mesh(obj.data) diff --git a/src/blenderbim/test/bim/feature/pset.feature b/src/blenderbim/test/bim/feature/pset.feature index c7ad1db8c9..b4c87ffb1f 100644 --- a/src/blenderbim/test/bim/feature/pset.feature +++ b/src/blenderbim/test/bim/feature/pset.feature @@ -347,7 +347,7 @@ Scenario: Remove pset - multiple objects Scenario: Edit pset length property Given an empty IFC project And I press "mesh.add_clever_stair" - And the variable "pset" is "tool.Pset.get_element_pset(tool.Ifc.get_entity(bpy.context.object), 'Pset_StairFlightCommon').id()" + And the variable "pset" is "tool.Pset.get_element_pset(tool.Ifc.get_entity(bpy.context.active_object), 'Pset_StairFlightCommon').id()" And the variable "si_conversion" is "ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())" And I press "bim.enable_pset_editing(pset_id={pset}, obj='IfcStairFlight/StairFlight', obj_type='Object')" @@ -380,7 +380,7 @@ Scenario: Edit qset length property Given an empty IFC project And I press "mesh.add_clever_stair" And I press "bim.calculate_all_quantities" - And the variable "pset" is "tool.Pset.get_element_pset(tool.Ifc.get_entity(bpy.context.object), 'Qto_StairFlightBaseQuantities').id()" + And the variable "pset" is "tool.Pset.get_element_pset(tool.Ifc.get_entity(bpy.context.active_object), 'Qto_StairFlightBaseQuantities').id()" And the variable "si_conversion" is "ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())" And I press "bim.enable_pset_editing(pset_id={pset}, obj='IfcStairFlight/StairFlight', obj_type='Object')" diff --git a/src/blenderbim/test/core/test_drawing.py b/src/blenderbim/test/core/test_drawing.py index 2cadacdc26..ba839a1869 100644 --- a/src/blenderbim/test/core/test_drawing.py +++ b/src/blenderbim/test/core/test_drawing.py @@ -492,6 +492,9 @@ class TestUpdateDrawingName: drawing.does_file_exist("absolute_layout_uri").should_be_called().will_return(True) drawing.update_embedded_svg_location("absolute_layout_uri", "old_location", "new_location").should_be_called() + drawing.is_editing_sheets().should_be_called().will_return(True) + drawing.import_sheets().should_be_called() + subject.update_drawing_name(ifc, drawing, drawing="drawing", name="name") diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python.rst b/src/ifcopenshell-python/docs/ifcopenshell-python.rst index 7991426418..ddaa9a33eb 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python.rst @@ -13,5 +13,6 @@ system, as well as high level analysis and authoring functions. ifcopenshell-python/hello_world ifcopenshell-python/code_examples ifcopenshell-python/geometry_processing + ifcopenshell-python/geometry_creation ifcopenshell-python/geometry_tree ifcopenshell-python/developer_guide diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_creation.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_creation.rst new file mode 100644 index 0000000000..75b3abf473 --- /dev/null +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_creation.rst @@ -0,0 +1,350 @@ +Geometry creation +================= + +Walls, doors, slabs, and other physical products in IFC can be represented with +2D or 3D geometry. Most commonly, this geometry is created using graphical +frontends, like the BlenderBIM Add-on. IfcOpenShell can create and edit +geometry with code. + +.. note:: + + Geometry is optional in IFC. For many usecases, geometry is not required, + such as in facility management. + +General concepts +---------------- + +Any IFC element may have a location in the 3D world known as the **Object +Placement**. The **Object Placement** is the "local origin" of the object. This +is sometimes known as the object's center or insertion point in other software. +The **Object Placement** is typically somewhere at a corner, center, or +midpoint of the object. The **Object Placement** may be used to identify a +rough "coordinate" location of the object's start / center, and used as a +center of transformation when moving or rotating the object's geometry. The +object's geometry is always relative to the **Object Placement**. + +.. note:: + + **Object Placements** are optional if the object has no geometry. However, + any object with a geometry must have an **Object Placement**. + + +IFC products may have multiple geometric representations, positioned relative +to the **Object Placement**. For example, a door might have a 3D body of a +"closed door" as one geometric representation, a 2D linework of an "open door" +intended to be shown in a plan, a 3D box showing the clearance of the door for +disabled access, and 3D dashed linework showing the hinge and swing of a door +in an elevation or section. Of course, you might not want to see all this +geometry at the same time. What you see depends on the context you are viewing +the door in. + +For this reason, each one of these geometric representations is called a +**Representation**. Each **Representation** belongs to a **Representation +Context**. The **Representation Context** determines how the **Representation** +is intended to be viewed. For example, a "2D Plan View" might be a +**Representation Context**. This allows the user to choose to see the +appropriate **Representation**. + + +Project units +------------- + +All coordinates in IFC are stored using project units. This means that prior to +creating **Object Placements** or **Representations** you have to define a +project length unit as a minimum. + +Assuming you are creating a project from scratch with code, here is how you +might define units: + +.. code-block:: python + + # You need a project before you can assign units. + run("root.create_entity", model, ifc_class="IfcProject") + + # Let's say we want coordinates to be in millimeters. + length = run("unit.add_si_unit", model, unit_type="LENGTHUNIT", prefix="MILLI") + run("unit.assign_unit", model, units=[length]) + + # Alternatively, you may specify without any arguments to automatically + # create millimeters, square meters, and cubic meters as a convenience for + # testing purposes. Sorry imperial folks, we prioritise metric here. + run("unit.assign_unit", model) + + +Object placements +----------------- + +The **Object Placement** describes **Location** and **Rotation**. The +**Location** is given as an XYZ coordinate, and the **Rotation** is given as +two vectors: a local X axis and a local Z axis vector. The local Y axis vector +is derived via a right-handed coordinate system. This means that the global X +axis points to "Project East", the global Y axis points to "Project North", and +the global Z axis points up (i.e. to the sky). This coordinate system is the +same system used in Blender. + +.. image:: images/object-placement.png + +The recommended way to set an **Object Placement** is to specify the placement +as a 4x4 matrix. You can use the ``numpy`` library to create and edit matrices. +A 4x4 matrix looks like this: + +.. code-block:: + + 1, 0, 0, 0 + 0, 1, 0, 0 + 0, 0, 1, 0 + 0, 0, 0, 1 + +This type of matrix is known as the **Identity Matrix**. It represents no +translation (i.e. a location at the origin of ``0, 0, 0``) and no rotation +(i.e. the X axis is ``1, 0, 0``, the Y axis is ``0, 1, 0``, and the Z axis is +``0, 0, 1``). The numbers in the matrix correlate to the location and rotation +axes as follows: + +.. code-block:: + + XAxis_X, YAxis_X, ZAxis_X, X + XAxis_Y, YAxis_Y, ZAxis_Y, Y + XAxis_Z, YAxis_Z, ZAxis_Z, Z + 0, 0, 0, 1 + +Notice how the last line is always fixed to ``0, 0, 0, 1``. For example, here +is another matrix of an object at ``2, 3, 5`` that is rotated anti-clockwise by +90 degrees. + +.. code-block:: + + 0, -1, 0, 2 + 1, 0, 0, 3 + 0, 0, 1, 5 + 0, 0, 0, 1 + +.. image:: images/object-placement-example.png + +Here's how we might do the same operation with Python code: + +.. code-block:: python + + import numpy + + # Create a wall. Our wall currently has no object placement or representations. + wall = run("root.create_entity", model, ifc_class="IfcWall") + + # Create a 4x4 identity matrix. This matrix is at the origin with no rotation. + matrix = numpy.eye(4) + + # Rotate the matix 90 degrees anti-clockwise around the Z axis (i.e. in plan). + # Anti-clockwise is positive. Clockwise is negative. + matrix = ifcopenshell.util.placement.rotation(90, "Z") @ matrix + + # Set the X, Y, Z coordinates. Notice how we rotate first then translate. + # This is because the rotation origin is always at 0, 0, 0. + matrix[:,3][0:3] = (2, 3, 5) + + # Set our wall's Object Placement using our matrix. + # `is_si=True` states that we are using SI units instead of project units. + run("geometry.edit_object_placement", model, product=wall, matrix=matrix, is_si=True) + +Representation contexts +----------------------- + +As an object may have multiple **Representations**, we need to use +**Representation Contexts** to distinguish the purpose and intended context of +each **Representation**. + +A **Representation Context** is defined in terms of X paramters: + +1. **Context Type**: 3D Model or 2D Plan +2. **Context Identifier**: The purpose of the **Representation** +3. **Target View**: The drafting convention of the **Representation** +4. **Target Scale**: The scale for the **representation** to be shown at + +The **Context Type** must either be set to **Model** for 3D **Representations** +or **Plan** for 2D **Representations**. + +The most common **Context Identifiers** you might use are: + +- Body: for the actual physical shape of the object +- Box: the bounding box of the object (useful for shape analytics) +- Axis: the parametric line determining the shape of the object +- Profile: the elevation silhouette of the object, useful for cutting out holes + for the object to fit into host elements +- Footprint: the plan view silhouette of the object, useful for certain + quantity take-off rules +- Clearance: the clearance zone of the object +- Annotation: symbolic annotations typically used in diagrams or drawings + +The most common **Target Views** you might use are: + +- MODEL_VIEW: for general 3D geometry you might see in a BIM viewer or any + generic fallback representation +- PLAN_VIEW: for 2D geometry you might see in a plan representation +- ELEVATION_VIEW: for 2D geometry you might see in an elevation representation +- SECTION_VIEW: for 2D geometry you might see in a section representation +- GRAPH_VIEW: for 2D or 3D line or frame or path connectivity diagrams you + might use for structural frame analysis, axis-based parametric modeling +- SKETCH_VIEW: for viewing abstract high-level representations such as in + bubble diagrams of spatial topology + +The vast majority of the time, you will only be interested in using a 3D Body +MODEL_VIEW **Representation Context**. + +.. code-block:: python + + # If we plan to store 3D geometry in our IFC model, we have to setup + # a "Model" context. + model3d = run("context.add_context", model, context_type="Model") + + # And/Or, if we plan to store 2D geometry, we need a "Plan" context + plan = run("context.add_context", model, context_type="Plan") + + # Now we setup the subcontexts with each of the geometric "purposes" + # we plan to store in our model. "Body" is by far the most important + # and common context, as most IFC models are assumed to be viewable + # in 3D. + body = run("context.add_context", model, + context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model3d) + + # The 3D Axis subcontext is important if any "axis-based" parametric + # geometry is going to be created. For example, a beam, or column + # may be drawn using a single 3D axis line, and for this we need an + # Axis subcontext. + run("context.add_context", model, + context_type="Model", context_identifier="Axis", target_view="GRAPH_VIEW", parent=model3d) + + # It's also important to have a 2D Axis subcontext for things like + # walls and claddings which can be drawn using a 2D axis line. + run("context.add_context", model, + context_type="Plan", context_identifier="Axis", target_view="GRAPH_VIEW", parent=plan) + + # The 3D Box subcontext is useful for clash detection or shape + # analysis, or even lazy-loading of large models. + run("context.add_context", model, + context_type="Model", context_identifier="Box", target_view="MODEL_VIEW", parent=model3d) + + # A 2D annotation subcontext for plan views are important for door + # swings, window cuts, and symbols for equipment like GPOs, fire + # extinguishers, and so on. + run("context.add_context", model, + context_type="Plan", context_identifier="Annotation", target_view="PLAN_VIEW", parent=plan) + + # You may also create 2D annotation subcontexts for sections and + # elevation views. + run("context.add_context", model, + context_type="Plan", context_identifier="Annotation", target_view="SECTION_VIEW", parent=plan) + run("context.add_context", model, + context_type="Plan", context_identifier="Annotation", target_view="ELEVATION_VIEW", parent=plan) + +Representations +--------------- + +Once you have an **Object Placement** and a **Representation Context**, you can +now create a **Representation**. + +Each **Representations** must choose a geometry modeling technique. For +example, you may specify a mesh-like geometry, which uses vertices, edges, and +faces. Alternatively, you may specify 2D profiles extruded into solid shapes +and potentially having boolean voids and subtractions. You may even specify +single edges and linework without any surfaces or solids. Representations may +even be single points, such as for survey points or structual point +connections. + +After the **Representation** is created, you will need to assign the +**Representation** to the IFC object (e.g. wall, door, slab, etc). Here's the +general pattern in code: + +.. code-block:: python + + # Let's create a new project using millimeters with a single furniture element at the origin. + model = run("project.create_file") + run("root.create_entity", model, ifc_class="IfcProject") + run("unit.assign_unit", model) + + # We want our representation to be the 3D body of the element. + # This representation context is only created once per project. + # You must reuse the same body context every time you create a new representation. + model3d = run("context.add_context", model, context_type="Model") + body = run("context.add_context", model, + context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model3d) + + # Create our element with an object placement. + element = run("root.create_entity", model, ifc_class="IfcFurniture") + run("geometry.edit_object_placement", model, product=element) + + # Let's create our representation! + # See below sections for examples on how to create representations. + representation = ... + + # Assign our new body representation back to our element + run("geometry.assign_representation", model, product=element, representation=representation) + + +Mesh representations +-------------------- + +Mesh **Representations** are specified in terms of a list of vertices, edges, +and faces. The faces may be triangles, quads, or n-gons. Faces may also contain +inner loops, or holes. Mesh **Representations** are most appropriately used for +complex shapes that only need to approximately represent physical products, +such as furniture or equipment, or flat, panellised design (e.g. triangulated +facade elements). Mesh **Representations** are also suitable for box-like +shapes that have bespoke indents, protrusions, TINs, textured, or as-built +geometry. + +In IFC, meshes may be stored as **Faceted BReps**, **Tessellations**, or +**Triangulations** (specifically only for triangles). + +.. code-block:: python + + # These vertices and faces represent a 2m square 1m high pyramid in SI units. + # Note how they are nested lists. Each nested list represents a "mesh". There may be multiple meshes. + vertices = [[(0.,0.,0.), (0.,2.,0.), (2.,2.,0.), (2.,0.,0.), (1.,1.,1.)]] + faces = [[(0,1,2,3), (0,4,1), (1,4,2), (2,4,3), (3,4,0)]] + representation = run("geometry.add_mesh_representation", model, context=body, vertices=vertices, faces=faces) + +.. image:: images/mesh-representation.png + +Wall representations +-------------------- + +Wall-like **Representations** are simple blocks with a length, height, and +thickness. They are most appropriately used for walls, insulation, bulkhead +ends, cladding, and other uniformly thick blocks that extend along an imaginary +2D line in the XY plane. + +.. note:: + + Even though the function is named ``add_wall_representation``, you may use + this geometry for any element, not just walls. + +.. code-block:: python + + # A wall-like representation, 5 meters long, 3 meters high, and 200mm thick + representation = run("geometry.add_wall_representation", model, + context=body, length=5, height=3, thickness=0.2) + +.. image:: images/wall-representation.png + +A wall-like **Representation** always starts at the **Object Placement** and +runs along the local +X axis. The thickness is always along the local Y axis. +This means that if you want the wall-like object to start and end at a +particular point, you have to set the **Object Placement** location and +rotation as appropriate. This can be done using the API: + +.. code-block:: python + + # A wall-like representation starting and ending at a particular 2D point + # It is not necessary to assign the representation after using this function. + run("geometry.create_2pt_wall", model, + element=element, context=body, p1=(1., 1.), p2=(3., 2.), elevation=0, height=3, thickness=0.2) + +.. image:: images/wall-2pt-representation.png + +Profile representations +----------------------- + +Custom representations +---------------------- + +Manual representations +---------------------- diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/images/mesh-representation.png b/src/ifcopenshell-python/docs/ifcopenshell-python/images/mesh-representation.png new file mode 100644 index 0000000000..b4c2d50873 Binary files /dev/null and b/src/ifcopenshell-python/docs/ifcopenshell-python/images/mesh-representation.png differ diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/images/object-placement-example.png b/src/ifcopenshell-python/docs/ifcopenshell-python/images/object-placement-example.png new file mode 100644 index 0000000000..3b86432bea Binary files /dev/null and b/src/ifcopenshell-python/docs/ifcopenshell-python/images/object-placement-example.png differ diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/images/object-placement.png b/src/ifcopenshell-python/docs/ifcopenshell-python/images/object-placement.png new file mode 100644 index 0000000000..dddb49f23e Binary files /dev/null and b/src/ifcopenshell-python/docs/ifcopenshell-python/images/object-placement.png differ diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/images/wall-2pt-representation.png b/src/ifcopenshell-python/docs/ifcopenshell-python/images/wall-2pt-representation.png new file mode 100644 index 0000000000..76929f061a Binary files /dev/null and b/src/ifcopenshell-python/docs/ifcopenshell-python/images/wall-2pt-representation.png differ diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/images/wall-representation.png b/src/ifcopenshell-python/docs/ifcopenshell-python/images/wall-representation.png new file mode 100644 index 0000000000..7fd262f50c Binary files /dev/null and b/src/ifcopenshell-python/docs/ifcopenshell-python/images/wall-representation.png differ diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_sverchok_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py similarity index 100% rename from src/ifcopenshell-python/ifcopenshell/api/geometry/add_sverchok_representation.py rename to src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/create_2pt_wall.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/create_2pt_wall.py new file mode 100644 index 0000000000..45860fe836 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/create_2pt_wall.py @@ -0,0 +1,73 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2023 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +import numpy as np +import ifcopenshell.api +import ifcopenshell.util.unit + + +class Usecase: + def __init__(self, file, element=None, context=None, p1=None, p2=None, elevation=None, height=None, thickness=None): + self.file = file + self.settings = { + "element": element, + "context": context, + "p1": p1, + "p2": p2, + "elevation": elevation, + "height": height, + "thickness": thickness, + } + + def execute(self): + self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) + + self.settings["p1"] = np.array(self.settings["p1"]) + self.settings["p2"] = np.array(self.settings["p2"]) + + length = float(np.linalg.norm(self.settings["p2"] - self.settings["p1"])) + representation = ifcopenshell.api.run( + "geometry.add_wall_representation", + self.file, + context=self.settings["context"], + length=length, + height=self.settings["height"], + thickness=self.settings["thickness"], + ) + v = self.settings["p2"] - self.settings["p1"] + v /= float(np.linalg.norm(v)) + matrix = np.array( + [ + [v[0], -v[1], 0, self.settings["p1"][0]], + [v[1], v[0], 0, self.settings["p1"][1]], + [0, 0, 1, self.convert_si_to_unit(self.settings["elevation"])], + [0, 0, 0, 1], + ] + ) + ifcopenshell.api.run( + "geometry.edit_object_placement", self.file, product=self.settings["element"], matrix=matrix + ) + ifcopenshell.api.run( + "geometry.assign_representation", self.file, product=self.settings["element"], representation=representation + ) + return representation + + def convert_si_to_unit(self, co): + if isinstance(co, (tuple, list)): + return [self.convert_si_to_unit(o) for o in co] + return co / self.settings["unit_scale"] diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py b/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py index a2618078e1..061b695d24 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py @@ -86,24 +86,40 @@ class Usecase: inverse.RelatedObjects = [to_element] pset = ifcopenshell.util.element.copy_deep(self.file, inverse.RelatingPropertyDefinition) inverse.RelatingPropertyDefinition = pset - elif inverse.is_a("IfcRelNests") and inverse.RelatingObject == from_element: - ports = [e for e in inverse.RelatedObjects if e.is_a("IfcDistributionPort")] - if ports: - new_ports = [ifcopenshell.api.run("root.copy_class", self.file, product=p) for p in ports] - inverse = ifcopenshell.util.element.copy(self.file, inverse) + elif ( + inverse.is_a("IfcRelNests") + and inverse.RelatingObject == from_element + or inverse.is_a("IfcRelConnectsPortToElement") + and inverse.RelatedElement == from_element + ): + # IfcRelConnectsPortToElement was used in IFC2X3 + if inverse.is_a("IfcRelNests"): + ports = [e for e in inverse.RelatedObjects if e.is_a("IfcDistributionPort")] + else: # IfcRelConnectsPortToElement + ports = [inverse.RelatingPort] + if not ports: + continue + new_ports = [ifcopenshell.api.run("root.copy_class", self.file, product=p) for p in ports] + inverse = ifcopenshell.util.element.copy(self.file, inverse) + + if inverse.is_a("IfcRelNests"): inverse.RelatingObject = to_element inverse.RelatedObjects = new_ports - for port in new_ports: - ifcopenshell.api.run("system.unassign_port", self.file, element=from_element, port=port) - matrix = ifcopenshell.util.placement.get_local_placement(port.ObjectPlacement) - ifcopenshell.api.run( - "geometry.edit_object_placement", - self.file, - product=port, - matrix=matrix, - is_si=False, - should_transform_children=False, - ) + else: + inverse.RelatedElement = to_element + inverse.RelatingPort = new_ports[0] + + for port in new_ports: + ifcopenshell.api.run("system.unassign_port", self.file, element=from_element, port=port) + matrix = ifcopenshell.util.placement.get_local_placement(port.ObjectPlacement) + ifcopenshell.api.run( + "geometry.edit_object_placement", + self.file, + product=port, + matrix=matrix, + is_si=False, + should_transform_children=False, + ) elif inverse.is_a("IfcRelAggregates") and inverse.RelatingObject == from_element: continue elif inverse.is_a("IfcRelContainedInSpatialStructure") and inverse.RelatingStructure == from_element: diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py index 37206881be..eb61cea031 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py @@ -125,7 +125,12 @@ class Usecase: elif inverse.is_a("IfcRelContainedInSpatialStructure"): if inverse.RelatingStructure == self.settings["product"] or len(inverse.RelatedElements) == 1: self.file.remove(inverse) - elif inverse.is_a("IfcRelConnectsPathElements"): + elif inverse.is_a("IfcRelConnectsElements"): + if inverse.is_a("IfcRelConnectsWithRealizingElements"): + if self.settings["product"] not in (inverse.RelatingElement, inverse.RelatedElement) and any( + el for el in inverse.RealizingElements if el != self.settings["product"] + ): + continue self.file.remove(inverse) elif inverse.is_a("IfcRelAssignsToGroup"): if len(inverse.RelatedObjects) == 1: diff --git a/src/ifcopenshell-python/ifcopenshell/util/placement.py b/src/ifcopenshell-python/ifcopenshell/util/placement.py index fe6e8f78bb..7219cd6856 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/placement.py +++ b/src/ifcopenshell-python/ifcopenshell/util/placement.py @@ -101,3 +101,30 @@ def get_storey_elevation(storey): matrix = get_local_placement(storey.ObjectPlacement) return matrix[2][3] return getattr(storey, "Elevation", 0.0) or 0.0 + + +def rotation(angle, axis, is_degrees=True): + theta = np.radians(angle) if is_degrees else angle + cos, sin = np.cos(theta), np.sin(theta) + + if axis == "X": + return np.array([ + [1, 0, 0, 0], + [0, cos, -sin, 0], + [0, sin, cos, 0], + [0, 0, 0, 1] + ]) + elif axis == "Y": + return np.array([ + [cos, 0, sin, 0], + [0, 1, 0, 0], + [-sin, 0, cos, 0], + [0, 0, 0, 1] + ]) + elif axis == "Z": + return np.array([ + [cos, -sin, 0, 0], + [sin, cos, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ]) diff --git a/src/ifcopenshell-python/test/api/root/test_copy_class.py b/src/ifcopenshell-python/test/api/root/test_copy_class.py index 9512fe1014..782ef05472 100644 --- a/src/ifcopenshell-python/test/api/root/test_copy_class.py +++ b/src/ifcopenshell-python/test/api/root/test_copy_class.py @@ -232,3 +232,15 @@ class TestCopyClass(test.bootstrap.IFC4): new = ifcopenshell.api.run("root.copy_class", self.file, product=element) assert len(self.file.by_type("IfcRelAssignsToGroup")) == 1 assert new.HasAssignments[0].RelatingGroup == group + + +class TestCopyClassIFC2X3(test.bootstrap.IFC2X3): + def test_copying_distribution_ports(self): + element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcFlowTerminal") + port = ifcopenshell.api.run("system.add_port", self.file) + ifcopenshell.api.run("system.assign_port", self.file, element=element, port=port) + new = ifcopenshell.api.run("root.copy_class", self.file, product=element) + new_ports = ifcopenshell.util.system.get_ports(new) + assert port not in new_ports + assert new_ports[0].is_a("IfcDistributionPort") + assert ifcopenshell.util.system.get_ports(element) == [port] diff --git a/src/ifcopenshell-python/test/api/root/test_remove_product.py b/src/ifcopenshell-python/test/api/root/test_remove_product.py index d383859f88..7431551d77 100644 --- a/src/ifcopenshell-python/test/api/root/test_remove_product.py +++ b/src/ifcopenshell-python/test/api/root/test_remove_product.py @@ -196,6 +196,76 @@ class TestRemoveProduct(test.bootstrap.IFC4): assert len(self.file.by_type("IfcColumn")) == 1 assert len(self.file.by_type("IfcBeam")) == 0 + def test_removing_connection_relationships_of_an_element(self): + element1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + element2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab") + ifcopenshell.api.run( + "geometry.connect_element", + self.file, + related_element=element1, + relating_element=element2, + ) + total_entities = len(list(self.file)) + ifcopenshell.api.run("root.remove_product", self.file, product=element1) + assert len(list(self.file)) == total_entities - 2 + assert len(self.file.by_type("IfcRelConnectsElements")) == 0 + assert len(self.file.by_type("IfcSlab")) == 1 + assert len(self.file.by_type("IfcWall")) == 0 + + def test_removing_connection_relationships_of_an_element_with_additional_realizing_element(self): + wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + slab1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab") + slab2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab") + self.file.createIfcRelConnectsWithRealizingElements( + ifcopenshell.guid.new(), + OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file), + RelatingElement=wall, + RelatedElement=slab1, + RealizingElements=(wall, slab1, slab2), + ) + total_entities = len(list(self.file)) + ifcopenshell.api.run("root.remove_product", self.file, product=wall) + assert len(list(self.file)) == total_entities - 2 + assert len(self.file.by_type("IfcRelConnectsElements")) == 0 + assert len(self.file.by_type("IfcSlab")) == 2 + assert len(self.file.by_type("IfcWall")) == 0 + + def test_removing_connection_relationships_of_an_element_element_is_realizing_element(self): + wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + slab1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab") + slab2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab") + self.file.createIfcRelConnectsWithRealizingElements( + ifcopenshell.guid.new(), + OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file), + RelatingElement=wall, + RelatedElement=slab1, + RealizingElements=(wall, slab1, slab2), + ) + total_entities = len(list(self.file)) + ifcopenshell.api.run("root.remove_product", self.file, product=slab2) + assert len(list(self.file)) == total_entities - 1 + assert len(self.file.by_type("IfcRelConnectsElements")) == 1 + assert len(self.file.by_type("IfcSlab")) == 1 + assert len(self.file.by_type("IfcWall")) == 1 + + def test_removing_connection_relationships_of_an_element_element_is_only_realizing_element(self): + wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + slab1 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab") + slab2 = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcSlab") + self.file.createIfcRelConnectsWithRealizingElements( + ifcopenshell.guid.new(), + OwnerHistory=ifcopenshell.api.run("owner.create_owner_history", self.file), + RelatingElement=wall, + RelatedElement=slab1, + RealizingElements=(slab2,), + ) + total_entities = len(list(self.file)) + ifcopenshell.api.run("root.remove_product", self.file, product=slab2) + assert len(list(self.file)) == total_entities - 2 + assert len(self.file.by_type("IfcRelConnectsElements")) == 0 + assert len(self.file.by_type("IfcSlab")) == 1 + assert len(self.file.by_type("IfcWall")) == 1 + def test_removing_all_property_relationships_of_an_element(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="Foo_Bar") diff --git a/src/ifcopenshell-python/test/test_wall_opening.py b/src/ifcopenshell-python/test/test_wall_opening.py index f331297e73..6c9ed4242d 100644 --- a/src/ifcopenshell-python/test/test_wall_opening.py +++ b/src/ifcopenshell-python/test/test_wall_opening.py @@ -64,6 +64,7 @@ X = 1.0, 0.0, 0.0 Y = 0.0, 1.0, 0.0 Z = 0.0, 0.0, 1.0 + # Creates an IfcAxis2Placement3D from Location, Axis and RefDirection specified as Python tuples def create_ifcaxis2placement(f, point=O, dir1=Z, dir2=X): point = f.createIfcCartesianPoint(point) @@ -100,7 +101,6 @@ def create_ifcextrudedareasolid(f, point_list, ifcaxis2placement, extrude_dir, e def create_case(fn, openings): - f = ifcopenshell.template.create() owner_history = f.by_type("IfcOwnerHistory")[0] @@ -127,7 +127,6 @@ def create_case(fn, openings): ) for opening in openings: - opening_placement = create_ifclocalplacement( f, (opening.x, 0.0, opening.z), (0.0, 1.0, 0.0), (1.0, 0.0, 0.0), wall_placement ) @@ -147,7 +146,6 @@ def create_case(fn, openings): class TestWallOpenings: @pytest.mark.skipif(shutil.which("IfcConvert") is None, reason="Requires IfcConvert in path") def test_all(self): - cases = [ ( "wall-openings-non-intersecting-rect-circle.ifc", @@ -205,7 +203,6 @@ class TestWallOpenings: result.append([fn]) for i in range(2 if PERF else 1): - args = [ shutil.which("IfcConvert") or "IfcConvert", "-qyvvv", @@ -236,12 +233,16 @@ class TestWallOpenings: # breakpoint() + temp_files = [fn, f"{fn}.obj", f"{fn}.log.json", f"{fn}.mtl"] + for temp_fn in temp_files: + os.unlink(temp_fn) + try: import tabulate except: return - print(tabulate.tabulate(result, headers=["file", "", "--no-2d-boolean"], tablefmt="github")) + print("\n" + tabulate.tabulate(result, headers=["file", "", "--no-2d-boolean"], tablefmt="github")) if __name__ == "__main__":