From 67c055579c31b58281c82f8c14609e3960d3b733 Mon Sep 17 00:00:00 2001 From: Gorgious56 Date: Mon, 23 Aug 2021 00:07:29 +0200 Subject: [PATCH] Various tweaks and fixes (#1683) * Use operator's poll method to ensure correct execution * Add utilities to get active collection item * Minor fixes zombie code list comprehensions typos cleanup tooltips, descriptions ensure file extension in folder operator use custom UI List instead of generic one ensure active object stays the same after operator execution use passed context instead of bpy * Jump through hoops to not display empty dynamic enums * Group mesh calculation methods in helper module If not using bmesh, the polygon selection status is updated only when getting out of Edit mode. Using bmesh we can get the values in realtime * Remove unused arg name in dosctring --- .../bim/module/bimtester/operator.py | 5 ++ .../blenderbim/bim/module/clash/operator.py | 36 ++++---- .../blenderbim/bim/module/clash/prop.py | 12 +++ .../blenderbim/bim/module/clash/ui.py | 2 +- .../blenderbim/bim/module/debug/operator.py | 2 +- .../blenderbim/bim/module/drawing/helper.py | 2 +- .../blenderbim/bim/module/drawing/operator.py | 67 +++++++++------ .../blenderbim/bim/module/drawing/prop.py | 11 +++ .../blenderbim/bim/module/drawing/ui.py | 8 +- .../bim/module/georeference/operator.py | 24 ++++++ .../bim/module/georeference/prop.py | 4 +- .../blenderbim/bim/module/model/product.py | 10 +++ .../blenderbim/bim/module/model/ui.py | 22 +++-- .../blenderbim/bim/module/model/wall.py | 37 +++++--- .../blenderbim/bim/module/model/workspace.py | 85 ++++++++++++------- .../blenderbim/bim/module/qto/helper.py | 47 +++++++--- .../blenderbim/bim/module/qto/operator.py | 58 ++++++------- src/blenderbim/blenderbim/bim/operator.py | 28 ++++-- src/ifcpatch/ifcpatch/__init__.py | 3 +- 19 files changed, 315 insertions(+), 148 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/bimtester/operator.py b/src/blenderbim/blenderbim/bim/module/bimtester/operator.py index 5e71a956cd..7bf9905313 100644 --- a/src/blenderbim/blenderbim/bim/module/bimtester/operator.py +++ b/src/blenderbim/blenderbim/bim/module/bimtester/operator.py @@ -39,6 +39,11 @@ class ExecuteBIMTester(bpy.types.Operator): bl_idname = "bim.execute_bim_tester" bl_label = "Execute BIMTester" + @classmethod + def poll(cls, context): + props = context.scene.BimTesterProperties + return props.ifc_file and props.feature + def execute(self, context): props = context.scene.BimTesterProperties diff --git a/src/blenderbim/blenderbim/bim/module/clash/operator.py b/src/blenderbim/blenderbim/bim/module/clash/operator.py index b924905483..90223e46d4 100644 --- a/src/blenderbim/blenderbim/bim/module/clash/operator.py +++ b/src/blenderbim/blenderbim/bim/module/clash/operator.py @@ -25,6 +25,7 @@ import logging import numpy as np from mathutils import Matrix from math import radians +from blenderbim.bim.ifc import IfcStore class ExportClashSets(bpy.types.Operator): @@ -124,7 +125,7 @@ class AddClashSource(bpy.types.Operator): group: bpy.props.StringProperty() def execute(self, context): - clash_set = context.scene.BIMClashProperties.clash_sets[context.scene.BIMClashProperties.active_clash_set_index] + clash_set = context.scene.BIMClashProperties.active_clash_set source = getattr(clash_set, self.group).add() return {"FINISHED"} @@ -137,7 +138,7 @@ class RemoveClashSource(bpy.types.Operator): group: bpy.props.StringProperty() def execute(self, context): - clash_set = context.scene.BIMClashProperties.clash_sets[context.scene.BIMClashProperties.active_clash_set_index] + clash_set = context.scene.BIMClashProperties.active_clash_set getattr(clash_set, self.group).remove(self.index) return {"FINISHED"} @@ -152,7 +153,7 @@ class SelectClashSource(bpy.types.Operator): group: bpy.props.StringProperty() def execute(self, context): - clash_set = context.scene.BIMClashProperties.clash_sets[context.scene.BIMClashProperties.active_clash_set_index] + clash_set = context.scene.BIMClashProperties.active_clash_set getattr(clash_set, self.group)[self.index].name = self.filepath return {"FINISHED"} @@ -276,9 +277,7 @@ class SelectIfcClashResults(bpy.types.Operator): self.filepath = bpy.path.ensure_ext(self.filepath, ".json") with open(self.filepath) as f: clash_sets = json.load(f) - clash_set_name = context.scene.BIMClashProperties.clash_sets[ - context.scene.BIMClashProperties.active_clash_set_index - ].name + clash_set_name = context.scene.BIMClashProperties.active_clash_set.name global_ids = [] for clash_set in clash_sets: if clash_set["name"] != clash_set_name: @@ -303,6 +302,10 @@ class SmartClashGroup(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") + @classmethod + def poll(cls, context): + return context.scene.BIMClashProperties.clash_results_path + def execute(self, context): import ifcclash @@ -326,9 +329,7 @@ class SmartClashGroup(bpy.types.Operator): with open(save_path, "w") as f: f.write(json.dumps(smart_grouped_clashes)) - clash_set_name = context.scene.BIMClashProperties.clash_sets[ - context.scene.BIMClashProperties.active_clash_set_index - ].name + clash_set_name = context.scene.BIMClashProperties.active_clash_set.name # Reset the list of smart_clash_groups for the UI context.scene.BIMClashProperties.smart_clash_groups.clear() @@ -355,12 +356,14 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator): bl_label = "Load Smart Groups for Active Clash Set" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return context.scene.BIMClashProperties.active_clash_set + def execute(self, context): smart_groups_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json") - clash_set_name = context.scene.BIMClashProperties.clash_sets[ - context.scene.BIMClashProperties.active_clash_set_index - ].name + clash_set_name = context.scene.BIMClashProperties.active_clash_set.name with open(smart_groups_path) as f: smart_grouped_clashes = json.load(f) @@ -389,11 +392,14 @@ class SelectSmartGroup(bpy.types.Operator): bl_label = "Select Smart Group" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return IfcStore.get_file() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group + def execute(self, context): + self.file = IfcStore.get_file() # Select smart group in view - selected_smart_group = context.scene.BIMClashProperties.smart_clash_groups[ - context.scene.BIMCLashProperties.active_smart_group_index - ] + selected_smart_group = context.scene.BIMClashProperties.active_smart_group # print(selected_smart_group.number) for obj in context.visible_objects: diff --git a/src/blenderbim/blenderbim/bim/module/clash/prop.py b/src/blenderbim/blenderbim/bim/module/clash/prop.py index 5ed121757f..e721db86ce 100644 --- a/src/blenderbim/blenderbim/bim/module/clash/prop.py +++ b/src/blenderbim/blenderbim/bim/module/clash/prop.py @@ -69,3 +69,15 @@ class BIMClashProperties(PropertyGroup): smart_clash_grouping_max_distance: IntProperty( name="Smart Clash Grouping Max Distance", default=3, soft_min=1, soft_max=10 ) + + @property + def active_clash_set(self): + if not self.clash_sets: + return None + return self.clash_sets[self.active_clash_set_index] + + @property + def active_smart_group(self): + if not self.smart_clash_groups: + return None + return self.smart_clash_groups[self.active_smart_group_index] diff --git a/src/blenderbim/blenderbim/bim/module/clash/ui.py b/src/blenderbim/blenderbim/bim/module/clash/ui.py index 2080a0c723..9b6d021a42 100644 --- a/src/blenderbim/blenderbim/bim/module/clash/ui.py +++ b/src/blenderbim/blenderbim/bim/module/clash/ui.py @@ -57,7 +57,7 @@ class BIM_PT_ifcclash(Panel): layout.template_list("BIM_UL_clash_sets", "", props, "clash_sets", props, "active_clash_set_index") if props.active_clash_set_index < len(props.clash_sets): - clash_set = props.clash_sets[props.active_clash_set_index] + clash_set = props.active_clash_set row = layout.row(align=True) row.prop(clash_set, "name") diff --git a/src/blenderbim/blenderbim/bim/module/debug/operator.py b/src/blenderbim/blenderbim/bim/module/debug/operator.py index a70f5e8afb..7ef5b31b99 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/operator.py +++ b/src/blenderbim/blenderbim/bim/module/debug/operator.py @@ -165,7 +165,7 @@ class SelectHighPolygonMeshes(bpy.types.Operator): def execute(self, context): [o.select_set(True) for o in context.view_layer.objects - if o.type == 'MESH' + if o.type == "MESH" and len(o.data.polygons) > context.scene.BIMDebugProperties.number_of_polygons] return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/drawing/helper.py b/src/blenderbim/blenderbim/bim/module/drawing/helper.py index eab5fbf9b0..e7a1c9ad19 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/helper.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/helper.py @@ -263,7 +263,7 @@ def get_active_drawing(scene): if props.active_drawing_index is None or len(props.drawings) == 0: return None, None try: - drawing = props.drawings[props.active_drawing_index] + drawing = props.active_drawing return scene.collection.children["Views"].children[f"IfcGroup/{drawing.name}"], drawing.camera except (KeyError, IndexError): raise RuntimeError("missing drawing collection") diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index eca290ca9c..d78db7b340 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -60,6 +60,10 @@ class AddDrawing(bpy.types.Operator): bl_label = "Add Drawing" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return IfcStore.get_file() + def execute(self, context): return IfcStore.execute_ifc_operator(self, context) @@ -502,12 +506,14 @@ class AddAnnotation(bpy.types.Operator): obj_name: bpy.props.StringProperty() data_type: bpy.props.StringProperty() + @classmethod + def poll(cls, context): + return IfcStore.get_file() and context.scene.camera + def execute(self, context): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): - if not context.scene.camera: - return {"FINISHED"} subcontext = ifcopenshell.util.representation.get_context( IfcStore.get_file(), "Plan", "Annotation", context.scene.camera.data.BIMCameraProperties.target_view ) @@ -563,7 +569,7 @@ class OpenSheet(bpy.types.Operator): open_with_user_command( context.preferences.addons["blenderbim"].preferences.svg_command, os.path.join( - context.scene.BIMProperties.data_dir, "sheets", props.sheets[props.active_sheet_index].name + ".svg" + context.scene.BIMProperties.data_dir, "sheets", props.active_sheet.name + ".svg" ), ) return {"FINISHED"} @@ -581,9 +587,9 @@ class AddDrawingToSheet(bpy.types.Operator): sheet_builder.data_dir = context.scene.BIMProperties.data_dir try: sheet_builder.add_drawing( - props.drawings[props.active_drawing_index].name, props.sheets[props.active_sheet_index].name + props.drawings.active_drawing.name, props.active_sheet.name ) - except: + except FileNotFoundError: self.report({"ERROR"}, "Drawings need to be created before being added to a sheet") return {"FINISHED"} @@ -596,7 +602,7 @@ class CreateSheets(bpy.types.Operator): def execute(self, context): scene = context.scene props = scene.DocProperties - name = props.sheets[props.active_sheet_index].name + name = props.active_sheet.name sheet_builder = sheeter.SheetBuilder() sheet_builder.data_dir = scene.BIMProperties.data_dir sheet_builder.build(name) @@ -650,18 +656,23 @@ class OpenView(bpy.types.Operator): class OpenViewCamera(bpy.types.Operator): + """Select this drawing's camera object and expand its drawing properties""" bl_idname = "bim.open_view_camera" bl_label = "Open View Camera" bl_options = {"REGISTER", "UNDO"} view_name: bpy.props.StringProperty() + @classmethod + def poll(cls, context): + return bpy.context.object.mode == "OBJECT" + def execute(self, context): - new_drawing_index = context.scene.DocProperties.drawings.find(self.view_name) - context.scene.DocProperties.active_drawing_index = new_drawing_index - drawing = context.scene.DocProperties.drawings[new_drawing_index] - context.view_layer.objects.active = drawing.camera + doc_props = context.scene.DocProperties + doc_props.active_drawing_index = doc_props.drawings.find(self.view_name) + drawing = doc_props.active_drawing bpy.ops.object.select_all(action="DESELECT") drawing.camera.select_set(True) + context.view_layer.objects.active = drawing.camera for area in context.screen.areas: if area.ui_type == "PROPERTIES": for space in area.spaces: @@ -712,6 +723,7 @@ class SelectDocIfcFile(bpy.types.Operator): bl_idname = "bim.select_doc_ifc_file" bl_label = "Select Documentation IFC File" bl_options = {"REGISTER", "UNDO"} + filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") index: bpy.props.IntProperty() @@ -1059,7 +1071,7 @@ class SelectScheduleFile(bpy.types.Operator): def execute(self, context): props = context.scene.DocProperties - props.schedules[props.active_schedule_index].file = self.filepath + props.active_schedule.file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -1071,9 +1083,13 @@ class BuildSchedule(bpy.types.Operator): bl_idname = "bim.build_schedule" bl_label = "Build Schedule" + @classmethod + def poll(cls, context): + return context.scene.DocProperties.active_schedule.file + def execute(self, context): props = context.scene.DocProperties - schedule = props.schedules[props.active_schedule_index] + schedule = props.active_schedule schedule_creator = scheduler.Scheduler() outfile = os.path.join(context.scene.BIMProperties.data_dir, "schedules", schedule.name + ".svg") schedule_creator.schedule(schedule.file, outfile) @@ -1092,7 +1108,7 @@ class AddScheduleToSheet(bpy.types.Operator): sheet_builder = sheeter.SheetBuilder() sheet_builder.data_dir = context.scene.BIMProperties.data_dir sheet_builder.add_schedule( - props.schedules[props.active_schedule_index].name, props.sheets[props.active_sheet_index].name + props.active_schedule.name, props.active_sheet.name ) return {"FINISHED"} @@ -1143,13 +1159,12 @@ class CleanWireframes(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - objects = context.scene.objects if context.selected_objects: objects = context.selected_objects - for obj in objects: - if not isinstance(obj.data, bpy.types.Mesh): - continue - if "EDGE_SPLIT" not in [m.type for m in obj.modifiers]: + else: + objects = context.scene.objects + for obj in (o for o in objects if o.type == "MESH"): + if "EDGE_SPLIT" not in (m.type for m in obj.modifiers): obj.modifiers.new("EdgeSplit", "EDGE_SPLIT") return {"FINISHED"} @@ -1159,11 +1174,13 @@ class CopyGrid(bpy.types.Operator): bl_label = "Add Grid" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return helper.get_active_drawing(context.scene)[0] is not None + def execute(self, context): proj_coll = helper.get_project_collection(context.scene) view_coll, camera = helper.get_active_drawing(context.scene) - if view_coll is None: - return {"CANCELLED"} is_ortho = camera.data.type == "ORTHO" bounds = helper.ortho_view_frame(camera.data) if is_ortho else None clipping = is_ortho and camera.data.BIMCameraProperties.target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") @@ -1240,13 +1257,15 @@ class AddSectionsAnnotations(bpy.types.Operator): bl_label = "Add Sections" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + camera = helper.get_active_drawing(context.scene)[1] + return camera and camera.data.type == "ORTHO" + def execute(self, context): scene = context.scene view_coll, camera = helper.get_active_drawing(scene) - is_ortho = camera.data.type == "ORTHO" - if not is_ortho: - return {"CANCELLED"} - bounds = helper.ortho_view_frame(camera.data) if is_ortho else None + bounds = helper.ortho_view_frame(camera.data) drawings = [ d diff --git a/src/blenderbim/blenderbim/bim/module/drawing/prop.py b/src/blenderbim/blenderbim/bim/module/drawing/prop.py index 210ad986e8..150d800e32 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/prop.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/prop.py @@ -269,6 +269,17 @@ class DocProperties(PropertyGroup): name="Decorations Colour", subtype="COLOR", default=(1, 0, 0, 1), min=0.0, max=1.0, size=4 ) + @property + def active_schedule(self): + return self.schedules[self.active_schedule_index] + + @property + def active_drawing(self): + return self.drawings[self.active_drawing_index] + + @property + def active_sheet(self): + return self.sheets[self.active_sheet_index] class BIMCameraProperties(PropertyGroup): view_name: StringProperty(name="View Name") diff --git a/src/blenderbim/blenderbim/bim/module/drawing/ui.py b/src/blenderbim/blenderbim/bim/module/drawing/ui.py index 464f3713e9..2662e885ec 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/ui.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/ui.py @@ -170,9 +170,9 @@ class BIM_PT_drawings(Panel): if props.drawings: if props.active_drawing_index < len(props.drawings): op = row.operator("bim.open_view", icon="URL", text="") - op.view = props.drawings[props.active_drawing_index].name + op.view = props.active_drawing.name row.operator("bim.remove_drawing", icon="X", text="").index = props.active_drawing_index - layout.template_list("BIM_UL_generic", "", props, "drawings", props, "active_drawing_index") + layout.template_list("BIM_UL_drawinglist", "", props, "drawings", props, "active_drawing_index") row = layout.row() row.operator("bim.add_ifc_file") @@ -206,7 +206,7 @@ class BIM_PT_schedules(Panel): layout.template_list("BIM_UL_generic", "", props, "schedules", props, "active_schedule_index") row = layout.row() - row.prop(props.schedules[props.active_schedule_index], "file") + row.prop(props.active_schedule, "file") row.operator("bim.select_schedule_file", icon="FILE_FOLDER", text="") @@ -343,7 +343,7 @@ class BIM_PT_annotation_utilities(Panel): if props.drawings: if props.active_drawing_index < len(props.drawings): op = row.operator("bim.open_view", icon="URL", text="") - op.view = props.drawings[props.active_drawing_index].name + op.view = props.active_drawing.name row.operator("bim.remove_drawing", icon="X", text="").index = props.active_drawing_index layout.template_list("BIM_UL_drawinglist", "", props, "drawings", props, "active_drawing_index") diff --git a/src/blenderbim/blenderbim/bim/module/georeference/operator.py b/src/blenderbim/blenderbim/bim/module/georeference/operator.py index b9066ab2ac..2026de99e4 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/operator.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/operator.py @@ -237,6 +237,12 @@ class ConvertLocalToGlobal(bpy.types.Operator): bl_label = "Convert Local To Global" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + file = IfcStore.get_file() + props = context.scene.BIMGeoreferenceProperties + return file and props.coordinate_input.count(",") == 2 + def execute(self, context): if not Data.is_loaded: Data.load(IfcStore.get_file()) @@ -284,6 +290,13 @@ class ConvertGlobalToLocal(bpy.types.Operator): bl_label = "Convert Global To Local" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + file = IfcStore.get_file() + props = context.scene.BIMGeoreferenceProperties + return file and file.by_type("IfcUnitAssignment") \ + and props.coordinate_input.count(",") == 2 + def execute(self, context): if not Data.is_loaded: Data.load(IfcStore.get_file()) @@ -330,6 +343,11 @@ class GetCursorLocation(bpy.types.Operator): bl_label = "Get Cursor Location" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + file = IfcStore.get_file() + return file and file.by_type("IfcUnitAssignment") + def execute(self, context): props = context.scene.BIMGeoreferenceProperties scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file()) @@ -343,6 +361,12 @@ class SetCursorLocation(bpy.types.Operator): bl_label = "Set Cursor Location" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + file = IfcStore.get_file() + props = context.scene.BIMGeoreferenceProperties + return file and file.by_type("IfcUnitAssignment") and props.coordinate_output.count(",") == 2 + def execute(self, context): props = context.scene.BIMGeoreferenceProperties scale = ifcopenshell.util.unit.calculate_unit_scale(IfcStore.get_file()) diff --git a/src/blenderbim/blenderbim/bim/module/georeference/prop.py b/src/blenderbim/blenderbim/bim/module/georeference/prop.py index f1cda83e62..900a1bdaaa 100644 --- a/src/blenderbim/blenderbim/bim/module/georeference/prop.py +++ b/src/blenderbim/blenderbim/bim/module/georeference/prop.py @@ -52,8 +52,8 @@ class BIMGeoreferenceProperties(PropertyGroup): default="foot", ) is_map_unit_null: BoolProperty(name="Is Map Unit Null") - coordinate_input: StringProperty(name="Coordinate Input") - coordinate_output: StringProperty(name="Coordinate Output") + coordinate_input: StringProperty(name="Coordinate Input", description="Formatted \"x,y,z\" (without quotes)") + coordinate_output: StringProperty(name="Coordinate Output", description="Formatted \"x,y,z\" (without quotes)") has_blender_offset: BoolProperty(name="Has Blender Offset") blender_eastings: StringProperty(name="Blender Eastings", default="0") blender_northings: StringProperty(name="Blender Northings", default="0") diff --git a/src/blenderbim/blenderbim/bim/module/model/product.py b/src/blenderbim/blenderbim/bim/module/model/product.py index bb3c92e584..c03062d007 100644 --- a/src/blenderbim/blenderbim/bim/module/model/product.py +++ b/src/blenderbim/blenderbim/bim/module/model/product.py @@ -129,6 +129,10 @@ class AddTypeInstance(bpy.types.Operator): else: if collection_obj and collection_obj.BIMObjectProperties.ifc_definition_id: obj.location[2] = collection_obj.location[2] - min([v[2] for v in obj.bound_box]) + + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + context.view_layer.objects.active = obj return {"FINISHED"} def generate_layered_element(self, ifc_class, relating_type): @@ -205,8 +209,14 @@ class DynamicallyVoidProduct(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() + @classmethod + def poll(cls, context): + return IfcStore.get_file() + def execute(self, context): obj = bpy.data.objects.get(self.obj) + if obj is None: + return {"FINISHED"} product = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) if not product.HasOpenings: return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/ui.py b/src/blenderbim/blenderbim/bim/module/model/ui.py index dbec2fe8c7..00a34969bf 100644 --- a/src/blenderbim/blenderbim/bim/module/model/ui.py +++ b/src/blenderbim/blenderbim/bim/module/model/ui.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . +import blenderbim.bim.module.type.prop as type_prop from bpy.types import Panel from blenderbim.bim.ifc import IfcStore @@ -35,9 +36,21 @@ class BIM_PT_authoring(Panel): def draw(self, context): tprops = context.scene.BIMTypeProperties col = self.layout.column(align=True) - col.prop(tprops, "ifc_class", text="", icon="FILE_VOLUME") - col.prop(tprops, "relating_type", text="", icon="FILE_3D") - col.operator("bim.add_type_instance", icon="ADD") + enabled = True + + if type_prop.getIfcTypes(tprops, context): + col.prop(tprops, "ifc_class", text="", icon="FILE_VOLUME") + else: + col.label(text="No IFC Class", icon="FILE_VOLUME") + enabled = False + if type_prop.getAvailableTypes(tprops, context): + col.prop(tprops, "relating_type", text="", icon="FILE_3D") + else: + col.label(text="No Relating Type", icon="FILE_3D") + enabled = False + row = col.row() + row.operator("bim.add_type_instance", icon="ADD") + row.enabled = enabled class BIM_PT_authoring_architectural(Panel): @@ -76,9 +89,8 @@ class BIM_PT_misc_utilities(Panel): layout = self.layout props = context.scene.BIMProperties - row = layout.row() + row = layout.split(factor=0.2, align=True) row.prop(props, "override_colour", text="") - row = layout.row(align=True) row.operator("bim.set_override_colour") row = layout.row(align=True) row.operator("bim.set_viewport_shadow_from_sun") diff --git a/src/blenderbim/blenderbim/bim/module/model/wall.py b/src/blenderbim/blenderbim/bim/module/model/wall.py index c18acfba78..3678422016 100644 --- a/src/blenderbim/blenderbim/bim/module/model/wall.py +++ b/src/blenderbim/blenderbim/bim/module/model/wall.py @@ -74,12 +74,14 @@ class JoinWall(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} join_type: bpy.props.StringProperty() + @classmethod + def poll(cls, context): + return context.selected_objects + def execute(self, context): - selected_objs = context.selected_objects + selected_objs = [o for o in context.selected_objects if o.BIMObjectProperties.ifc_definition_id] for obj in selected_objs: bpy.ops.bim.dynamically_void_product(obj=obj.name) - if len(selected_objs) == 0: - return {"FINISHED"} if not self.join_type: for obj in selected_objs: DumbWallJoiner(obj, obj).unjoin() @@ -114,11 +116,14 @@ class AlignWall(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} align_type: bpy.props.StringProperty() + @classmethod + def poll(cls, context): + selected_valid_objects = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")] + return context.active_object and len(selected_valid_objects) > 1 + def execute(self, context): - selected_objs = context.selected_objects - if len(selected_objs) < 2 or not context.active_object: - return {"FINISHED"} - for obj in selected_objs: + selected_objects = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")] + for obj in selected_objects: if obj == context.active_object: continue aligner = DumbWallAligner(obj, context.active_object) @@ -137,10 +142,12 @@ class FlipWall(bpy.types.Operator): bl_label = "Flip Wall" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return context.selected_objects + def execute(self, context): - selected_objs = context.selected_objects - if len(selected_objs) == 0: - return {"FINISHED"} + selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")] for obj in selected_objs: DumbWallFlipper(obj).flip() IfcStore.edited_objs.add(obj) @@ -152,12 +159,14 @@ class SplitWall(bpy.types.Operator): bl_label = "Split Wall" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return context.selected_objects + def execute(self, context): - selected_objs = context.selected_objects - if len(selected_objs) == 0: - return {"FINISHED"} + selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")] for obj in selected_objs: - DumbWallSplitter(obj, bpy.context.scene.cursor.location).split() + DumbWallSplitter(obj, context.scene.cursor.location).split() IfcStore.edited_objs.add(obj) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 59c8fd3234..7215a1bb2d 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -19,6 +19,7 @@ import os import bpy +import blenderbim.bim.module.type.prop as type_prop from bpy.types import WorkSpaceTool from blenderbim.bim.ifc import IfcStore @@ -52,9 +53,20 @@ class BimTool(WorkSpaceTool): def draw_settings(context, layout, tool): row = layout.row(align=True) + if not IfcStore.get_file(): + row.label(text="No IFC Project", icon="ERROR") + return props = context.scene.BIMTypeProperties - row.prop(props, "ifc_class", text="") - row.prop(props, "relating_type", text="") + ifc_classes_is_empty = not bool(type_prop.getIfcTypes(props, context)) + if ifc_classes_is_empty: + row.label(text="No IFC Class") + else: + row.prop(props, "ifc_class", text="") + if type_prop.getAvailableTypes(props, context): + row.prop(props, "relating_type", text="") + else: + row.label(text="No Relating Type") + row.label(text="", icon="BLANK1") @@ -62,34 +74,35 @@ class BimTool(WorkSpaceTool): row.label(text="", icon="EVENT_SHIFT") row.label(text="Add Type Instance", icon="EVENT_A") - if props.ifc_class == "IfcWallType": - row = layout.row() - row.label(text="Join") - row = layout.row(align=True) - row.label(text="", icon="EVENT_SHIFT") - row.label(text="Extend", icon="EVENT_E") - row = layout.row(align=True) - row.label(text="", icon="EVENT_SHIFT") - row.label(text="Butt", icon="EVENT_T") - row = layout.row(align=True) - row.label(text="", icon="EVENT_SHIFT") - row.label(text="Mitre", icon="EVENT_Y") + if not ifc_classes_is_empty: + if props.ifc_class == "IfcWallType": + row = layout.row() + row.label(text="Join") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="Extend", icon="EVENT_E") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="Butt", icon="EVENT_T") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="Mitre", icon="EVENT_Y") - row = layout.row() - row.label(text="Wall Tools") - row = layout.row(align=True) - row.label(text="", icon="EVENT_SHIFT") - row.label(text="Flip", icon="EVENT_F") - row = layout.row(align=True) - row.label(text="", icon="EVENT_SHIFT") - row.label(text="Split", icon="EVENT_S") + row = layout.row() + row.label(text="Wall Tools") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="Flip", icon="EVENT_F") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="Split", icon="EVENT_S") - if props.ifc_class in ["IfcColumnType", "IfcBeamType", "IfcMemberType"]: - row = layout.row() - row.label(text="Join") - row = layout.row(align=True) - row.label(text="", icon="EVENT_SHIFT") - row.label(text="Extend", icon="EVENT_E") + if props.ifc_class in ("IfcColumnType", "IfcBeamType", "IfcMemberType"): + row = layout.row() + row.label(text="Join") + row = layout.row(align=True) + row.label(text="", icon="EVENT_SHIFT") + row.label(text="Extend", icon="EVENT_E") row = layout.row(align=True) row.label(text="", icon="EVENT_SHIFT") @@ -124,11 +137,16 @@ class Hotkey(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} hotkey: bpy.props.StringProperty() + @classmethod + def poll(cls, context): + return IfcStore.get_file() + def execute(self, context): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): self.props = context.scene.BIMTypeProperties + self.ifc_classes_is_empty = not bool(type_prop.getIfcTypes(self.props, context)) getattr(self, f"hotkey_{self.hotkey}")() return {"FINISHED"} @@ -136,26 +154,29 @@ class Hotkey(bpy.types.Operator): bpy.ops.bim.add_type_instance() def hotkey_S_C(self): - if self.props.ifc_class == "IfcWallType": + if not self.ifc_classes_is_empty and self.props.ifc_class == "IfcWallType": bpy.ops.bim.align_wall(align_type="CENTERLINE") else: bpy.ops.bim.align_product(align_type="CENTERLINE") def hotkey_S_E(self): + if self.ifc_classes_is_empty: + return if self.props.ifc_class == "IfcWallType": bpy.ops.bim.join_wall(join_type="T") elif self.props.ifc_class in ["IfcColumnType", "IfcBeamType", "IfcMemberType"]: bpy.ops.bim.extend_profile() def hotkey_S_V(self): - if self.props.ifc_class == "IfcWallType": + if not self.ifc_classes_is_empty and self.props.ifc_class == "IfcWallType": bpy.ops.bim.align_wall(align_type="INTERIOR") else: bpy.ops.bim.align_product(align_type="POSITIVE") def hotkey_S_X(self): - if self.props.ifc_class == "IfcWallType": - bpy.ops.bim.align_wall(align_type="EXTERIOR") + if not self.ifc_classes_is_empty and self.props.ifc_class == "IfcWallType": + if bpy.ops.bim.align_wall.poll(): + bpy.ops.bim.align_wall(align_type="EXTERIOR") else: bpy.ops.bim.align_product(align_type="NEGATIVE") diff --git a/src/blenderbim/blenderbim/bim/module/qto/helper.py b/src/blenderbim/blenderbim/bim/module/qto/helper.py index d2c53baaff..8826a8d5b7 100644 --- a/src/blenderbim/blenderbim/bim/module/qto/helper.py +++ b/src/blenderbim/blenderbim/bim/module/qto/helper.py @@ -25,12 +25,38 @@ def calculate_height(obj): return obj.dimensions[2] -def calculate_volume(obj): - bm = bmesh.new() - bm.from_mesh(obj.data) - result = bm.calc_volume() - bm.free() - return result +def calculate_edges_lengths(objs, context): + return calculate_mesh_quantity(objs, context, lambda bm: sum((e.calc_length() for e in bm.edges if e.select))) + + +def calculate_faces_areas(objs, context): + return calculate_mesh_quantity(objs, context, lambda bm: sum((f.calc_area() for f in bm.faces if f.select))) + + +def calculate_volumes(objs, context): + return calculate_mesh_quantity(objs, context, lambda bm: bm.calc_volume()) + + +def calculate_mesh_quantity(objs: bpy.types.Object, context, operation): + """Get the sum of the target quantity on all passed mesh objects + + :param objs: iterable of mesh object + :param context: current execution context + :param operation: function which takes a single bmesh as an argument, returns a float value + :returns float: + """ + result = 0 + edit_mode = context.active_object.mode == "EDIT" + for obj in objs: + if edit_mode: + bm = bmesh.from_edit_mesh(obj.data) + result += operation(bm) + else: + bm = bmesh.new() + bm.from_mesh(obj.data) + result += operation(bm) + bm.free() + return result def calculate_formwork_area(objs, context): @@ -49,10 +75,11 @@ def calculate_formwork_area(objs, context): context.collection.objects.link(new_obj) copied_objs.append(new_obj) - context_override = {} - context_override["object"] = context_override["active_object"] = copied_objs[0] - context_override["selected_objects"] = context_override["selected_editable_objects"] = copied_objs - bpy.ops.object.join(context_override) + if len(objs) > 1: + context_override = {} + context_override["object"] = context_override["active_object"] = copied_objs[0] + context_override["selected_objects"] = context_override["selected_editable_objects"] = copied_objs + bpy.ops.object.join(context_override) copied_objs[0].name = "Formwork" copied_objs[0].BIMObjectProperties.ifc_definition_id = 0 diff --git a/src/blenderbim/blenderbim/bim/module/qto/operator.py b/src/blenderbim/blenderbim/bim/module/qto/operator.py index b77d067e14..de4b968764 100644 --- a/src/blenderbim/blenderbim/bim/module/qto/operator.py +++ b/src/blenderbim/blenderbim/bim/module/qto/operator.py @@ -18,7 +18,6 @@ # along with BlenderBIM Add-on. If not, see . import bpy -import bmesh import ifcopenshell import ifcopenshell.api from blenderbim.bim.ifc import IfcStore @@ -31,14 +30,12 @@ class CalculateEdgeLengths(bpy.types.Operator): bl_label = "Calculate Edge Lengths" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return context.selected_objects and context.active_object + def execute(self, context): - result = 0 - for obj in context.selected_objects: - if not obj.data or not obj.data.edges: - continue - for edge in obj.data.edges: - if edge.select: - result += (obj.data.vertices[edge.vertices[1]].co - obj.data.vertices[edge.vertices[0]].co).length + result = helper.calculate_edges_lengths([o for o in context.selected_objects if o.type == "MESH"], context) context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) return {"FINISHED"} @@ -48,14 +45,12 @@ class CalculateFaceAreas(bpy.types.Operator): bl_label = "Calculate Face Areas" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return context.selected_objects and context.active_object + def execute(self, context): - result = 0 - for obj in context.selected_objects: - if not obj.data or not obj.data.polygons: - continue - for polygon in obj.data.polygons: - if polygon.select: - result += polygon.area + result = helper.calculate_faces_areas([o for o in context.selected_objects if o.type == "MESH"], context) context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) return {"FINISHED"} @@ -65,15 +60,12 @@ class CalculateObjectVolumes(bpy.types.Operator): bl_label = "Calculate Object Volumes" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return context.selected_objects and context.active_object + def execute(self, context): - result = 0 - for obj in context.selected_objects: - if not obj.data or not isinstance(obj.data, bpy.types.Mesh): - continue - bm = bmesh.new() - bm.from_mesh(obj.data) - result += bm.calc_volume() - bm.free() + result = helper.calculate_volumes([o for o in context.selected_objects if o.type == "MESH"], context) context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) return {"FINISHED"} @@ -83,17 +75,21 @@ class ExecuteQtoMethod(bpy.types.Operator): bl_label = "Execute Qto Method" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return context.selected_objects + def execute(self, context): + selected_mesh_objects = [o for o in context.selected_objects if o.type == "MESH"] props = context.scene.BIMQtoProperties result = 0 if props.qto_methods == "HEIGHT": - for obj in context.selected_objects: + for obj in selected_mesh_objects: result += helper.calculate_height(obj) elif props.qto_methods == "VOLUME": - for obj in context.selected_objects: - result += helper.calculate_volume(obj) + result = helper.calculate_volumes(selected_mesh_objects, context) elif props.qto_methods == "FORMWORK": - result = helper.calculate_formwork_area(context.selected_objects, context) + result = helper.calculate_formwork_area(selected_mesh_objects, context) props.qto_result = str(round(result, 3)) return {"FINISHED"} @@ -103,20 +99,24 @@ class QuantifyObjects(bpy.types.Operator): bl_label = "Quantify Objects" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return IfcStore.get_file() and context.selected_objects + def execute(self, context): return IfcStore.execute_ifc_operator(self, context) def _execute(self, context): props = context.scene.BIMQtoProperties self.file = IfcStore.get_file() - for obj in context.selected_objects: + for obj in (o for o in context.selected_objects if o.type == "MESH"): if not obj.BIMObjectProperties.ifc_definition_id: continue result = 0 if props.qto_methods == "HEIGHT": result = helper.calculate_height(obj) elif props.qto_methods == "VOLUME": - result = helper.calculate_volume(obj) + result = helper.calculate_volumes([obj], context) elif props.qto_methods == "FORMWORK": result = helper.calculate_formwork_area([obj], context) if not result: diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index fccaab384a..14cf470a42 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -411,11 +411,13 @@ class RemoveSectionPlane(bpy.types.Operator): bl_label = "Remove Temporary Section Cutaway" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return context.active_object and bpy.data.node_groups.get("Section Override") + def execute(self, context): name = context.active_object.name section_override = bpy.data.node_groups.get("Section Override") - if not section_override: - return {"FINISHED"} for node in section_override.nodes: if node.type != "TEX_COORD" or node.object.name != name: continue @@ -497,8 +499,11 @@ class SetOverrideColour(bpy.types.Operator): bl_label = "Set Override Colour" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return context.selected_objects + def execute(self, context): - result = 0 for obj in context.selected_objects: obj.color = context.scene.BIMProperties.override_colour area = next(area for area in context.screen.areas if area.type == "VIEW_3D") @@ -511,6 +516,10 @@ class SetViewportShadowFromSun(bpy.types.Operator): bl_label = "Set Viewport Shadow from Sun" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return context.active_object + def execute(self, context): # The vector used for the light direction is a bit funny mat = Matrix(((-1.0, 0.0, 0.0, 0.0), (0.0, 0, 1.0, 0.0), (-0.0, -1.0, 0, 0.0), (0.0, 0.0, 0.0, 1.0))) @@ -553,17 +562,20 @@ class SnapSpacesTogether(bpy.types.Operator): bl_label = "Snap Spaces Together" bl_options = {"REGISTER", "UNDO"} + @classmethod + def poll(cls, context): + return context.selected_objects + def execute(self, context): threshold = 0.5 processed_polygons = set() - for obj in context.selected_objects: - if obj.type != "MESH": - continue + selected_mesh_objects = [o for o in context.selected_objects if o.type == "MESH"] + for obj in selected_mesh_objects: for polygon in obj.data.polygons: center = obj.matrix_world @ polygon.center distance = None - for obj2 in context.selected_objects: - if obj2 == obj or obj.type != "MESH": + for obj2 in selected_mesh_objects: + if obj2 == obj: continue result = obj2.ray_cast(obj2.matrix_world.inverted() @ center, polygon.normal, distance=threshold) if not result[0]: diff --git a/src/ifcpatch/ifcpatch/__init__.py b/src/ifcpatch/ifcpatch/__init__.py index b9e9166d66..74f38c36dd 100644 --- a/src/ifcpatch/ifcpatch/__init__.py +++ b/src/ifcpatch/ifcpatch/__init__.py @@ -63,8 +63,7 @@ def extract_docs( method_name: str="__init__", boilerplate_args : typing.Iterable[str]=None): """Extract class docstrings and method arguments - - :param module: Parent module from which to extract the submodule class + :param submodule_name: Submodule from which to extract the class :param cls_name: Class from which to extract the docstring and method arguments :param method_name: Class Method name from which to extract arguments