From 5b63b00e986121535ffe5c571162b93c64f3a81f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 13 Nov 2024 15:27:31 +0500 Subject: [PATCH 01/20] Workaround for Blender 4.3 #5743 --- src/bonsai/Makefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/bonsai/Makefile b/src/bonsai/Makefile index 291fe95b1d..aafff82b3a 100644 --- a/src/bonsai/Makefile +++ b/src/bonsai/Makefile @@ -317,6 +317,12 @@ endif exit 1; \ fi + # Temporary workaround for Blender not handling non-3.11 wheels #5743. + # Use '-n' as on macos x86-64 doesn't have a binary wheel and it autoincludes 'cp311'. + prev_whl_name=$$(find build/bonsai/wheels/tzfpy-*.whl); \ + whl_name=$$(echo $$prev_whl_name | sed "s/-cp39-/-cp$(PYNUMBER)-/"); \ + mv -n "$$prev_whl_name" "$$whl_name"; + ifneq ($(PLATFORM), linux) # Safeguard: in case one of `pip download` will break, # it will produce a linux wheel for non-linux build (our github action machine is using linux). From 77fc2a89d0273de90d4bc0e6e342829385bf6dc2 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 13 Nov 2024 16:18:50 +0500 Subject: [PATCH 02/20] Oops, fix broken #4771 by mistake --- src/bonsai/bonsai/bim/module/material/operator.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index b8d5de5ce0..dffee45435 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -745,7 +745,6 @@ class ExpandMaterialCategory(bpy.types.Operator): category.is_expanded = True if category.name == self.category: props.active_material_index = index - break core.load_materials(tool.Material, props.material_type) return {"FINISHED"} @@ -775,7 +774,6 @@ class ContractMaterialCategory(bpy.types.Operator): category.is_expanded = False if category.name == self.category: props.active_material_index = index - break core.load_materials(tool.Material, props.material_type) return {"FINISHED"} From e2bf611c1793884d136c64ea7a1170b9e569ea82 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 12 Nov 2024 14:53:36 +0500 Subject: [PATCH 03/20] black . --- src/bonsai/bonsai/bim/module/profile/operator.py | 16 ++++++++++++---- src/bonsai/bonsai/bim/module/profile/prop.py | 4 +++- src/bonsai/bonsai/bim/module/project/operator.py | 1 - 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py index 20bf981e52..9306efc7ef 100644 --- a/src/bonsai/bonsai/bim/module/profile/operator.py +++ b/src/bonsai/bonsai/bim/module/profile/operator.py @@ -155,18 +155,26 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator): obj = context.scene.BIMProfileProperties.object_to_profile if obj: if len(obj.data.polygons) != 1: - self.report({"WARNING"}, "This mesh is invalid to create a profile. Select a flat mesh with no more then one face.") + self.report( + {"WARNING"}, + "This mesh is invalid to create a profile. Select a flat mesh with no more then one face.", + ) return for v in obj.data.polygons[0].vertices: vert = obj.data.vertices[v] if vert.co.z > 0: - self.report({"WARNING"}, "This mesh is invalid to create a profile. Select a flat mesh with all its vertices at z=0") + self.report( + {"WARNING"}, + "This mesh is invalid to create a profile. Select a flat mesh with all its vertices at z=0", + ) return obj_points.append((vert.co.x, vert.co.y)) if obj_points: obj_points.append(obj_points[0]) - obj_points = [(v[0] - obj_points[0][0], v[1] - obj_points[0][1]) for v in obj_points] # Make sure the first point is (0, 0) - + obj_points = [ + (v[0] - obj_points[0][0], v[1] - obj_points[0][1]) for v in obj_points + ] # Make sure the first point is (0, 0) + props = context.scene.BIMProfileProperties profile_class = props.profile_classes if profile_class == "IfcArbitraryClosedProfileDef": diff --git a/src/bonsai/bonsai/bim/module/profile/prop.py b/src/bonsai/bonsai/bim/module/profile/prop.py index ab385b8f29..222868e67c 100644 --- a/src/bonsai/bonsai/bim/module/profile/prop.py +++ b/src/bonsai/bonsai/bim/module/profile/prop.py @@ -72,7 +72,9 @@ class BIMProfileProperties(PropertyGroup): description="Check to only show IfcProfileDefs attached to IfcMaterialProfiles", update=lambda self, context: bpy.ops.bim.load_profiles(), ) - object_to_profile: PointerProperty(name="Object to profile", type=bpy.types.Object, description="Object to copy the mesh to a profile") + object_to_profile: PointerProperty( + name="Object to profile", type=bpy.types.Object, description="Object to copy the mesh to a profile" + ) def generate_thumbnail_for_active_profile(): diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 9274ee1efb..8b34cdd3a1 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2503,7 +2503,6 @@ class ClearMeasurement(bpy.types.Operator): def poll(cls, context): return len(context.scene.BIMPolylineProperties.measurement_polyline) > 0 - def execute(self, context): context.scene.BIMPolylineProperties.measurement_polyline.clear() MeasureDecorator.uninstall() From 00046898412d82ceb500e2f5fb82b23236a5527a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 12 Nov 2024 18:40:38 +0500 Subject: [PATCH 04/20] typing --- src/bonsai/bonsai/bim/module/spatial/operator.py | 2 +- src/bonsai/bonsai/tool/collector.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py index 2ff0689225..676f1939d6 100644 --- a/src/bonsai/bonsai/bim/module/spatial/operator.py +++ b/src/bonsai/bonsai/bim/module/spatial/operator.py @@ -400,7 +400,7 @@ class SetContainerVisibility(bpy.types.Operator, tool.Ifc.Operator): if self.mode == "ISOLATE": if tool.Ifc.get_schema() == "IFC2X3": containers = tool.Ifc.get().by_type("IfcSpatialStructureElement") - elif tool.Ifc.get_schema() != "IFC2X3": + else: containers = set(tool.Ifc.get().by_type("IfcSpatialElement")) containers -= set(tool.Ifc.get().by_type("IfcSpatialZone")) for container in containers: diff --git a/src/bonsai/bonsai/tool/collector.py b/src/bonsai/bonsai/tool/collector.py index 76aeb1483c..40382258cd 100644 --- a/src/bonsai/bonsai/tool/collector.py +++ b/src/bonsai/bonsai/tool/collector.py @@ -39,6 +39,7 @@ class Collector(bonsai.core.tool.Collector): users_collection.objects.unlink(obj) element = tool.Ifc.get_entity(obj) + assert element # Note that tool.Geometry.is_locked is only checked within the if # statements for efficiency as it is a slow check. From 3bbbfd23518bb43c28a9b4ff6276c547d882151e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 12 Nov 2024 19:14:56 +0500 Subject: [PATCH 05/20] Show errors if failing to parse false origin / project north --- src/bonsai/bonsai/bim/import_ifc.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index f5388d1097..23c21fa6f4 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -21,6 +21,7 @@ import bpy import time import json import logging +import traceback import mathutils import numpy as np import multiprocessing @@ -1226,12 +1227,20 @@ class IfcImportSettings: settings.false_origin_mode = props.false_origin_mode try: settings.false_origin = [float(o) for o in props.false_origin.split(",")[:3]] - except: - settings.false_origin = [0, 0, 0] + except Exception as e: + print(traceback.format_exc()) + raise Exception( + f"Failed to set false origin from string '{props.false_origin}'.\n" + f"Error: {str(e)}.\nSee above for the details." + ) try: settings.project_north = float(props.project_north) - except: - settings.project_north = 0 + except Exception as e: + print(traceback.format_exc()) + raise Exception( + f"Failed to set project north from string '{props.project_north}'.\n" + f"Error: {str(e)}.\nSee above for the details." + ) settings.element_limit_mode = props.element_limit_mode settings.element_offset = props.element_offset settings.element_limit = props.element_limit From 659c3295bcfe6a01dd77e6b5723a4f08e68d2057 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 13 Nov 2024 10:59:03 +0500 Subject: [PATCH 06/20] Remove unnecessary IFC operators --- .../bonsai/bim/module/spatial/operator.py | 58 +++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py index 676f1939d6..bf6acfe3d9 100644 --- a/src/bonsai/bonsai/bim/module/spatial/operator.py +++ b/src/bonsai/bonsai/bim/module/spatial/operator.py @@ -90,22 +90,24 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator): core.assign_container(tool.Ifc, tool.Collector, tool.Spatial, container=container, element_obj=element_obj) -class EnableEditingContainer(bpy.types.Operator, tool.Ifc.Operator): +class EnableEditingContainer(bpy.types.Operator): bl_idname = "bim.enable_editing_container" bl_label = "Enable Editing Container" bl_options = {"REGISTER", "UNDO"} - def _execute(self, context): + def execute(self, context): core.enable_editing_container(tool.Spatial, obj=context.active_object) + return {"FINISHED"} -class DisableEditingContainer(bpy.types.Operator, tool.Ifc.Operator): +class DisableEditingContainer(bpy.types.Operator): bl_idname = "bim.disable_editing_container" bl_label = "Disable Editing Container" bl_options = {"REGISTER", "UNDO"} - def _execute(self, context): + def execute(self, context): core.disable_editing_container(tool.Spatial, obj=context.active_object) + return {"FINISHED"} class RemoveContainer(bpy.types.Operator, tool.Ifc.Operator): @@ -155,7 +157,7 @@ class CopyToContainer(bpy.types.Operator, tool.Ifc.Operator): bonsai.bim.handler.refresh_ui_data() -class SelectContainer(bpy.types.Operator, tool.Ifc.Operator): +class SelectContainer(bpy.types.Operator): bl_idname = "bim.select_container" bl_label = "Select Container" bl_options = {"REGISTER", "UNDO"} @@ -172,13 +174,13 @@ class SelectContainer(bpy.types.Operator, tool.Ifc.Operator): self.selection_mode = "SINGLE" return self.execute(context) - def _execute(self, context): + def execute(self, context): if self.container: container = tool.Ifc.get().by_id(self.container) elif element := tool.Ifc.get_entity(context.active_object): container = ifcopenshell.util.element.get_container(element) else: - return + return {"CANCELLED"} if container: core.select_container( tool.Ifc, @@ -186,15 +188,17 @@ class SelectContainer(bpy.types.Operator, tool.Ifc.Operator): container=container, selection_mode=self.selection_mode, ) + return {"FINISHED"} -class SelectSimilarContainer(bpy.types.Operator, tool.Ifc.Operator): +class SelectSimilarContainer(bpy.types.Operator): bl_idname = "bim.select_similar_container" bl_label = "Select Similar Container" bl_options = {"REGISTER", "UNDO"} - def _execute(self, context): + def execute(self, context): core.select_similar_container(tool.Ifc, tool.Spatial, obj=context.active_object) + return {"FINISHED"} class SelectProduct(bpy.types.Operator): @@ -279,18 +283,19 @@ class ToggleContainerElement(bpy.types.Operator): return {"FINISHED"} -class SelectDecomposedElement(bpy.types.Operator, tool.Ifc.Operator): +class SelectDecomposedElement(bpy.types.Operator): bl_idname = "bim.select_decomposed_element" bl_label = "Select Decomposed Element" bl_options = {"REGISTER", "UNDO"} element: bpy.props.IntProperty() - def _execute(self, context): + def execute(self, context): if self.element: core.select_decomposed_element(tool.Ifc, tool.Spatial, element=tool.Ifc.get().by_id(self.element)) + return {"FINISHED"} -class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator): +class SelectDecomposedElements(bpy.types.Operator): bl_idname = "bim.select_decomposed_elements" bl_label = "Select Children" bl_options = {"REGISTER", "UNDO"} @@ -306,7 +311,7 @@ class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator): self.should_filter = False return self.execute(context) - def _execute(self, context): + def execute(self, context): ifc_file = tool.Ifc.get() container = ifc_file.by_id(self.container) props = context.scene.BIMSpatialDecompositionProperties @@ -315,13 +320,13 @@ class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator): if not self.should_filter and not element_filter: tool.Spatial.select_products(tool.Spatial.get_decomposed_elements(container)) - return + return {"CANCELLED"} if props.element_mode == "TYPE": if active_element.type == "OCCURRENCE": if obj := tool.Ifc.get_object(ifc_file.by_id(active_element.ifc_definition_id)): tool.Blender.set_active_object(obj) - return + return {"CANCELLED"} ifc_class = relating_type = None is_untyped = False @@ -346,7 +351,7 @@ class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator): if active_element.type == "OCCURRENCE": if obj := tool.Ifc.get_object(ifc_file.by_id(active_element.ifc_definition_id)): tool.Blender.set_active_object(obj) - return + return {"CANCELLED"} if active_element.type == "CLASSIFICATION": identification = active_element.identification @@ -362,20 +367,22 @@ class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator): return False tool.Spatial.select_products(filter(filter_element, elements)) + return {"FINISHED"} -class SetDefaultContainer(bpy.types.Operator, tool.Ifc.Operator): +class SetDefaultContainer(bpy.types.Operator): bl_idname = "bim.set_default_container" bl_label = "Set Default Container" bl_options = {"REGISTER", "UNDO"} bl_description = "Set this as the default container that all new elements will be contained in" container: bpy.props.IntProperty() - def _execute(self, context): + def execute(self, context): core.set_default_container(tool.Spatial, container=tool.Ifc.get().by_id(self.container)) + return {"FINISHED"} -class SetContainerVisibility(bpy.types.Operator, tool.Ifc.Operator): +class SetContainerVisibility(bpy.types.Operator): bl_idname = "bim.set_container_visibility" bl_label = "Set Container Visibility" bl_options = {"REGISTER", "UNDO"} @@ -396,7 +403,7 @@ class SetContainerVisibility(bpy.types.Operator, tool.Ifc.Operator): self.should_include_children = False return self.execute(context) - def _execute(self, context): + def execute(self, context): if self.mode == "ISOLATE": if tool.Ifc.get_schema() == "IFC2X3": containers = tool.Ifc.get().by_type("IfcSpatialStructureElement") @@ -420,29 +427,31 @@ class SetContainerVisibility(bpy.types.Operator, tool.Ifc.Operator): collection.hide_viewport = should_hide if self.should_include_children: queue.extend(ifcopenshell.util.element.get_parts(container)) + return {"FINISHED"} -class ToggleGrids(bpy.types.Operator, tool.Ifc.Operator): +class ToggleGrids(bpy.types.Operator): bl_idname = "bim.toggle_grids" bl_label = "Toggle Grids" bl_options = {"REGISTER", "UNDO"} bl_description = "Show or hide grids and grid axes" is_visible: bpy.props.BoolProperty(name="Is Visible", default=False, options={"SKIP_SAVE"}) - def _execute(self, context): + def execute(self, context): for element in tool.Ifc.get().by_type("IfcGrid") + tool.Ifc.get().by_type("IfcGridAxis"): if obj := tool.Ifc.get_object(element): obj.hide_set(not self.is_visible) + return {"FINISHED"} -class ToggleSpatialElements(bpy.types.Operator, tool.Ifc.Operator): +class ToggleSpatialElements(bpy.types.Operator): bl_idname = "bim.toggle_spatial_elements" bl_label = "Toggle Spatial Elements" bl_options = {"REGISTER", "UNDO"} bl_description = "Show or hide spatial elements, such as buildings, sites, etc" is_visible: bpy.props.BoolProperty(name="Is Visible", default=False, options={"SKIP_SAVE"}) - def _execute(self, context): + def execute(self, context): if tool.Ifc.get().schema == "IFC2X3": elements = tool.Ifc.get().by_type("IfcSpatialStructureElement") else: @@ -450,3 +459,4 @@ class ToggleSpatialElements(bpy.types.Operator, tool.Ifc.Operator): for element in elements: if obj := tool.Ifc.get_object(element): obj.hide_set(not self.is_visible) + return {"FINISHED"} From 5913fe881ee0b9d50dec054d9181ed33001bf5b9 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 13 Nov 2024 11:30:37 +0500 Subject: [PATCH 07/20] Fix missing BIM tool UI when just spatial element is active Noticed by "select decomposition" button missing, spatial elements are unselectable by default - they're just active. cc @trhyder --- src/bonsai/bonsai/bim/module/model/workspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 9a733646a2..6dbedcb6fe 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -109,7 +109,7 @@ class BimTool(WorkSpaceTool): def draw_settings(cls, context, layout, ws_tool): if context.scene.BIMGeometryProperties.mode == "ITEM": EditItemUI.draw(context, layout) - elif context.active_object and context.selected_objects and tool.Ifc.get_entity(context.active_object): + elif context.active_object and tool.Ifc.get_entity(context.active_object): EditObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) else: CreateObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) From 2845071621e00689ee3b16490b98a64019bccb25 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 13 Nov 2024 17:26:51 +0500 Subject: [PATCH 08/20] Fix error switching from different type of representations E.g. from curve representation to mesh representation and vice versa. Mentioned in #5687. --- src/bonsai/bonsai/tool/geometry.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 532fd0fccc..e7e5e4eaff 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -747,7 +747,10 @@ class Geometry(bonsai.core.tool.Geometry): meshes[mesh_name] = mesh old_mesh = obj.data - cls.change_object_data(obj, mesh, is_global=False) + if type(old_mesh) == type(mesh): + cls.change_object_data(obj, mesh, is_global=False) + else: + obj = cls.recreate_object_with_data(obj, mesh, is_global=False) cls.record_object_materials(obj) if not cls.has_data_users(old_mesh): cls.delete_data(old_mesh) @@ -778,7 +781,10 @@ class Geometry(bonsai.core.tool.Geometry): meshes[mesh_name] = mesh old_mesh = obj.data - cls.change_object_data(obj, mesh, is_global=False) + if type(old_mesh) == type(mesh): + cls.change_object_data(obj, mesh, is_global=False) + else: + obj = cls.recreate_object_with_data(obj, mesh, is_global=False) cls.record_object_materials(obj) if not cls.has_data_users(old_mesh): cls.delete_data(old_mesh) From a7caee14b08ced1508aa215814f578df4bccd649 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 13 Nov 2024 17:32:19 +0500 Subject: [PATCH 09/20] Remove fallback from ca96815 since bonsai build is already updated --- src/bonsai/bonsai/bim/import_ifc.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 23c21fa6f4..b5d5c33ea3 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -1061,11 +1061,7 @@ class IfcImporter: v2 = vertices[edge[1]] polyline.points.add(1) polyline.points[-1].co = mathutils.Vector(v2) - # TODO: remove error handling after we update build in Bonsai. - try: - edges_item_ids = ifcopenshell.util.shape.get_edges_representation_item_ids(geometry).tolist() - except AttributeError: - edges_item_ids = [] + edges_item_ids = ifcopenshell.util.shape.get_edges_representation_item_ids(geometry).tolist() curve["ios_edges_item_ids"] = edges_item_ids return curve From 6fb744561050b143f43b560188e58f75d3daefb6 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 13 Nov 2024 18:15:25 +0500 Subject: [PATCH 10/20] Set blender materials for imported IfcSweptDiskSolids #5687 --- src/bonsai/bonsai/bim/import_ifc.py | 38 ++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index b5d5c33ea3..57f3eca136 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -844,14 +844,46 @@ class IfcImporter: curve = bpy.data.curves.new(mesh_name, type="CURVE") curve.dimensions = "3D" curve.resolution_u = 2 - polyline = curve.splines.new("POLY") - for item_data in ifcopenshell.util.representation.resolve_items(native_data["representation"]): + rep_items = ifcopenshell.util.representation.resolve_items(native_data["representation"]) + + # Find item styles and add them to the curve. + material_style = None + material = ifcopenshell.util.element.get_material(element) + if material: + material_style = tool.Material.get_style(material) + item_styles: list[Union[bpy.types.Material, None]] = [] + for item_data in rep_items: item = item_data["item"] + item_style = tool.Style.get_representation_item_style(item) or material_style + if item_style is not None: + item_style = tool.Ifc.get_object(item_style) + assert isinstance(item_style, bpy.types.Material) + item_styles.append(item_style) + item_styles_unique = list(set(item_styles)) + for item_style in item_styles_unique: + curve.materials.append(item_style) + use_same_material_index = len(item_styles_unique) < 2 + + def new_polyline(item_style: Union[bpy.types.Material, None]) -> bpy.types.Spline: + if use_same_material_index: + material_index = 0 + else: + material_index = item_styles_unique.index(item_style) + + polyline = curve.splines.new("POLY") + polyline.material_index = material_index + return polyline + + for item_data, item_style in zip(rep_items, item_styles): + item = item_data["item"] + + polyline = new_polyline(item_style) matrix = item_data["matrix"] matrix[0][3] *= self.unit_scale matrix[1][3] *= self.unit_scale matrix[2][3] *= self.unit_scale + # TODO: support inner radius, start param, and end param geometry = tool.Loader.create_generic_shape(item.Directrix) if not geometry: @@ -864,7 +896,7 @@ class IfcImporter: for edge in edges: v1 = vertices[edge[0]] if v1 != v2: - polyline = curve.splines.new("POLY") + polyline = new_polyline(item_style) polyline.points[-1].co = native_data["matrix"] @ mathutils.Vector(v1) v2 = vertices[edge[1]] polyline.points.add(1) From 099703fda753704d99bdbebef98c164c423bdb1c Mon Sep 17 00:00:00 2001 From: sboddy Date: Wed, 13 Nov 2024 13:38:26 +0000 Subject: [PATCH 11/20] UI drawing improvements (#5677) * Fix Shift-clicking SVG-DXF conversion * Fix Schedules and References not incrementing the ID number properly * Remove the superfluous Create and Open buttons from the Active Drawing pane * Add active camera indicator to the Drawing list * Improved enable/disabled icon behaviour As many operators as possible now use polling method to set enabled/disabled state. Icons set their enabled/disabled state intelligently depending on what is selected. When an operator is disabled it says why it is disabled in the tooltip. * Rename core open_sheet, operator OpenSheet/bim.open_sheet to better reflect what it actually does, i.e. open_layout * Added selection checkmarks to Sheets, and operator to select all/none, to do batch ops on sheets * Missed adding the renamed operator to __init__ in commit 4cfc0bd9f3d795f68febb323d6dd6b19fb5a46bf * Decouple creating and viewing drawings/sheets to avoid focus stealing popups polluting display on long batch runs * Improve drawing selections in OpenDrawing operator * Add the Shift+click power move to Create Sheet * Add the Open Sheet + Shift+click power move * Improve drawing selections in RemoveDrawing operator * All operators are now documented with standardised language and terms. * Add status bar Info messages for feedback on create tasks that now don't have visual feedback * Fix enable/disable for ActivateDrawing operator icons. This was a strange one. They have common invoke and execute methods, but the polling has different conditions. I tried subclassing the original ActivateDrawing for the Sheets version, but something about Blenders operators breaks the classmethod decorated poll. This was the cleanest solution I found, but maybe there's a better one. * Placeholder operator for DuplicateSheet function. Does nothing but report Not Implemented. Could be omitted if this kind of thing is frowned upon. * Change icons to be more consistent * Final changes to toolbars Change button order and spacing for better usability. Add a checkmark all/none button, to do batch ops on sheets. Moved Bonsai Web UI button up to sheets toolbar. Some general cleanup of unnecessary clutter in the toolbar code. * Sheet/Drawing checkmarks remember their state properly * Adjust sheet/drawing operators to view after singular create, but not when batch creating more than one. * By special request, add back the opening windows when creating, but requiring Ctrl to be pressed. * black the files * Second attempt at blacking ui.py because I used an old black * Change method of getting active drawing to use one in tool import as requested * Fix for incorrect d1fc5158d - actually checked it this time --- .../bonsai/bim/module/drawing/__init__.py | 4 + .../bonsai/bim/module/drawing/operator.py | 529 ++++++++++++++---- src/bonsai/bonsai/bim/module/drawing/prop.py | 1 + src/bonsai/bonsai/bim/module/drawing/ui.py | 111 ++-- src/bonsai/bonsai/core/drawing.py | 2 +- src/bonsai/bonsai/tool/drawing.py | 10 +- 6 files changed, 485 insertions(+), 172 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/__init__.py b/src/bonsai/bonsai/bim/module/drawing/__init__.py index a3578ed702..83dd36f5ce 100644 --- a/src/bonsai/bonsai/bim/module/drawing/__init__.py +++ b/src/bonsai/bonsai/bim/module/drawing/__init__.py @@ -21,6 +21,7 @@ from . import ui, prop, operator, handler, gizmos, workspace classes = ( operator.ActivateDrawing, + operator.ActivateDrawingFromSheet, operator.ActivateDrawingStyle, operator.ActivateModel, operator.AddAnnotation, @@ -52,6 +53,7 @@ classes = ( operator.DisableEditingSheets, operator.DisableEditingText, operator.DuplicateDrawing, + operator.DuplicateSheet, operator.EditAssignedProduct, operator.EditElementFilter, operator.EditSheet, @@ -68,6 +70,7 @@ classes = ( operator.LoadSchedules, operator.LoadSheets, operator.OpenDrawing, + operator.OpenLayout, operator.OpenReference, operator.OpenSchedule, operator.OpenSheet, @@ -86,6 +89,7 @@ classes = ( operator.SaveDrawingStyle, operator.SaveDrawingStylesData, operator.SelectAllDrawings, + operator.SelectAllSheets, operator.SelectAssignedProduct, operator.SelectDocIfcFile, operator.OpenDocumentationWebUi, diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index ef16ebd69e..3f0c4378d9 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -154,10 +154,19 @@ class AddDrawing(bpy.types.Operator, tool.Ifc.Operator): class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.duplicate_drawing" bl_label = "Duplicate Drawing" + bl_description = "Make a copy of currently selected drawing" bl_options = {"REGISTER", "UNDO"} drawing: bpy.props.IntProperty() should_duplicate_annotations: bpy.props.BoolProperty(name="Should Duplicate Annotations", default=False) + @classmethod + def poll(cls, context): + props = context.scene.DocProperties + if not tool.Drawing.get_active_drawing_item(): + cls.poll_message_set("No drawing selected.") + return False + return True + def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) @@ -190,10 +199,16 @@ class CreateDrawing(bpy.types.Operator): bl_idname = "bim.create_drawing" bl_label = "Create Drawing" bl_description = ( - "Creates/refreshes a .svg drawing based on currently active camera.\n\n" - + "SHIFT+CLICK to print all selected drawings" + "Creates/refreshes a .svg drawing based on currently active camera\n" + + 'and open with default system viewer or using "svg_command" or\n' + + '"pdf_command" from the Bonsai preferences (if provided).\n\n' + + "SHIFT+CLICK to create/refresh all shown checked drawings, but doesn't\n" + + "open them for viewing.\n\n" + + "Add the CTRL modifier to optionally open drawings to view them as\n" + + "they are created" ) print_all: bpy.props.BoolProperty(name="Print All", default=False, options={"SKIP_SAVE"}) + open_viewer: bpy.props.BoolProperty(name="Open in Viewer", default=False, options={"SKIP_SAVE"}) sync: bpy.props.BoolProperty( name="Sync Before Creating Drawing", description="Could save some time if you're sure IFC and current Blender session are already in sync", @@ -223,6 +238,8 @@ class CreateDrawing(bpy.types.Operator): # make sure to use SKIP_SAVE on property, otherwise it might get stuck if event.type == "LEFTMOUSE" and event.shift: self.print_all = True + if event.type == "LEFTMOUSE" and event.ctrl: + self.open_viewer = True return self.execute(context) def execute(self, context): @@ -291,8 +308,11 @@ class CreateDrawing(bpy.types.Operator): with profile("Combine SVG layers"): svg_path = self.combine_svgs(context, underlay_svg, linework_svg, annotation_svg) - tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().svg_command, svg_path) - + if self.open_viewer: + drawing_uri = tool.Drawing.get_document_uri(tool.Drawing.get_drawing_document(self.drawing)) + tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().svg_command, drawing_uri) + if not self.open_viewer: + self.report({"INFO"}, f"{len(drawings_to_print)} drawings created...") if self.print_all: bpy.ops.bim.activate_drawing(drawing=original_drawing_id, should_view_from_camera=False) return {"FINISHED"} @@ -1436,22 +1456,163 @@ class AddSheet(bpy.types.Operator, tool.Ifc.Operator): core.add_sheet(tool.Ifc, tool.Drawing, titleblock=context.scene.DocProperties.titleblock) -class OpenSheet(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.open_sheet" - bl_label = "Open Sheet Layout" +class DuplicateSheet(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.duplicate_sheet" + bl_label = "Duplicate Sheet" + bl_description = "Make a copy of currently selected sheet" bl_options = {"REGISTER", "UNDO"} + drawing: bpy.props.IntProperty() + + @classmethod + def poll(cls, context): + # Unconditionally disable until implemented + cls.poll_message_set("Not implemented yet.") + return False + + props = context.scene.DocProperties + if not tool.Drawing.get_active_drawing_item(): + cls.poll_message_set("No drawing selected.") + return False + return True + + def _execute(self, context): + pass + """ + self.props = context.scene.DocProperties + core.duplicate_sheet( + tool.Ifc, + tool.Drawing, + sheet=tool.Ifc.get().by_id(self.sheet), + ) + try: + sheet = tool.Ifc.get().by_id(self.props.active_sheet_id) + core.sync_references(tool.Ifc, tool.Collector, tool.Sheet, drawing=sheet) + except: + pass + """ + + +class OpenLayout(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.open_layout" + bl_label = "Open Sheet Layout" + bl_description = ( + "Opens selected .svg layout with default system viewer\n" + + 'or using "layout_svg_command" from the Bonsai preferences\n' + + "(if provided)" + ) + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + props = context.scene.DocProperties + active_sheet = props.sheets[props.active_sheet_index] + if not active_sheet.is_sheet: + cls.poll_message_set("No sheet selected.") + return False + return True def _execute(self, context): self.props = context.scene.DocProperties sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id) sheet_builder = sheeter.SheetBuilder() sheet_builder.update_sheet_drawing_sizes(sheet) - core.open_sheet(tool.Drawing, sheet=sheet) + core.open_layout(tool.Drawing, sheet=sheet) + + +class SelectAllSheets(bpy.types.Operator): + bl_idname = "bim.select_all_sheets" + bl_label = "Select All Sheetss" + view: bpy.props.StringProperty() + bl_description = "Select all sheets in the sheet list.\n\n" + "SHIFT+CLICK to deselect all sheets" + select_all: bpy.props.BoolProperty(name="Open All", default=True, options={"SKIP_SAVE"}) + + def invoke(self, context, event): + # deselect all sheets on shift+click + # make sure to use SKIP_SAVE on property, otherwise it might get stuck + if event.type == "LEFTMOUSE" and event.shift: + self.select_all = False + return self.execute(context) + + def execute(self, context): + for sheet in context.scene.DocProperties.sheets: + if sheet.is_selected != self.select_all: + sheet.is_selected = self.select_all + return {"FINISHED"} + + +class OpenSheet(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.open_sheet" + bl_label = "Open Sheet" + bl_description = ( + "Opens selected sheet with default system viewer\n" + + 'or using "svg_command" or "pdf_command" from\n' + + "the Bonsai preferences (if provided).\n\n" + + "SHIFT+CLICK to open all shown checked sheets" + ) + bl_options = {"REGISTER", "UNDO"} + open_all: bpy.props.BoolProperty(name="Open All", default=False, options={"SKIP_SAVE"}) + + @classmethod + def poll(cls, context): + props = context.scene.DocProperties + active_sheet = props.sheets[props.active_sheet_index] + if not active_sheet.is_sheet: + cls.poll_message_set("No sheet selected.") + return False + return True + + def invoke(self, context, event): + # opening all sheets on shift+click + # make sure to use SKIP_SAVE on property, otherwise it might get stuck + if event.type == "LEFTMOUSE" and event.shift: + self.open_all = True + return self.execute(context) + + def execute(self, context): + self.props = context.scene.DocProperties + svg2pdf_command = tool.Blender.get_addon_preferences().svg2pdf_command + + if self.open_all: + sheets = [ + tool.Ifc.get().by_id(s.ifc_definition_id) for s in self.props.sheets if s.is_sheet and s.is_selected + ] + else: + sheets = [tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id)] + + sheet_uris = [] + sheets_not_found = [] + + for sheet in sheets: + if not sheet.is_a("IfcDocumentInformation"): + continue + sheet_builder = sheeter.SheetBuilder() + references = sheet_builder.build(sheet) + sheet_uri = references["SHEET"] + if svg2pdf_command: + sheet_uri = os.path.splitext(sheet_uri)[0] + ".pdf" + sheet_uris.append(sheet_uri) + if not os.path.exists(sheet_uri): + sheets_not_found.append(sheet.Name) + + if sheets_not_found: + msg = "Some sheets .svg/.pdf files were not found, need to create them first: \n{}.".format( + "\n".join(sheets_not_found) + ) + self.report({"ERROR"}, msg) + return {"CANCELLED"} + + for sheet_uri in sheet_uris: + if svg2pdf_command: + tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().pdf_command, sheet_uri) + else: + tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().svg_command, sheet_uri) + return {"FINISHED"} class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_drawing_to_sheet" - bl_label = "Add Drawing To Sheet" + bl_label = "Add Selected Drawing To Sheet" + bl_description = "Add the drawing selected in the\nDrawings list below to the sheet" bl_options = {"REGISTER", "UNDO"} @classmethod @@ -1496,7 +1657,14 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator): attributes = tool.Drawing.generate_reference_attributes( reference, Identification=str( - len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE")]) + 1 + len( + [ + r + for r in references + if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE", "REFERENCE") + ] + ) + + 1 ), Location=drawing_reference.Location, Description="DRAWING", @@ -1512,9 +1680,20 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator): class RemoveDrawingFromSheet(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_drawing_from_sheet" bl_label = "Remove Drawing From Sheet" + bl_description = "Remove currently selected drawing from sheet" bl_options = {"REGISTER", "UNDO"} reference: bpy.props.IntProperty() + @classmethod + def poll(cls, context): + props = context.scene.DocProperties + active_sheet = props.sheets[props.active_sheet_index] + + if active_sheet.reference_type == "TITLEBLOCK": + cls.poll_message_set("No effect deleting this.") + return False + return True + def _execute(self, context): reference = tool.Ifc.get().by_id(self.reference) sheet = tool.Drawing.get_reference_document(reference) @@ -1531,98 +1710,129 @@ class RemoveDrawingFromSheet(bpy.types.Operator, tool.Ifc.Operator): class CreateSheets(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.create_sheets" bl_label = "Create Sheets" - bl_description = "Build a sheet from the sheet layout" + bl_description = ( + "Build and open selected sheet from the sheet layout and\n" + + "optionally create .pdf and .dxf from commands in\n" + + "the Bonsai preferences (if provided).\n\n" + + "SHIFT+CLICK to create all shown checked sheets, but doesn't\n" + + "open them for viewing\n\n" + + "Add the CTRL modifier to optionally open sheets to view them as\n" + + "they are created" + ) bl_options = {"REGISTER", "UNDO"} + create_all: bpy.props.BoolProperty(name="Create All", default=False, options={"SKIP_SAVE"}) + open_viewer: bpy.props.BoolProperty(name="Open in Viewer", default=False, options={"SKIP_SAVE"}) + @classmethod def poll(cls, context): - return context.scene.DocProperties.sheets and context.scene.BIMProperties.data_dir + props = context.scene.DocProperties + active_sheet = props.sheets[props.active_sheet_index] + if not active_sheet.is_sheet: + cls.poll_message_set("No sheet selected.") + return False + return props.sheets and context.scene.BIMProperties.data_dir + + def invoke(self, context, event): + # opening all sheets on shift+click + # make sure to use SKIP_SAVE on property, otherwise it might get stuck + if event.type == "LEFTMOUSE" and event.shift: + self.create_all = True + if event.type == "LEFTMOUSE" and event.ctrl: + self.open_viewer = True + return self.execute(context) def _execute(self, context): scene = context.scene props = scene.DocProperties - active_sheet = props.sheets[props.active_sheet_index] - sheet = tool.Ifc.get().by_id(active_sheet.ifc_definition_id) - - # Update any drawing boundary changes - sheet_builder = sheeter.SheetBuilder() - sheet_builder.update_sheet_drawing_sizes(sheet) - - if not sheet.is_a("IfcDocumentInformation"): - return - - name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0] - sheet_builder = sheeter.SheetBuilder() - sheet_builder.data_dir = scene.BIMProperties.data_dir - - references = sheet_builder.build(sheet) - raster_references = [tool.Ifc.get_relative_uri(r) for r in references["RASTER"]] - - # These variables will be made available to the evaluated commands - svg = references["SHEET"] - pdf = os.path.splitext(svg)[0] + ".pdf" - replacements = { - "svg": svg, - "basename": os.path.basename(svg), - "path": os.path.dirname(svg), - "pdf": pdf, - "eps": os.path.splitext(svg)[0] + ".eps", - "dxf": os.path.splitext(svg)[0] + ".dxf", - } - - has_sheet_reference = False - for reference in tool.Drawing.get_document_references(sheet): - reference_description = tool.Drawing.get_reference_description(reference) - if reference_description == "SHEET": - has_sheet_reference = True - elif reference_description == "RASTER": - if reference.Location in raster_references: - raster_references.remove(reference.Location) - else: - tool.Ifc.run("document.remove_reference", reference=reference) - - if not has_sheet_reference: - reference = tool.Ifc.run("document.add_reference", information=sheet) - tool.Ifc.run( - "document.edit_reference", - reference=reference, - attributes=tool.Drawing.generate_reference_attributes( - reference, Location=tool.Ifc.get_relative_uri(svg), Description="SHEET" - ), - ) - - for raster_reference in raster_references: - reference = tool.Ifc.run("document.add_reference", information=sheet) - tool.Ifc.run( - "document.edit_reference", - reference=reference, - attributes=tool.Drawing.generate_reference_attributes( - reference, Location=tool.Ifc.get_relative_uri(raster_reference), Description="RASTER" - ), - ) - svg2pdf_command = tool.Blender.get_addon_preferences().svg2pdf_command svg2dxf_command = tool.Blender.get_addon_preferences().svg2dxf_command - if svg2pdf_command: - # With great power comes great responsibility. Example: - # [["inkscape", "svg", "-o", "pdf"]] - commands = json.loads(svg2pdf_command) - for command in commands: - subprocess.run([replacements.get(c, c) for c in command]) - - if svg2dxf_command: - # With great power comes great responsibility. Example: - # [["inkscape", "svg", "-o", "eps"], ["pstoedit", "-dt", "-f", "dxf:-polyaslines -mm", "eps", "dxf", "-psarg", "-dNOSAFER"]] - commands = json.loads(svg2dxf_command) - for command in commands: - command[0] = shutil.which(command[0]) or command[0] - subprocess.run([replacements.get(c, c) for c in command]) - - if svg2pdf_command: - tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().pdf_command, pdf) + if self.create_all: + sheets = [tool.Ifc.get().by_id(s.ifc_definition_id) for s in props.sheets if s.is_sheet and s.is_selected] else: - tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().svg_command, svg) + sheets = [tool.Ifc.get().by_id(props.sheets[props.active_sheet_index].ifc_definition_id)] + + for sheet in sheets: + # Update any drawing boundary changes + sheet_builder = sheeter.SheetBuilder() + sheet_builder.update_sheet_drawing_sizes(sheet) + + if not sheet.is_a("IfcDocumentInformation"): + return + + name = os.path.splitext(os.path.basename(tool.Drawing.get_document_uri(sheet)))[0] + sheet_builder = sheeter.SheetBuilder() + sheet_builder.data_dir = scene.BIMProperties.data_dir + + references = sheet_builder.build(sheet) + raster_references = [tool.Ifc.get_relative_uri(r) for r in references["RASTER"]] + + # These variables will be made available to the evaluated commands + svg = references["SHEET"] + pdf = os.path.splitext(svg)[0] + ".pdf" + replacements = { + "svg": svg, + "basename": os.path.basename(svg), + "path": os.path.dirname(svg), + "pdf": pdf, + "eps": os.path.splitext(svg)[0] + ".eps", + "dxf": os.path.splitext(svg)[0] + ".dxf", + } + + has_sheet_reference = False + for reference in tool.Drawing.get_document_references(sheet): + reference_description = tool.Drawing.get_reference_description(reference) + if reference_description == "SHEET": + has_sheet_reference = True + elif reference_description == "RASTER": + if reference.Location in raster_references: + raster_references.remove(reference.Location) + else: + tool.Ifc.run("document.remove_reference", reference=reference) + + if not has_sheet_reference: + reference = tool.Ifc.run("document.add_reference", information=sheet) + tool.Ifc.run( + "document.edit_reference", + reference=reference, + attributes=tool.Drawing.generate_reference_attributes( + reference, Location=tool.Ifc.get_relative_uri(svg), Description="SHEET" + ), + ) + + for raster_reference in raster_references: + reference = tool.Ifc.run("document.add_reference", information=sheet) + tool.Ifc.run( + "document.edit_reference", + reference=reference, + attributes=tool.Drawing.generate_reference_attributes( + reference, Location=tool.Ifc.get_relative_uri(raster_reference), Description="RASTER" + ), + ) + + if svg2pdf_command: + # With great power comes great responsibility. Example: + # [["inkscape", "svg", "-o", "pdf"]] + commands = json.loads(svg2pdf_command) + for command in commands: + subprocess.run([replacements.get(c, c) for c in command]) + + if svg2dxf_command: + # With great power comes great responsibility. Example: + # [["inkscape", "svg", "-o", "eps"], ["pstoedit", "-dt", "-f", "dxf:-polyaslines -mm", "eps", "dxf", "-psarg", "-dNOSAFER"]] + commands = json.loads(svg2dxf_command) + for command in commands: + command[0] = shutil.which(command[0]) or command[0] + subprocess.run([replacements.get(c, c) for c in command]) + + if self.open_viewer: + if svg2pdf_command: + tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().pdf_command, pdf) + else: + tool.Drawing.open_with_user_command(tool.Blender.get_addon_preferences().svg_command, svg) + if not self.open_viewer: + self.report({"INFO"}, f"{len(sheets)} sheets created...") class SelectAllDrawings(bpy.types.Operator): @@ -1651,12 +1861,20 @@ class OpenDrawing(bpy.types.Operator): bl_label = "Open Drawing" view: bpy.props.StringProperty() bl_description = ( - "Opens a .svg drawing based on currently active camera with default system viewer\n" + "Opens selected .svg drawing with default system viewer\n" + 'or using "svg_command" from the Bonsai preferences (if provided).\n\n' - + "SHIFT+CLICK to open all selected drawings" + + "SHIFT+CLICK to open all shown checked drawings" ) open_all: bpy.props.BoolProperty(name="Open All", default=False, options={"SKIP_SAVE"}) + @classmethod + def poll(cls, context): + props = context.scene.DocProperties + if not tool.Drawing.get_active_drawing_item(): + cls.poll_message_set("No drawing selected.") + return False + return True + def invoke(self, context, event): # opening all drawings on shift+click # make sure to use SKIP_SAVE on property, otherwise it might get stuck @@ -1665,12 +1883,13 @@ class OpenDrawing(bpy.types.Operator): return self.execute(context) def execute(self, context): + self.props = context.scene.DocProperties if self.open_all: drawings = [ - tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings if d.is_selected + tool.Ifc.get().by_id(d.ifc_definition_id) for d in self.props.drawings if d.is_drawing and d.is_selected ] else: - drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)] + drawings = [tool.Ifc.get().by_id(self.props.drawings.get(self.view).ifc_definition_id)] drawing_uris = [] drawings_not_found = [] @@ -1682,7 +1901,7 @@ class OpenDrawing(bpy.types.Operator): drawings_not_found.append(drawing.Name) if drawings_not_found: - msg = "Some drawings .svg files were not found, need to print them first: \n{}.".format( + msg = "Some drawings .svg files were not found, need to create them first: \n{}.".format( "\n".join(drawings_not_found) ) self.report({"ERROR"}, msg) @@ -1755,20 +1974,7 @@ class ActivateModel(bpy.types.Operator): return {"FINISHED"} -class ActivateDrawing(bpy.types.Operator): - bl_idname = "bim.activate_drawing" - bl_label = "Activate Drawing" - bl_options = {"REGISTER", "UNDO"} - bl_description = ( - "Activates the selected drawing view.\n\n" - + "ALT+CLICK to keep the viewport position.\n\n" - + "SHIFT+CLICK to load a quick preview of the drawing view." - ) - - drawing: bpy.props.IntProperty() - should_view_from_camera: bpy.props.BoolProperty(name="Should View From Camera", default=True, options={"SKIP_SAVE"}) - use_quick_preview: bpy.props.BoolProperty(name="Use Quick Preview", default=False, options={"SKIP_SAVE"}) - +class ActivateDrawingBase: def invoke(self, context, event): if event.type == "LEFTMOUSE" and event.alt: self.should_view_from_camera = False @@ -1817,6 +2023,54 @@ class ActivateDrawing(bpy.types.Operator): return {"FINISHED"} +class ActivateDrawing(bpy.types.Operator, ActivateDrawingBase): + bl_idname = "bim.activate_drawing" + bl_label = "Activate Drawing" + bl_options = {"REGISTER", "UNDO"} + bl_description = ( + "Activates the selected drawing view.\n\n" + + "ALT+CLICK to keep the viewport position.\n\n" + + "SHIFT+CLICK to load a quick preview of the drawing view" + ) + + drawing: bpy.props.IntProperty() + should_view_from_camera: bpy.props.BoolProperty(name="Should View From Camera", default=True, options={"SKIP_SAVE"}) + use_quick_preview: bpy.props.BoolProperty(name="Use Quick Preview", default=False, options={"SKIP_SAVE"}) + + @classmethod + def poll(cls, context): + props = context.scene.DocProperties + if not tool.Drawing.get_active_drawing_item(): + cls.poll_message_set("No drawing selected.") + return False + return True + + +class ActivateDrawingFromSheet(bpy.types.Operator, ActivateDrawingBase): + bl_idname = "bim.activate_drawing_from_sheet" + bl_label = "Activate Drawing" + bl_options = {"REGISTER", "UNDO"} + bl_description = ( + "Activates the selected drawing view.\n\n" + + "ALT+CLICK to keep the viewport position.\n\n" + + "SHIFT+CLICK to load a quick preview of the drawing view" + ) + + drawing: bpy.props.IntProperty() + should_view_from_camera: bpy.props.BoolProperty(name="Should View From Camera", default=True, options={"SKIP_SAVE"}) + use_quick_preview: bpy.props.BoolProperty(name="Use Quick Preview", default=False, options={"SKIP_SAVE"}) + + @classmethod + def poll(cls, context): + props = context.scene.DocProperties + active_sheet = props.sheets[props.active_sheet_index] + is_drawing_selected = active_sheet.reference_type == "DRAWING" + if not is_drawing_selected: + cls.poll_message_set("No drawing selected.") + return False + return True + + class SelectDocIfcFile(bpy.types.Operator): bl_idname = "bim.select_doc_ifc_file" bl_label = "Select Documentation IFC File" @@ -1856,6 +2110,14 @@ class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator): drawing: bpy.props.IntProperty() remove_all: bpy.props.BoolProperty(name="Remove All", default=False, options={"SKIP_SAVE"}) + @classmethod + def poll(cls, context): + props = context.scene.DocProperties + if not tool.Drawing.get_active_drawing_item(): + cls.poll_message_set("No drawing selected.") + return False + return True + def invoke(self, context, event): # removing all selected drawings on shift+click # make sure to use SKIP_SAVE on property, otherwise it might get stuck @@ -1866,7 +2128,9 @@ class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): if self.remove_all: drawings = [ - tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings if d.is_selected + tool.Ifc.get().by_id(d.ifc_definition_id) + for d in context.scene.DocProperties.drawings + if d.is_drawing and d.is_selected ] else: if not self.drawing: @@ -2192,6 +2456,7 @@ class RemoveSheet(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_sheet" bl_label = "Remove Sheet" bl_options = {"REGISTER", "UNDO"} + bl_description = "Remove currently selected sheet" sheet: bpy.props.IntProperty() def _execute(self, context): @@ -2260,11 +2525,15 @@ class BuildSchedule(bpy.types.Operator, tool.Ifc.Operator): class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_schedule_to_sheet" bl_label = "Add Schedule To Sheet" + bl_description = "Add the schedule selected in the\nSchedules list below to the sheet" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): props = context.scene.DocProperties + if not props.schedules: + cls.poll_message_set("No schedule selected.") + return False return props.schedules and props.sheets and context.scene.BIMProperties.data_dir def _execute(self, context): @@ -2299,7 +2568,14 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator): attributes = tool.Drawing.generate_reference_attributes( reference, Identification=str( - len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE")]) + 1 + len( + [ + r + for r in references + if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE", "REFERENCE") + ] + ) + + 1 ), Location=schedule_location, Description="SCHEDULE", @@ -2316,11 +2592,15 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator): class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_reference_to_sheet" bl_label = "Add Reference To Sheet" + bl_description = "Add the reference selected in the\nReferences list below to the sheet" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): props = context.scene.DocProperties + if not props.references: + cls.poll_message_set("No reference selected.") + return False return props.references and props.sheets and context.scene.BIMProperties.data_dir def _execute(self, context): @@ -2355,7 +2635,13 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator): attributes = tool.Drawing.generate_reference_attributes( reference, Identification=str( - len([r for r in references if tool.Drawing.get_reference_description(r) in ("DRAWING", "REFERENCE")]) + len( + [ + r + for r in references + if tool.Drawing.get_reference_description(r) in ("DRAWING", "SCHEDULE", "REFERENCE") + ] + ) + 1 ), Location=extref_location, @@ -2720,7 +3006,8 @@ class LoadSheets(bpy.types.Operator, tool.Ifc.Operator): class EditSheet(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.edit_sheet" - bl_label = "Edit Sheet" + bl_label = "Edit Sheet / Drawing" + bl_description = "Edit details of sheet or drawing" bl_options = {"REGISTER", "UNDO"} identification: bpy.props.StringProperty() name: bpy.props.StringProperty() @@ -3069,20 +3356,30 @@ class ConvertSVGToDXF(bpy.types.Operator): bl_label = "Convert SVG to DXF" bl_options = {"REGISTER", "UNDO"} view: bpy.props.StringProperty() - bl_description = "Convert current drawing's .svg to .dxf.\n\nSHIFT+CLICK to convert all selected drawings" + bl_description = "Convert selected drawing's .svg to .dxf.\n\nSHIFT+CLICK to convert all shown checked drawings" convert_all: bpy.props.BoolProperty(name="Convert All", default=False, options={"SKIP_SAVE"}) + @classmethod + def poll(cls, context): + props = context.scene.DocProperties + if not tool.Drawing.get_active_drawing_item(): + cls.poll_message_set("No drawing selected.") + return False + return True + def invoke(self, context, event): # convert all drawings on shift+click # make sure to use SKIP_SAVE on property, otherwise it might get stuck if event.type == "LEFTMOUSE" and event.shift: - self.open_all = True + self.convert_all = True return self.execute(context) def execute(self, context): if self.convert_all: drawings = [ - tool.Ifc.get().by_id(d.ifc_definition_id) for d in context.scene.DocProperties.drawings if d.is_selected + tool.Ifc.get().by_id(d.ifc_definition_id) + for d in context.scene.DocProperties.drawings + if d.is_drawing and d.is_selected ] else: drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)] diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index 710d1f3918..882e4b3165 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -290,6 +290,7 @@ class Sheet(PropertyGroup): identification: StringProperty(name="Identification") name: StringProperty(name="Name") is_sheet: BoolProperty(name="Is Sheet", default=False) + is_selected: BoolProperty(name="Is Selected", default=True) reference_type: StringProperty(name="Reference Type") is_expanded: BoolProperty(name="Is Expanded", default=False) diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py index 367ef612fc..58d1a990f2 100644 --- a/src/bonsai/bonsai/bim/module/drawing/ui.py +++ b/src/bonsai/bonsai/bim/module/drawing/ui.py @@ -92,11 +92,6 @@ class BIM_PT_camera(Panel): row = self.layout.row() row.prop(props, "dpi") - row = self.layout.row(align=True) - row.operator("bim.create_drawing", text="Create Drawing", icon="OUTPUT") - op = row.operator("bim.open_drawing", icon="URL", text="") - op.view = context.scene.camera.name.split("/")[1] - class BIM_PT_element_filters(Panel): bl_label = "Element Filters" @@ -252,43 +247,29 @@ class BIM_PT_drawings(Panel): if self.props.active_drawing_index < len(self.props.drawings): active_drawing = self.props.drawings[self.props.active_drawing_index] row = self.layout.row(align=True) - col = row.column() - col.alignment = "LEFT" - row2 = col.row(align=True) + row2 = row.row(align=True) row2.operator("bim.remove_drawing", icon="X", text="").drawing = active_drawing.ifc_definition_id - row2.operator("bim.duplicate_drawing", icon="COPYDOWN", text="").drawing = ( + row2.separator(factor=0.5, type="SPACE") + + row2.operator("bim.duplicate_drawing", icon="DUPLICATE", text="").drawing = ( active_drawing.ifc_definition_id ) - col = row.column() - col.alignment = "RIGHT" + row3 = row.row(align=True) + row3.alignment = "RIGHT" - convert_to_dxf = row.row(align=True) - op = convert_to_dxf.operator("bim.convert_svg_to_dxf", text="", icon="IMAGE_DATA") - op.view = active_drawing.name - convert_to_dxf.enabled = active_drawing.ifc_definition_id > 0 + row3.operator("bim.activate_drawing", icon="OUTLINER_OB_CAMERA", text="").drawing = ( + active_drawing.ifc_definition_id + ) + row3.operator("bim.activate_model", icon="VIEW3D", text="") - op = row.operator("bim.select_all_drawings", icon="SELECT_SUBTRACT", text="") + row3.separator(factor=0.5, type="SPACE") - open_drawing_button = row.row(align=True) - op = open_drawing_button.operator("bim.open_drawing", icon="URL", text="") - op.view = active_drawing.name - open_drawing_button.enabled = active_drawing.ifc_definition_id > 0 - - row.operator("bim.activate_model", icon="VIEW3D", text="") - - drawing_button = row.row(align=True) - op = drawing_button.operator("bim.activate_drawing", icon="OUTLINER_OB_CAMERA", text="") - op.drawing = active_drawing.ifc_definition_id - drawing_button.enabled = active_drawing.ifc_definition_id > 0 - - create_drawing_button = row.row(align=True) - create_drawing_button.operator("bim.create_drawing", text="", icon="OUTPUT") - create_drawing_button.enabled = active_drawing.ifc_definition_id > 0 - - # might need a different icon since the URL icon is already used by the open_drawing - row.operator("bim.open_documentation_web_ui", icon="URL", text="") + row3.operator("bim.select_all_drawings", icon="CHECKBOX_HLT", text="") + row3.operator("bim.create_drawing", text="", icon="OUTPUT") + row3.operator("bim.convert_svg_to_dxf", text="", icon="SEQ_PREVIEW").view = active_drawing.name + row3.operator("bim.open_drawing", icon="HIDE_OFF", text="").view = active_drawing.name self.layout.template_list( "BIM_UL_drawinglist", "", self.props, "drawings", self.props, "active_drawing_index" ) @@ -325,7 +306,7 @@ class BIM_PT_schedules(Panel): if not self.props.is_editing_schedules: row = self.layout.row(align=True) - row.label(text=f"{DocumentsData.data['total_schedules']} Schedules Found", icon="LONGDISPLAY") + row.label(text=f"{DocumentsData.data['total_schedules']} Schedules Found", icon="PRESET") row.operator("bim.load_schedules", text="", icon="IMPORT") return @@ -375,7 +356,7 @@ class BIM_PT_references(Panel): if not self.props.is_editing_references: row = self.layout.row(align=True) - row.label(text=f"{DocumentsData.data['total_references']} References Found", icon="OBJECT_HIDDEN") + row.label(text=f"{DocumentsData.data['total_references']} References Found", icon="IMAGE_REFERENCE") row.operator("bim.load_references", text="", icon="IMPORT") return @@ -429,7 +410,23 @@ class BIM_PT_sheets(Panel): if self.props.sheets and self.props.active_sheet_index < len(self.props.sheets): active_sheet = self.props.sheets[self.props.active_sheet_index] row = self.layout.row(align=True) - row.alignment = "RIGHT" + row2 = row.row(align=True) + if active_sheet.is_sheet: + row2.operator("bim.remove_sheet", icon="X", text="").sheet = active_sheet.ifc_definition_id + else: + row2.operator("bim.remove_drawing_from_sheet", icon="X", text="").reference = ( + active_sheet.ifc_definition_id + ) + + row2.separator(factor=0.5, type="SPACE") + + row2.operator("bim.duplicate_sheet", icon="DUPLICATE", text="").drawing = active_sheet.ifc_definition_id + row2.operator("bim.open_documentation_web_ui", icon="URL", text="") + + row3 = row.row(align=True) + row3.alignment = "RIGHT" + + op = row3.operator("bim.activate_drawing_from_sheet", icon="OUTLINER_OB_CAMERA", text="") if active_sheet.reference_type == "DRAWING": drawingnamesvg = active_sheet.name @@ -445,23 +442,21 @@ class BIM_PT_sheets(Panel): break if drawingid is not None: - drawing_button = row.row(align=True) - op = drawing_button.operator("bim.activate_drawing", icon="OUTLINER_OB_CAMERA", text="") op.drawing = drawingid - else: - print(f"No matching drawing ID found for {drawingname}") - row.operator("bim.edit_sheet", icon="GREASEPENCIL", text="") - row.operator("bim.open_sheet", icon="URL", text="") - row.operator("bim.add_drawing_to_sheet", icon="IMAGE_PLANE", text="") - row.operator("bim.add_schedule_to_sheet", icon="PRESET_NEW", text="") - row.operator("bim.add_reference_to_sheet", icon="IMAGE_REFERENCE", text="") - row.operator("bim.create_sheets", icon="FILE_REFRESH", text="") - if active_sheet.is_sheet: - row.operator("bim.remove_sheet", icon="X", text="").sheet = active_sheet.ifc_definition_id - else: - op = row.operator("bim.remove_drawing_from_sheet", icon="X", text="") - op.reference = active_sheet.ifc_definition_id + row3.separator(factor=0.5, type="SPACE") + + row3.operator("bim.edit_sheet", icon="GREASEPENCIL", text="") + row3.operator("bim.add_drawing_to_sheet", icon="IMAGE_PLANE", text="") + row3.operator("bim.add_schedule_to_sheet", icon="PRESET_NEW", text="") + row3.operator("bim.add_reference_to_sheet", icon="IMAGE_REFERENCE", text="") + row3.operator("bim.open_layout", icon="CURRENT_FILE", text="") + + row3.separator(factor=0.5, type="SPACE") + + row3.operator("bim.select_all_sheets", icon="CHECKBOX_HLT", text="") + row3.operator("bim.create_sheets", icon="OUTPUT", text="") + row3.operator("bim.open_sheet", icon="HIDE_OFF", text="") self.layout.template_list("BIM_UL_sheets", "", self.props, "sheets", self.props, "active_sheet_index") @@ -600,6 +595,13 @@ class BIM_UL_drawinglist(bpy.types.UIList): selected_icon = "CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT" row.prop(item, "is_selected", text="", icon=selected_icon, emboss=False) row.prop(item, "name", text="", emboss=False) + self.props = context.scene.DocProperties + if ( + self.props.drawings + and self.props.active_drawing_id + and item.ifc_definition_id == self.props.active_drawing_id + ): + row.label(text="", icon="OUTLINER_OB_CAMERA") else: if item.target_view == "PLAN_VIEW": icon = "UV_FACESEL" @@ -641,13 +643,16 @@ class BIM_UL_sheets(bpy.types.UIList): item.ifc_definition_id ) + selected_icon = "CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT" + row.prop(item, "is_selected", text="", icon=selected_icon, emboss=False) + row.label(text=f"{item.identification} - {item.name}") else: row.label(text="", icon="BLANK1") if item.reference_type == "DRAWING": row.label(text="", icon="IMAGE_DATA") elif item.reference_type == "SCHEDULE": - row.label(text="", icon="LONGDISPLAY") + row.label(text="", icon="PRESET") elif item.reference_type == "TITLEBLOCK": row.label(text="", icon="MENU_PANEL") elif item.reference_type == "REVISION": diff --git a/src/bonsai/bonsai/core/drawing.py b/src/bonsai/bonsai/core/drawing.py index 4e0c15a714..b9a0b99e6c 100644 --- a/src/bonsai/bonsai/core/drawing.py +++ b/src/bonsai/bonsai/core/drawing.py @@ -113,7 +113,7 @@ def regenerate_sheet(drawing: tool.Drawing, sheet: ifcopenshell.entity_instance) drawing.delete_file(path_layout) -def open_sheet(drawing: tool.Drawing, sheet: ifcopenshell.entity_instance) -> None: +def open_layout(drawing: tool.Drawing, sheet: ifcopenshell.entity_instance) -> None: drawing.open_layout_svg(drawing.get_document_uri(sheet, "LAYOUT")) diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 88ce914f5b..cdb6e91334 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -774,7 +774,9 @@ class Drawing(bonsai.core.tool.Drawing): def import_drawings(cls) -> None: props = bpy.context.scene.DocProperties expanded_target_views = {d.target_view for d in props.drawings if d.is_expanded} - current_drawings_selection = {d.ifc_definition_id: d.is_selected for d in props.drawings} + if not hasattr(cls, "drawing_selected_states"): + cls.drawing_selected_states = {} + cls.drawing_selected_states.update({d.ifc_definition_id: d.is_selected for d in props.drawings if d.is_drawing}) props.drawings.clear() drawings = [e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"] grouped_drawings = { @@ -801,7 +803,7 @@ class Drawing(bonsai.core.tool.Drawing): for drawing in sorted(drawings, key=lambda x: x.Name or "Unnamed"): new = props.drawings.add() new.name = drawing.Name or "Unnamed" - new.is_selected = current_drawings_selection.get(drawing.id(), True) + new.is_selected = cls.drawing_selected_states.setdefault(drawing.id(), True) new.is_drawing = True new.ifc_definition_id = drawing.id() # Last, to prevent unnecessary prop callbacks @@ -828,6 +830,9 @@ class Drawing(bonsai.core.tool.Drawing): def import_sheets(cls) -> None: props = bpy.context.scene.DocProperties expanded_sheets = {s.ifc_definition_id for s in props.sheets if s.is_expanded} + if not hasattr(cls, "sheet_selected_states"): + cls.sheet_selected_states = {} + cls.sheet_selected_states.update({s.ifc_definition_id: s.is_selected for s in props.sheets if s.is_sheet}) props.sheets.clear() sheets = [d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SHEET"] for sheet in sorted(sheets, key=lambda s: getattr(s, "Identification", getattr(s, "DocumentId", None))): @@ -840,6 +845,7 @@ class Drawing(bonsai.core.tool.Drawing): new.name = sheet.Name new.is_sheet = True new.is_expanded = sheet.id() in expanded_sheets + new.is_selected = cls.sheet_selected_states.setdefault(sheet.id(), True) if not new.is_expanded: continue From e74adcbba3ec86a9e831fc5bc83c092560f9253d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Wed, 13 Nov 2024 11:58:50 -0300 Subject: [PATCH 12/20] Improve f4cbd13 and add option for profile with voids. Fix rebase conflict --- .../bonsai/bim/module/profile/operator.py | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py index 9306efc7ef..ef62d8b604 100644 --- a/src/bonsai/bonsai/bim/module/profile/operator.py +++ b/src/bonsai/bonsai/bim/module/profile/operator.py @@ -26,6 +26,7 @@ import bonsai.core.profile as core from bonsai.bim.module.model.decorator import ProfileDecorator from bonsai.bim.module.profile.prop import generate_thumbnail_for_active_profile from bonsai.bim.module.profile.data import refresh +from bonsai.bim.module.geometry.helper import Helper class LoadProfiles(bpy.types.Operator): @@ -151,35 +152,36 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - obj_points = [] - obj = context.scene.BIMProfileProperties.object_to_profile - if obj: - if len(obj.data.polygons) != 1: - self.report( - {"WARNING"}, - "This mesh is invalid to create a profile. Select a flat mesh with no more then one face.", - ) - return - for v in obj.data.polygons[0].vertices: - vert = obj.data.vertices[v] - if vert.co.z > 0: - self.report( - {"WARNING"}, - "This mesh is invalid to create a profile. Select a flat mesh with all its vertices at z=0", - ) - return - obj_points.append((vert.co.x, vert.co.y)) - if obj_points: - obj_points.append(obj_points[0]) - obj_points = [ - (v[0] - obj_points[0][0], v[1] - obj_points[0][1]) for v in obj_points - ] # Make sure the first point is (0, 0) - props = context.scene.BIMProfileProperties profile_class = props.profile_classes if profile_class == "IfcArbitraryClosedProfileDef": - points = [(0, 0), (0.1, 0), (0.1, 0.1), (0, 0.1), (0, 0)] if not obj_points else obj_points - profile = ifcopenshell.api.run("profile.add_arbitrary_profile", tool.Ifc.get(), profile=points) + obj = props.object_to_profile + if obj: + if len(obj.data.polygons) == 0: + self.report({"WARNING"}, "This mesh is invalid to create a profile. Select a flat mesh with at least one face.") + props.object_to_profile = None + return + helper = Helper(tool.Ifc.get()) + indices = helper.auto_detect_arbitrary_profile_with_voids_extruded_area_solid(obj.data) + if not indices["inner_curves"]: + indices = helper.auto_detect_arbitrary_closed_profile_extruded_area_solid(obj.data) + props.object_to_profile = None + if not indices: + points = [(0, 0), (0.1, 0), (0.1, 0.1), (0, 0.1), (0, 0)] + profile = ifcopenshell.api.run("profile.add_arbitrary_profile", tool.Ifc.get(), profile=points) + else: + if "inner_curves" not in indices: + points = [(obj.data.vertices[i].co.x,obj.data.vertices[i].co.y) for i in indices["profile"]] + points.append(points[0]) + profile = ifcopenshell.api.run("profile.add_arbitrary_profile", tool.Ifc.get(), profile=points) + else: + outer_points = [(obj.data.vertices[i].co.x,obj.data.vertices[i].co.y) for i in indices["profile"]] + outer_points.append(outer_points[0]) + inner_points = [[(obj.data.vertices[i].co.x,obj.data.vertices[i].co.y) for i in curve] for curve in indices["inner_curves"]] + for curve in inner_points: + curve.append(curve[0]) + profile = ifcopenshell.api.run("profile.add_arbitrary_profile_with_voids", tool.Ifc.get(), outer_profile=outer_points, inner_profiles=inner_points) + else: profile = ifcopenshell.api.run("profile.add_parameterized_profile", tool.Ifc.get(), ifc_class=profile_class) tool.Profile.set_default_profile_attrs(profile) From 6a20ecb2a081e154e1a2a39b3ae3de128ad6945f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 14 Nov 2024 11:20:58 +0500 Subject: [PATCH 13/20] Fix #5746 --- src/bonsai/bonsai/bim/module/model/workspace.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 6dbedcb6fe..87443ababf 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -109,10 +109,16 @@ class BimTool(WorkSpaceTool): def draw_settings(cls, context, layout, ws_tool): if context.scene.BIMGeometryProperties.mode == "ITEM": EditItemUI.draw(context, layout) - elif context.active_object and tool.Ifc.get_entity(context.active_object): + elif ( + active_ifc_object := (context.active_object and tool.Ifc.get_entity(context.active_object)) + ) and context.selected_objects: EditObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) else: CreateObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) + # Show some UI for spatial elements that are unselectable by default. + if active_ifc_object: + EditObjectUI.layout = layout # Prevent .draw_modes from using old layout and crash. + EditObjectUI.draw_modes(context) class WallTool(BimTool): From a8d86000876a20108d330093e798c82ab696ea52 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 14 Nov 2024 14:12:28 +0500 Subject: [PATCH 14/20] calculate resource usage/work - add poll messages --- .../bonsai/bim/module/resource/operator.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/resource/operator.py b/src/bonsai/bonsai/bim/module/resource/operator.py index feff57030f..8ecef856e0 100644 --- a/src/bonsai/bonsai/bim/module/resource/operator.py +++ b/src/bonsai/bonsai/bim/module/resource/operator.py @@ -189,9 +189,13 @@ class CalculateResourceWork(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): active_resource = tool.Resource.get_highlighted_resource() - if active_resource: - if tool.Resource.get_productivity(active_resource, should_inherit=True): - return True + if not active_resource: + cls.poll_message_set("No resource is active.") + return False + if not tool.Resource.get_productivity(active_resource, should_inherit=True): + cls.poll_message_set("No productivity data for active resource.") + return False + return True def _execute(self, context): core.calculate_resource_work(tool.Ifc, tool.Resource, resource=tool.Ifc.get().by_id(self.resource)) @@ -415,12 +419,14 @@ class CalculateResourceUsage(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): active_resource = tool.Resource.get_highlighted_resource() - if active_resource: - if active_resource.Usage and active_resource.Usage.ScheduleWork: - task = tool.Resource.get_task_assignments(active_resource) - if task and tool.Sequence.has_duration(task): - return True - + if not active_resource: + cls.poll_message_set("No resource is active.") + return False + if active_resource.Usage and active_resource.Usage.ScheduleWork: + task = tool.Resource.get_task_assignments(active_resource) + if task and tool.Sequence.has_duration(task): + return True + cls.poll_message_set("No usage data for active resource.") return False def _execute(self, context): From 2be6afa4113265611ad9bceb2d4a8d449279c569 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 14 Nov 2024 14:14:34 +0500 Subject: [PATCH 15/20] Productivity data operators - remove confusing descriptions --- src/bonsai/bonsai/bim/module/resource/operator.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/resource/operator.py b/src/bonsai/bonsai/bim/module/resource/operator.py index 8ecef856e0..9280d8ce17 100644 --- a/src/bonsai/bonsai/bim/module/resource/operator.py +++ b/src/bonsai/bonsai/bim/module/resource/operator.py @@ -349,7 +349,6 @@ class ImportResources(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): class AddProductivityData(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.add_productivity_data" - bl_description = "Apply" bl_label = "Add Productivity" bl_options = {"REGISTER", "UNDO"} @@ -359,7 +358,6 @@ class AddProductivityData(bpy.types.Operator, tool.Ifc.Operator): class EditProductivityData(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.edit_productivity_data" - bl_description = "Apply" bl_label = "Edit Productivity" bl_options = {"REGISTER", "UNDO"} From 77bc70456301403031978687478983d163fccf82 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 14 Nov 2024 14:21:49 +0500 Subject: [PATCH 16/20] assign/unassign process - add descriptions --- src/bonsai/bonsai/bim/module/sequence/operator.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/sequence/operator.py b/src/bonsai/bonsai/bim/module/sequence/operator.py index 47c3ac8cd3..3478bddf1a 100644 --- a/src/bonsai/bonsai/bim/module/sequence/operator.py +++ b/src/bonsai/bonsai/bim/module/sequence/operator.py @@ -510,6 +510,10 @@ class AssignProcess(bpy.types.Operator, tool.Ifc.Operator): related_object_type: bpy.props.StringProperty() related_object: bpy.props.IntProperty() + @classmethod + def description(cls, context, properties): + return f"Assign selected {properties.related_object_type} to the selected task" + def _execute(self, context): if self.related_object_type == "RESOURCE": core.assign_resource(tool.Ifc, tool.Sequence, tool.Resource, task=tool.Ifc.get().by_id(self.task)) @@ -537,6 +541,10 @@ class UnassignProcess(bpy.types.Operator, tool.Ifc.Operator): related_object: bpy.props.IntProperty() resource: bpy.props.IntProperty() + @classmethod + def description(cls, context, properties): + return f"Unassign selected {properties.related_object_type} from the selected task" + def _execute(self, context): if self.related_object_type == "RESOURCE": core.unassign_resource( From 1f21e9688708da17b0bf4c3c14bef00cff26aed8 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 14 Nov 2024 15:21:23 +0500 Subject: [PATCH 17/20] resource.remove_resource - fix bug not removing orphaned ifcrelnests --- .../api/resource/remove_resource.py | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/remove_resource.py b/src/ifcopenshell-python/ifcopenshell/api/resource/remove_resource.py index b75e1f908c..6c061a532d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/remove_resource.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/remove_resource.py @@ -36,22 +36,25 @@ def remove_resource(file: ifcopenshell.file, resource: ifcopenshell.entity_insta """ settings = {"resource": resource} + def remove_consider_history(root: ifcopenshell.entity_instance) -> None: + history = root.OwnerHistory + file.remove(root) + if history: + ifcopenshell.util.element.remove_deep2(file, history) + # TODO: review deep purge for inverse in file.get_inverse(settings["resource"]): if inverse.is_a("IfcRelNests"): if inverse.RelatingObject == settings["resource"]: for related_object in inverse.RelatedObjects: ifcopenshell.api.resource.remove_resource(file, resource=related_object) - history = inverse.OwnerHistory - file.remove(inverse) - if history: - ifcopenshell.util.element.remove_deep2(file, history) + remove_consider_history(inverse) + elif inverse.RelatedObjects == (resource,): + remove_consider_history(inverse) + elif inverse.is_a("IfcRelAssignsToControl"): if len(inverse.RelatedObjects) == 1: - history = inverse.OwnerHistory - file.remove(inverse) - if history: - ifcopenshell.util.element.remove_deep2(file, history) + remove_consider_history(inverse) else: related_objects = list(inverse.RelatedObjects) related_objects.remove(settings["resource"]) @@ -63,16 +66,10 @@ def remove_resource(file: ifcopenshell.file, resource: ifcopenshell.entity_insta file, related_object=related_object, relating_resource=settings["resource"] ) elif inverse.RelatedObjects == tuple(settings["resource"]): - history = inverse.OwnerHistory - file.remove(inverse) - if history: - ifcopenshell.util.element.remove_deep2(file, history) + remove_consider_history(inverse) # Usage was added in IFC4. if usage := getattr(settings["resource"], "Usage", None): file.remove(usage) if settings["resource"].BaseQuantity: ifcopenshell.api.resource.remove_resource_quantity(file, resource=settings["resource"]) - history = settings["resource"].OwnerHistory - file.remove(settings["resource"]) - if history: - ifcopenshell.util.element.remove_deep2(file, history) + remove_consider_history(resource) From 6c26403b9c7f1d18a0ed0c20917cd5312b29cebd Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 14 Nov 2024 15:27:51 +0500 Subject: [PATCH 18/20] resource.remove_resource - fix bug removing IfcRelAssignsToResource tuple(resource) was resulting in (GlobalId, OwnerHistory, Name, ...) instead of (resource,), so the check never passed and orphaned IfcRelAssignsToResource was never removed. --- .../ifcopenshell/api/resource/remove_resource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/remove_resource.py b/src/ifcopenshell-python/ifcopenshell/api/resource/remove_resource.py index 6c061a532d..b9c53c54dc 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/remove_resource.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/remove_resource.py @@ -65,7 +65,7 @@ def remove_resource(file: ifcopenshell.file, resource: ifcopenshell.entity_insta ifcopenshell.api.resource.unassign_resource( file, related_object=related_object, relating_resource=settings["resource"] ) - elif inverse.RelatedObjects == tuple(settings["resource"]): + elif inverse.RelatedObjects == (resource,): remove_consider_history(inverse) # Usage was added in IFC4. if usage := getattr(settings["resource"], "Usage", None): From 9f49cbe0c9429db9bd76a84a503508ebc76d242c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 14 Nov 2024 16:18:09 +0500 Subject: [PATCH 19/20] bim.calculate_resource_quantity #5744 Added operator to calculate resource quantity based on the output products and the quantity Name. See video demo - https://imgchest.com/p/9rydjlbmnyk --- src/bonsai/bonsai/bim/module/resource/__init__.py | 1 + src/bonsai/bonsai/bim/module/resource/operator.py | 12 ++++++++++++ src/bonsai/bonsai/bim/module/resource/ui.py | 3 +++ src/bonsai/bonsai/core/resource.py | 4 ++++ src/bonsai/bonsai/tool/resource.py | 6 ++++++ .../api/cost/calculate_cost_item_resource_value.py | 1 + 6 files changed, 27 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/resource/__init__.py b/src/bonsai/bonsai/bim/module/resource/__init__.py index 8d5c4e26fd..002952b3c1 100644 --- a/src/bonsai/bonsai/bim/module/resource/__init__.py +++ b/src/bonsai/bonsai/bim/module/resource/__init__.py @@ -25,6 +25,7 @@ classes = ( operator.AddProductivityData, operator.AssignResource, operator.CalculateResourceWork, + operator.CalculateResourceQuantity, operator.ConstrainResourceWork, operator.ContractResource, operator.DisableEditingResource, diff --git a/src/bonsai/bonsai/bim/module/resource/operator.py b/src/bonsai/bonsai/bim/module/resource/operator.py index 9280d8ce17..9d33c2d79e 100644 --- a/src/bonsai/bonsai/bim/module/resource/operator.py +++ b/src/bonsai/bonsai/bim/module/resource/operator.py @@ -429,3 +429,15 @@ class CalculateResourceUsage(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.calculate_resource_usage(tool.Ifc, tool.Resource, resource=tool.Resource.get_highlighted_resource()) + + +class CalculateResourceQuantity(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.calculate_resource_quantity" + bl_label = "Calculate Resource Quantity" + bl_description = "Calcule resource quantity based on the same name quantities from the output products" + bl_options = {"REGISTER", "UNDO"} + resource: bpy.props.IntProperty(name="Resource ID") + + def _execute(self, context): + core.calculate_resource_quantity(tool.Resource, resource=tool.Ifc.get().by_id(self.resource)) + return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/resource/ui.py b/src/bonsai/bonsai/bim/module/resource/ui.py index fe5d85acd7..1560cb027f 100644 --- a/src/bonsai/bonsai/bim/module/resource/ui.py +++ b/src/bonsai/bonsai/bim/module/resource/ui.py @@ -258,6 +258,9 @@ class BIM_PT_resources(Panel): else: op = row.operator("bim.enable_editing_resource_quantity", text="", icon="GREASEPENCIL") op.resource = self.props.active_resource_id + if resource["type"] == "IfcConstructionMaterialResource": + op = row.operator("bim.calculate_resource_quantity", text="", icon="FILE_REFRESH") + op.resource = self.props.active_resource_id op = row.operator("bim.remove_resource_quantity", text="", icon="X") op.resource = self.props.active_resource_id diff --git a/src/bonsai/bonsai/core/resource.py b/src/bonsai/bonsai/core/resource.py index b53fcc131b..12c6dc2b14 100644 --- a/src/bonsai/bonsai/core/resource.py +++ b/src/bonsai/bonsai/core/resource.py @@ -262,3 +262,7 @@ def calculate_resource_usage( ) -> None: ifc.run("resource.calculate_resource_usage", resource=resource) resource_tool.load_resources() + + +def calculate_resource_quantity(resource_tool: tool.Resource, resource: ifcopenshell.entity_instance) -> None: + resource_tool.calculate_resource_quantity(resource) diff --git a/src/bonsai/bonsai/tool/resource.py b/src/bonsai/bonsai/tool/resource.py index a1513fd948..b9bc5938f4 100644 --- a/src/bonsai/bonsai/tool/resource.py +++ b/src/bonsai/bonsai/tool/resource.py @@ -443,6 +443,12 @@ class Resource(bonsai.core.tool.Resource): def run_calculate_resource_usage(cls, resource: ifcopenshell.entity_instance) -> None: tool.Ifc.run("resource.calculate_resource_usage", resource=resource) + @classmethod + def calculate_resource_quantity(cls, resource: ifcopenshell.entity_instance) -> None: + quantity: ifcopenshell.entity_instance = resource.BaseQuantity + quantity_from_products = ifcopenshell.util.resource.get_total_quantity_produced(resource, quantity.Name) + quantity[3] = quantity_from_products + @classmethod def get_task_assignments(cls, resource: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: return ifcopenshell.util.resource.get_task_assignments(resource) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py index 3371d3e200..fcdd4084ce 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py @@ -101,6 +101,7 @@ def calculate_cost_item_resource_value(file: ifcopenshell.file, cost_item: ifcop for resource in resources: cost, unit = ifcopenshell.util.resource.get_cost(resource) + # TODO: cost is never None because get_cost always returns a float. if cost is None: # Concept to standardise - Not defined in schema, but this makes manual scheduling of resources 10x faster and less duplicate data. parent_cost = ifcopenshell.util.resource.get_parent_cost(resource) From ceb49bac0ee694d0cb5e62ed655146eb934afb74 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 14 Nov 2024 16:32:00 +0500 Subject: [PATCH 20/20] Note in UI derived schedule work explicitly There is "*" mark but since we don't have a documentation for this UI section it may be hard for new users to guess what it means. --- src/bonsai/bonsai/bim/module/resource/ui.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/resource/ui.py b/src/bonsai/bonsai/bim/module/resource/ui.py index 1560cb027f..5899e21275 100644 --- a/src/bonsai/bonsai/bim/module/resource/ui.py +++ b/src/bonsai/bonsai/bim/module/resource/ui.py @@ -121,10 +121,11 @@ class BIM_PT_resources(Panel): col3.ui_units_x = 2 row1_col1 = col1.row() - row1_col1.label(text="Schedule Work") - row1col2 = col2.row() schedule_work = resource.get("ScheduleWork", None) derived_schedule_work = resource.get("DerivedScheduleWork", None) + derived_str = "" if schedule_work else " (Derived)" + row1_col1.label(text=f"Schedule Work {derived_str}") + row1col2 = col2.row() row1col2.label( text="{}".format(schedule_work) if schedule_work else "{} h*".format(derived_schedule_work), icon="TIME",