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