From 8618befa0f2585e2e9af74238dac54408234c479 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 26 Jan 2021 09:51:33 +1100 Subject: [PATCH] WIP refactor clash and smart clash. See #1222. --- .../blenderbim/bim/__init__.py | 30 +- .../blenderbim/bim/module/bcf/operator.py | 10 +- .../blenderbim/bim/module/bcf/ui.py | 2 +- .../blenderbim/bim/module/clash/__init__.py | 38 ++ .../blenderbim/bim/module/clash/operator.py | 456 +++++++++++++++ .../blenderbim/bim/module/clash/prop.py | 53 ++ .../blenderbim/bim/module/clash/ui.py | 148 +++++ .../blenderbim/bim/operator.py | 542 ------------------ src/ifcblenderexport/blenderbim/bim/prop.py | 67 +-- src/ifcblenderexport/blenderbim/bim/ui.py | 178 ------ 10 files changed, 728 insertions(+), 796 deletions(-) create mode 100644 src/ifcblenderexport/blenderbim/bim/module/clash/__init__.py create mode 100644 src/ifcblenderexport/blenderbim/bim/module/clash/operator.py create mode 100644 src/ifcblenderexport/blenderbim/bim/module/clash/prop.py create mode 100644 src/ifcblenderexport/blenderbim/bim/module/clash/ui.py diff --git a/src/ifcblenderexport/blenderbim/bim/__init__.py b/src/ifcblenderexport/blenderbim/bim/__init__.py index 80f91e3089..9fea5176cc 100644 --- a/src/ifcblenderexport/blenderbim/bim/__init__.py +++ b/src/ifcblenderexport/blenderbim/bim/__init__.py @@ -13,6 +13,7 @@ if bpy is not None: "aggregate": None, "attribute": None, "bcf": None, + "clash": None, "classification": None, "cobie": None, "context": None, @@ -61,10 +62,6 @@ if bpy is not None: operator.SelectSweptSolidInnerCurves, operator.AssignSweptSolidExtrusion, operator.SelectSweptSolidExtrusion, - operator.AddMaterialPset, - operator.RemoveMaterialPset, - operator.AddMaterialAttribute, - operator.RemoveMaterialAttribute, operator.FetchLibraryInformation, operator.FetchExternalMaterial, operator.FetchObjectPassport, @@ -76,20 +73,6 @@ if bpy is not None: operator.OpenView, operator.OpenViewCamera, operator.ActivateView, - operator.ExportClashSets, - operator.ImportClashSets, - operator.AddClashSet, - operator.RemoveClashSet, - operator.AddClashSource, - operator.RemoveClashSource, - operator.SelectClashSource, - operator.ExecuteIfcClash, - operator.SelectIfcClashResults, - operator.SelectClashResults, - operator.SelectSmartGroupedClashesPath, - operator.SmartClashGroup, - operator.SelectSmartGroup, - operator.LoadSmartGroupsForActiveClashSet, operator.OpenUpstream, operator.AddSectionPlane, operator.RemoveSectionPlane, @@ -123,9 +106,6 @@ if bpy is not None: operator.CopyPropertyToSelection, operator.CopyAttributeToSelection, operator.RefreshDrawingList, - operator.SetBlenderClashSetA, - operator.SetBlenderClashSetB, - operator.ExecuteBlenderClash, operator.CleanWireframes, operator.LinkIfc, operator.SnapSpacesTogether, @@ -134,9 +114,6 @@ if bpy is not None: prop.StrProperty, prop.Attribute, prop.Variable, - prop.ClashSource, - prop.ClashSet, - prop.SmartClashGroup, prop.Drawing, prop.Schedule, prop.DrawingStyle, @@ -160,18 +137,13 @@ if bpy is not None: ui.BIM_PT_drawings, ui.BIM_PT_schedules, ui.BIM_PT_sheets, - ui.BIM_PT_ifcclash, ui.BIM_PT_library, - ui.BIM_PT_object_structural, ui.BIM_PT_camera, ui.BIM_PT_text, ui.BIM_PT_annotation_utilities, - ui.BIM_PT_clash_manager, ui.BIM_PT_misc_utilities, ui.BIM_UL_generic, ui.BIM_UL_drawinglist, - ui.BIM_UL_clash_sets, - ui.BIM_UL_smart_groups, ui.BIM_UL_topics, ui.BIM_ADDON_preferences, ] diff --git a/src/ifcblenderexport/blenderbim/bim/module/bcf/operator.py b/src/ifcblenderexport/blenderbim/bim/module/bcf/operator.py index 0f29905bfb..ec7b1969ea 100644 --- a/src/ifcblenderexport/blenderbim/bim/module/bcf/operator.py +++ b/src/ifcblenderexport/blenderbim/bim/module/bcf/operator.py @@ -725,9 +725,15 @@ class ActivateBcfViewpoint(bpy.types.Operator): obj.hide_set(True) continue if "IfcSpace" in obj.name: - is_visible = viewpoint.components.view_setup_hints.spaces_visible + if viewpoint.components.view_setup_hints: + is_visible = viewpoint.components.view_setup_hints.spaces_visible + else: + is_visible = False elif "IfcOpeningElement" in obj.name: - is_visible = viewpoint.components.view_setup_hints.openings_visible + if viewpoint.components.view_setup_hints: + is_visible = viewpoint.components.view_setup_hints.openings_visible + else: + is_visible = False obj.hide_set(not is_visible) if not is_visible: continue diff --git a/src/ifcblenderexport/blenderbim/bim/module/bcf/ui.py b/src/ifcblenderexport/blenderbim/bim/module/bcf/ui.py index f2d22c08ee..f03032d4ef 100644 --- a/src/ifcblenderexport/blenderbim/bim/module/bcf/ui.py +++ b/src/ifcblenderexport/blenderbim/bim/module/bcf/ui.py @@ -4,7 +4,7 @@ from . import bcfstore from bpy.types import Panel class BIM_PT_bcf(Panel): - bl_label = "BIM Collaboration Format (BCF)" + bl_label = "BCF Project" bl_idname = "BIM_PT_bcf" bl_options = {"DEFAULT_CLOSED"} bl_space_type = "PROPERTIES" diff --git a/src/ifcblenderexport/blenderbim/bim/module/clash/__init__.py b/src/ifcblenderexport/blenderbim/bim/module/clash/__init__.py new file mode 100644 index 0000000000..63448c96e3 --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/clash/__init__.py @@ -0,0 +1,38 @@ +import bpy +from . import ui, prop, operator + +classes = ( + operator.ExportClashSets, + operator.ImportClashSets, + operator.AddClashSet, + operator.RemoveClashSet, + operator.AddClashSource, + operator.RemoveClashSource, + operator.SelectClashSource, + operator.ExecuteIfcClash, + operator.SelectIfcClashResults, + operator.SelectClashResults, + operator.SelectSmartGroupedClashesPath, + operator.SmartClashGroup, + operator.SelectSmartGroup, + operator.LoadSmartGroupsForActiveClashSet, + operator.SetBlenderClashSetA, + operator.SetBlenderClashSetB, + operator.ExecuteBlenderClash, + prop.ClashSource, + prop.ClashSet, + prop.SmartClashGroup, + prop.BIMClashProperties, + ui.BIM_PT_ifcclash, + ui.BIM_PT_clash_manager, + ui.BIM_UL_clash_sets, + ui.BIM_UL_smart_groups, +) + + +def register(): + bpy.types.Scene.BIMClashProperties = bpy.props.PointerProperty(type=prop.BIMClashProperties) + + +def unregister(): + del bpy.types.Scene.BIMClashProperties diff --git a/src/ifcblenderexport/blenderbim/bim/module/clash/operator.py b/src/ifcblenderexport/blenderbim/bim/module/clash/operator.py new file mode 100644 index 0000000000..b6824ace7f --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/clash/operator.py @@ -0,0 +1,456 @@ +import os +import bpy +import json +import bmesh +import logging +import numpy as np +from mathutils import Matrix +from math import radians + + +class ExportClashSets(bpy.types.Operator): + bl_idname = "bim.export_clash_sets" + bl_label = "Export Clash Sets" + filename_ext = ".json" + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + + def invoke(self, context, event): + self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") + WindowManager = context.window_manager + WindowManager.fileselect_add(self) + return {"RUNNING_MODAL"} + + def execute(self, context): + self.filepath = bpy.path.ensure_ext(self.filepath, ".json") + clash_sets = [] + for clash_set in bpy.context.scene.BIMClashProperties.clash_sets: + self.a = [] + self.b = [] + for ab in ["a", "b"]: + for data in getattr(clash_set, ab): + clash_source = {"file": data.name} + if data.selector: + clash_source["selector"] = data.selector + clash_source["mode"] = data.mode + getattr(self, ab).append(clash_source) + clash_sets.append({"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a, "b": self.b}) + with open(self.filepath, "w") as destination: + destination.write(json.dumps(clash_sets, indent=4)) + return {"FINISHED"} + + +class ImportClashSets(bpy.types.Operator): + bl_idname = "bim.import_clash_sets" + bl_label = "Import Clash Sets" + filename_ext = ".json" + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + + def invoke(self, context, event): + self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") + WindowManager = context.window_manager + WindowManager.fileselect_add(self) + return {"RUNNING_MODAL"} + + def execute(self, context): + with open(self.filepath) as f: + clash_sets = json.load(f) + for clash_set in clash_sets: + new = bpy.context.scene.BIMClashProperties.clash_sets.add() + new.name = clash_set["name"] + new.tolerance = clash_set["tolerance"] + for clash_source in clash_set["a"]: + new_source = new.a.add() + new_source.name = clash_source["file"] + if "selector" in clash_source: + new_source.selector = clash_source["selector"] + new_source.mode = clash_source["mode"] + if clash_set["b"]: + for clash_source in clash_set["b"]: + new_source = new.b.add() + new_source.name = clash_source["file"] + if "selector" in clash_source: + new_source.selector = clash_source["selector"] + new_source.mode = clash_source["mode"] + return {"FINISHED"} + + +class AddClashSet(bpy.types.Operator): + bl_idname = "bim.add_clash_set" + bl_label = "Add Clash Set" + + def execute(self, context): + new = bpy.context.scene.BIMClashProperties.clash_sets.add() + new.name = "New Clash Set" + new.tolerance = 0.01 + return {"FINISHED"} + + +class RemoveClashSet(bpy.types.Operator): + bl_idname = "bim.remove_clash_set" + bl_label = "Remove Clash Set" + index: bpy.props.IntProperty() + + def execute(self, context): + bpy.context.scene.BIMClashProperties.clash_sets.remove(self.index) + return {"FINISHED"} + + +class AddClashSource(bpy.types.Operator): + bl_idname = "bim.add_clash_source" + bl_label = "Add Clash Source" + group: bpy.props.StringProperty() + + def execute(self, context): + clash_set = bpy.context.scene.BIMClashProperties.clash_sets[bpy.context.scene.BIMClashProperties.active_clash_set_index] + source = getattr(clash_set, self.group).add() + return {"FINISHED"} + + +class RemoveClashSource(bpy.types.Operator): + bl_idname = "bim.remove_clash_source" + bl_label = "Remove Clash Source" + index: bpy.props.IntProperty() + group: bpy.props.StringProperty() + + def execute(self, context): + clash_set = bpy.context.scene.BIMClashProperties.clash_sets[bpy.context.scene.BIMClashProperties.active_clash_set_index] + getattr(clash_set, self.group).remove(self.index) + return {"FINISHED"} + + +class SelectClashSource(bpy.types.Operator): + bl_idname = "bim.select_clash_source" + bl_label = "Select Clash Source" + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + index: bpy.props.IntProperty() + group: bpy.props.StringProperty() + + def execute(self, context): + clash_set = bpy.context.scene.BIMClashProperties.clash_sets[bpy.context.scene.BIMClashProperties.active_clash_set_index] + getattr(clash_set, self.group)[self.index].name = self.filepath + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + +class SelectClashResults(bpy.types.Operator): + bl_idname = "bim.select_clash_results" + bl_label = "Select Clash Results" + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + + def execute(self, context): + bpy.context.scene.BIMClashProperties.clash_results_path = self.filepath + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + +class SelectSmartGroupedClashesPath(bpy.types.Operator): + bl_idname = "bim.select_smart_grouped_clashes_path" + bl_label = "Select Smart-Grouped Clashes Path" + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + + def execute(self, context): + bpy.context.scene.BIMClashProperties.smart_grouped_clashes_path = self.filepath + return {"FINISHED"} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + +class ExecuteIfcClash(bpy.types.Operator): + bl_idname = "bim.execute_ifc_clash" + bl_label = "Execute IFC Clash" + filename_ext = ".bcf" + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + + def invoke(self, context, event): + if ".json" not in bpy.data.filepath: + self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf") + WindowManager = context.window_manager + WindowManager.fileselect_add(self) + return {"RUNNING_MODAL"} + + def execute(self, context): + import ifcclash + + settings = ifcclash.IfcClashSettings() + if ".json" not in self.filepath: + self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf") + settings.output = self.filepath + settings.logger = logging.getLogger("Clash") + settings.logger.setLevel(logging.DEBUG) + ifc_clasher = ifcclash.IfcClasher(settings) + + if bpy.context.scene.BIMClashProperties.should_create_clash_snapshots: + + def get_viewpoint_snapshot(self, viewpoint, mat): + camera = bpy.data.objects.get("IFC Clash Camera") + if not camera: + camera = bpy.data.objects.new("IFC Clash Camera", bpy.data.cameras.new("IFC Clash Camera")) + bpy.context.scene.collection.objects.link(camera) + camera.matrix_world = Matrix(mat) + bpy.context.scene.camera = camera + camera.data.angle = radians(60) + area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") + area.spaces[0].region_3d.view_perspective = "CAMERA" + area.spaces[0].shading.show_xray = True + bpy.context.scene.render.resolution_x = 480 + bpy.context.scene.render.resolution_y = 270 + bpy.context.scene.render.image_settings.file_format = "PNG" + bpy.context.scene.render.filepath = os.path.join( + bpy.context.scene.BIMProperties.data_dir, "snapshot.png" + ) + bpy.ops.render.opengl(write_still=True) + return bpy.context.scene.render.filepath + + ifcclash.IfcClasher.get_viewpoint_snapshot = get_viewpoint_snapshot + + ifc_clasher.clash_sets = [] + for clash_set in bpy.context.scene.BIMClashProperties.clash_sets: + self.a = [] + self.b = [] + for ab in ["a", "b"]: + for data in getattr(clash_set, ab): + clash_source = {"file": data.name} + if data.selector: + clash_source["selector"] = data.selector + clash_source["mode"] = data.mode + getattr(self, ab).append(clash_source) + ifc_clasher.clash_sets.append( + {"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a, "b": self.b} + ) + ifc_clasher.clash() + ifc_clasher.export() + return {"FINISHED"} + + +class SelectIfcClashResults(bpy.types.Operator): + bl_idname = "bim.select_ifc_clash_results" + bl_label = "Select IFC Clash Results" + filename_ext = ".json" + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + + def invoke(self, context, event): + self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") + WindowManager = context.window_manager + WindowManager.fileselect_add(self) + return {"RUNNING_MODAL"} + + def execute(self, context): + self.filepath = bpy.path.ensure_ext(self.filepath, ".json") + with open(self.filepath) as f: + clash_sets = json.load(f) + clash_set_name = bpy.context.scene.BIMClashProperties.clash_sets[ + bpy.context.scene.BIMClashProperties.active_clash_set_index + ].name + global_ids = [] + for clash_set in clash_sets: + if clash_set["name"] != clash_set_name: + continue + if not "clashes" in clash_set.keys(): + self.report({"WARNING"}, "No clashes found for the selected Clash Set.") + return {"CANCELLED"} + for clash in clash_set["clashes"].values(): + global_ids.extend([clash["a_global_id"], clash["b_global_id"]]) + for obj in bpy.context.visible_objects: + if not obj.BIMObjectProperties.ifc_definition_id: + continue + element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + if element.GlobalId in global_ids: + obj.select_set(True) + return {"FINISHED"} + + +class SmartClashGroup(bpy.types.Operator): + bl_idname = "bim.smart_clash_group" + bl_label = "Smart Group Clashes" + filepath: bpy.props.StringProperty(subtype="FILE_PATH") + + def execute(self, context): + import ifcclash + + settings = ifcclash.IfcClashSettings() + self.filepath = bpy.path.ensure_ext(bpy.context.scene.BIMClashProperties.clash_results_path, ".json") + settings.output = self.filepath + settings.logger = logging.getLogger("Clash") + settings.logger.setLevel(logging.DEBUG) + ifc_clasher = ifcclash.IfcClasher(settings) + + with open(self.filepath) as f: + clash_sets = json.load(f) + + # execute the smart grouping + save_path = bpy.path.ensure_ext(bpy.context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json") + smart_grouped_clashes = ifc_clasher.smart_group_clashes( + clash_sets, bpy.context.scene.BIMClashProperties.smart_clash_grouping_max_distance + ) + + # save smart_groups to json + with open(save_path, "w") as f: + f.write(json.dumps(smart_grouped_clashes)) + + clash_set_name = bpy.context.scene.BIMClashProperties.clash_sets[ + bpy.context.scene.BIMClashProperties.active_clash_set_index + ].name + + # Reset the list of smart_clash_groups for the UI + bpy.context.scene.BIMClashProperties.smart_clash_groups.clear() + + for clash_set, smart_groups in smart_grouped_clashes.items(): + # Only select the clashes that correspond to the actively selected IFC Clash Set + if clash_set != clash_set_name: + continue + else: + for smart_group, global_id_pairs in smart_groups[0].items(): + new_group = bpy.context.scene.BIMClashProperties.smart_clash_groups.add() + new_group.number = f"{smart_group}" + + for pair in global_id_pairs: + for id in pair: + new_global_id = new_group.global_ids.add() + new_global_id.name = id + + return {"FINISHED"} + + +class LoadSmartGroupsForActiveClashSet(bpy.types.Operator): + bl_idname = "bim.load_smart_groups_for_active_clash_set" + bl_label = "Load Smart Groups for Active Clash Set" + + def execute(self, context): + smart_groups_path = bpy.path.ensure_ext(bpy.context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json") + + clash_set_name = bpy.context.scene.BIMClashProperties.clash_sets[ + bpy.context.scene.BIMClashProperties.active_clash_set_index + ].name + + with open(smart_groups_path) as f: + smart_grouped_clashes = json.load(f) + + # Reset the list of smart_clash_groups for the UI + bpy.context.scene.BIMClashProperties.smart_clash_groups.clear() + + for clash_set, smart_groups in smart_grouped_clashes.items(): + # Only select the clashes that correspond to the actively selected IFC Clash Set + if clash_set != clash_set_name: + continue + else: + for smart_group, global_id_pairs in smart_groups[0].items(): + new_group = bpy.context.scene.BIMClashProperties.smart_clash_groups.add() + new_group.number = f"{smart_group}" + for pair in global_id_pairs: + for id in pair: + new_global_id = new_group.global_ids.add() + new_global_id.name = id + + return {"FINISHED"} + + +class SelectSmartGroup(bpy.types.Operator): + bl_idname = "bim.select_smart_group" + bl_label = "Select Smart Group" + + def execute(self, context): + # Select smart group in view + selected_smart_group = bpy.context.scene.BIMClashProperties.smart_clash_groups[ + bpy.context.scene.BIMCLashProperties.active_smart_group_index + ] + # print(selected_smart_group.number) + + for obj in bpy.context.visible_objects: + if not obj.BIMObjectProperties.ifc_definition_id: + continue + element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + for id in selected_smart_group.global_ids: + # print("Id: ", id) + # print("Global id: ", element.GlobalId) + if element.GlobalId in id.name: + # print("object match: ", global_id) + obj.select_set(True) + + return {"FINISHED"} + + +class BlenderClasher: + def process_clash_set(self): + import collision + + a_cm = collision.CollisionManager() + b_cm = collision.CollisionManager() + self.add_to_cm(a_cm, bpy.context.scene.BIMClashProperties.blender_clash_set_a) + self.add_to_cm(b_cm, bpy.context.scene.BIMClashProperties.blender_clash_set_b) + results = a_cm.in_collision_other(b_cm, return_data=True) + if not results[0]: + print("No clashes") + return + for contact in results[1]: + if contact.raw.penetration_depth < 0.01: + continue + print("-----") + print(contact.names) + print(contact.raw.normal) + print(contact.raw.pos) + + def add_to_cm(self, cm, object_names): + import ifcclash + + for object_name in object_names: + name = object_name.name + obj = bpy.data.objects[name] + triangulated_mesh = self.triangulate_mesh(obj) + mesh = ifcclash.Mesh() + mesh.vertices = np.array([tuple(obj.matrix_world @ v.co) for v in triangulated_mesh.vertices]) + mesh.faces = np.array([tuple(p.vertices) for p in triangulated_mesh.polygons]) + cm.add_object(name, mesh) + + def triangulate_mesh(self, obj): + mesh = obj.evaluated_get(bpy.context.evaluated_depsgraph_get()).to_mesh() + bm = bmesh.new() + bm.from_mesh(mesh) + bmesh.ops.triangulate(bm, faces=bm.faces) + bm.to_mesh(mesh) + bm.free() + del bm + return mesh + + +class SetBlenderClashSetA(bpy.types.Operator): + bl_idname = "bim.set_blender_clash_set_a" + bl_label = "Set Blender Clash Set A" + + def execute(self, context): + while len(bpy.context.scene.BIMClashProperties.blender_clash_set_a) > 0: + bpy.context.scene.BIMClashProperties.blender_clash_set_a.remove(0) + for obj in bpy.context.selected_objects: + new = bpy.context.scene.BIMClashProperties.blender_clash_set_a.add() + new.name = obj.name + return {"FINISHED"} + + +class SetBlenderClashSetB(bpy.types.Operator): + bl_idname = "bim.set_blender_clash_set_b" + bl_label = "Set Blender Clash Set B" + + def execute(self, context): + while len(bpy.context.scene.BIMClashProperties.blender_clash_set_b) > 0: + bpy.context.scene.BIMClashProperties.blender_clash_set_b.remove(0) + for obj in bpy.context.selected_objects: + new = bpy.context.scene.BIMClashProperties.blender_clash_set_b.add() + new.name = obj.name + return {"FINISHED"} + + +class ExecuteBlenderClash(bpy.types.Operator): + bl_idname = "bim.execute_blender_clash" + bl_label = "Execute Blender Clash" + + def execute(self, context): + blender_clasher = BlenderClasher() + blender_clasher.process_clash_set() + return {"FINISHED"} diff --git a/src/ifcblenderexport/blenderbim/bim/module/clash/prop.py b/src/ifcblenderexport/blenderbim/bim/module/clash/prop.py new file mode 100644 index 0000000000..185e9fe163 --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/clash/prop.py @@ -0,0 +1,53 @@ +import bpy +from blenderbim.bim.prop import StrProperty, Attribute +from blenderbim.bim.module.owner.data import Data +from bpy.types import PropertyGroup +from bpy.props import ( + PointerProperty, + StringProperty, + EnumProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + CollectionProperty, +) + + +class ClashSource(PropertyGroup): + name: StringProperty(name="File") + selector: StringProperty(name="Selector") + mode: EnumProperty( + items=[ + ("i", "Include", "Only the selected objects are included for clashing"), + ("e", "Exclude", "All objects except the selected objects are included for clashing"), + ], + name="Mode", + ) + + +class ClashSet(PropertyGroup): + name: StringProperty(name="Name") + tolerance: FloatProperty(name="Tolerance") + a: CollectionProperty(name="Group A", type=ClashSource) + b: CollectionProperty(name="Group B", type=ClashSource) + + +class SmartClashGroup(PropertyGroup): + number: StringProperty(name="Number") + global_ids: CollectionProperty(name="GlobalIDs", type=StrProperty) + + +class BIMClashProperties(PropertyGroup): + blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty) + blender_clash_set_b: CollectionProperty(name="Blender Clash Set B", type=StrProperty) + clash_sets: CollectionProperty(name="Clash Sets", type=ClashSet) + should_create_clash_snapshots: BoolProperty(name="Create Snapshots", default=True) + clash_results_path: StringProperty(name="Clash Results Path") + smart_grouped_clashes_path: StringProperty(name="Smart Grouped Clashes Path") + active_clash_set_index: IntProperty(name="Active Clash Set Index") + smart_clash_groups: CollectionProperty(name="Smart Clash Groups", type=SmartClashGroup) + active_smart_group_index: IntProperty(name="Active Smart Group Index") + smart_clash_grouping_max_distance: IntProperty( + name="Smart Clash Grouping Max Distance", default=3, soft_min=1, soft_max=10 + ) diff --git a/src/ifcblenderexport/blenderbim/bim/module/clash/ui.py b/src/ifcblenderexport/blenderbim/bim/module/clash/ui.py new file mode 100644 index 0000000000..d7f3416b65 --- /dev/null +++ b/src/ifcblenderexport/blenderbim/bim/module/clash/ui.py @@ -0,0 +1,148 @@ +import bpy +from bpy.types import Panel + + +class BIM_PT_ifcclash(Panel): + bl_label = "IFC Clash Sets" + bl_idname = "BIM_PT_ifcclash" + bl_options = {"DEFAULT_CLOSED"} + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "scene" + + def draw(self, context): + layout = self.layout + + scene = context.scene + props = scene.BIMClashProperties + + layout.label(text="Blender Clash:") + + row = layout.row(align=True) + row.operator("bim.set_blender_clash_set_a") + row.operator("bim.set_blender_clash_set_b") + + row = layout.row(align=True) + row.operator("bim.execute_blender_clash") + + layout.label(text="IFC Clash:") + + row = layout.row(align=True) + row.operator("bim.add_clash_set") + row.operator("bim.import_clash_sets", text="", icon="IMPORT") + row.operator("bim.export_clash_sets", text="", icon="EXPORT") + + if not props.clash_sets: + return + + 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] + + row = layout.row(align=True) + row.prop(clash_set, "name") + row.operator("bim.remove_clash_set", icon="X", text="").index = props.active_clash_set_index + + row = layout.row(align=True) + row.prop(clash_set, "tolerance") + + layout.label(text="Group A:") + row = layout.row() + row.operator("bim.add_clash_source").group = "a" + + for index, source in enumerate(clash_set.a): + row = layout.row(align=True) + row.prop(source, "name", text="") + op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="") + op.index = index + op.group = "a" + op = row.operator("bim.remove_clash_source", icon="X", text="") + op.index = index + op.group = "a" + + row = layout.row(align=True) + row.prop(source, "mode", text="") + row.prop(source, "selector", text="") + + layout.label(text="Group B:") + row = layout.row() + row.operator("bim.add_clash_source").group = "b" + + for index, source in enumerate(clash_set.b): + row = layout.row(align=True) + row.prop(source, "name", text="") + op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="") + op.index = index + op.group = "b" + op = row.operator("bim.remove_clash_source", icon="X", text="") + op.index = index + op.group = "b" + + row = layout.row(align=True) + row.prop(source, "mode", text="") + row.prop(source, "selector", text="") + + row = layout.row() + row.prop(props, "should_create_clash_snapshots") + row = layout.row(align=True) + row.operator("bim.execute_ifc_clash") + row.operator("bim.select_ifc_clash_results") + + +class BIM_PT_clash_manager(Panel): + bl_idname = "BIM_PT_clash_manager" + bl_label = "Clash Manager" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "BlenderBIM" + + def draw(self, context): + layout = self.layout + props = context.scene.BIMClashProperties + + row = layout.row() + layout.label(text="Select clash results to group:") + + row = layout.row(align=True) + row.prop(props, "clash_results_path", text="") + op = row.operator("bim.select_clash_results", icon="FILE_FOLDER", text="") + + row = layout.row() + layout.label(text="Select output path for smart-grouped clashes:") + + row = layout.row(align=True) + row.prop(props, "smart_grouped_clashes_path", text="") + op = row.operator("bim.select_smart_grouped_clashes_path", icon="FILE_FOLDER", text="") + + row = layout.row(align=True) + row.prop(props, "smart_clash_grouping_max_distance") + + row = layout.row(align=True) + row.operator("bim.smart_clash_group") + + row = layout.row(align=True) + row.operator("bim.load_smart_groups_for_active_clash_set") + + layout.template_list("BIM_UL_smart_groups", "", props, "smart_clash_groups", props, "active_smart_group_index") + + row = layout.row(align=True) + row.operator("bim.select_smart_group") + + +class BIM_UL_clash_sets(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + ob = data + if item: + layout.prop(item, "name", text="", emboss=False) + else: + layout.label(text="", translate=False) + + +class BIM_UL_smart_groups(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + ob = data + if item: + layout.label(text=str(item.number), translate=False, icon="NONE", icon_value=0) + else: + layout.label(text="", translate=False) diff --git a/src/ifcblenderexport/blenderbim/bim/operator.py b/src/ifcblenderexport/blenderbim/bim/operator.py index acfce7de62..ff58a603bc 100644 --- a/src/ifcblenderexport/blenderbim/bim/operator.py +++ b/src/ifcblenderexport/blenderbim/bim/operator.py @@ -12,7 +12,6 @@ import ifcopenshell.util.selector import ifcopenshell.util.geolocation import ifcopenshell.util.element import ifcopenshell.util.schema -import tempfile import numpy as np from . import export_ifc from . import import_ifc @@ -28,7 +27,6 @@ from bpy_extras.io_utils import ImportHelper from mathutils import Vector, Matrix, Euler, geometry import bmesh from math import radians, degrees, atan, tan, cos, sin -from pathlib import Path from bpy.app.handlers import persistent @@ -179,96 +177,6 @@ class OpenUri(bpy.types.Operator): return {"FINISHED"} -class AddQto(bpy.types.Operator): - bl_idname = "bim.add_qto" - bl_label = "Add Qto" - - def execute(self, context): - name = bpy.context.active_object.BIMObjectProperties.qto_name - qto_template = schema.ifc.psetqto.get_by_name(name) - if not qto_template: - return {"FINISHED"} - for obj in bpy.context.selected_objects: - if "/" not in obj.name or obj.BIMObjectProperties.qtos.find(name) != -1: - continue - applicable_qtos = schema.ifc.psetqto.get_applicable_names(obj.name.split("/")[0], qto_only=True) - if name not in applicable_qtos: - continue - qto = obj.BIMObjectProperties.qtos.add() - qto.name = name - for prop_name in (p.Name for p in qto_template.HasPropertyTemplates): - prop = qto.properties.add() - prop.name = prop_name - return {"FINISHED"} - - -class RemoveQto(bpy.types.Operator): - bl_idname = "bim.remove_qto" - bl_label = "Remove Qto" - index: bpy.props.IntProperty() - - def execute(self, context): - name = bpy.context.active_object.BIMObjectProperties.qtos[self.index].name - for obj in bpy.context.selected_objects: - if "/" not in obj.name: - continue - index = obj.BIMObjectProperties.qtos.find(name) - if index != -1: - obj.BIMObjectProperties.qtos.remove(index) - return {"FINISHED"} - - -class AddMaterialPset(bpy.types.Operator): - bl_idname = "bim.add_material_pset" - bl_label = "Add Material Pset" - - def execute(self, context): - material = bpy.context.active_object.active_material - name = material.BIMMaterialProperties.pset_name - pset_template = schema.ifc.psetqto.get_by_name(name) - if not pset_template: - return {"FINISHED"} - if material.BIMMaterialProperties.psets.find(name) != -1: - return {"FINISHED"} - pset = material.BIMMaterialProperties.psets.add() - pset.name = name - for prop_name in (p.Name for p in pset_template.HasPropertyTemplates): - prop = pset.properties.add() - prop.name = prop_name - return {"FINISHED"} - - -class RemoveMaterialPset(bpy.types.Operator): - bl_idname = "bim.remove_material_pset" - bl_label = "Remove Pset" - pset_index: bpy.props.IntProperty() - - def execute(self, context): - bpy.context.active_object.active_material.BIMMaterialProperties.psets.remove(self.pset_index) - return {"FINISHED"} - - -class AddMaterialAttribute(bpy.types.Operator): - bl_idname = "bim.add_material_attribute" - bl_label = "Add Material Attribute" - - def execute(self, context): - if bpy.context.active_object.active_material.BIMMaterialProperties.applicable_attributes: - attribute = bpy.context.active_object.active_material.BIMMaterialProperties.attributes.add() - attribute.name = bpy.context.active_object.active_material.BIMMaterialProperties.applicable_attributes - return {"FINISHED"} - - -class RemoveMaterialAttribute(bpy.types.Operator): - bl_idname = "bim.remove_material_attribute" - bl_label = "Remove Material Attribute" - attribute_index: bpy.props.IntProperty() - - def execute(self, context): - bpy.context.active_object.active_material.BIMMaterialProperties.attributes.remove(self.attribute_index) - return {"FINISHED"} - - class AddSweptSolid(bpy.types.Operator): bl_idname = "bim.add_swept_solid" bl_label = "Add Swept Solid" @@ -413,374 +321,6 @@ class SelectExternalMaterialDir(bpy.types.Operator): return {"RUNNING_MODAL"} - - -class ExportClashSets(bpy.types.Operator): - bl_idname = "bim.export_clash_sets" - bl_label = "Export Clash Sets" - filename_ext = ".json" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def invoke(self, context, event): - self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} - - def execute(self, context): - self.filepath = bpy.path.ensure_ext(self.filepath, ".json") - clash_sets = [] - for clash_set in bpy.context.scene.BIMProperties.clash_sets: - self.a = [] - self.b = [] - for ab in ["a", "b"]: - for data in getattr(clash_set, ab): - clash_source = {"file": data.name} - if data.selector: - clash_source["selector"] = data.selector - clash_source["mode"] = data.mode - getattr(self, ab).append(clash_source) - clash_sets.append({"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a, "b": self.b}) - with open(self.filepath, "w") as destination: - destination.write(json.dumps(clash_sets, indent=4)) - return {"FINISHED"} - - -class ImportClashSets(bpy.types.Operator): - bl_idname = "bim.import_clash_sets" - bl_label = "Import Clash Sets" - filename_ext = ".json" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def invoke(self, context, event): - self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} - - def execute(self, context): - with open(self.filepath) as f: - clash_sets = json.load(f) - for clash_set in clash_sets: - new = bpy.context.scene.BIMProperties.clash_sets.add() - new.name = clash_set["name"] - new.tolerance = clash_set["tolerance"] - for clash_source in clash_set["a"]: - new_source = new.a.add() - new_source.name = clash_source["file"] - if "selector" in clash_source: - new_source.selector = clash_source["selector"] - new_source.mode = clash_source["mode"] - if clash_set["b"]: - for clash_source in clash_set["b"]: - new_source = new.b.add() - new_source.name = clash_source["file"] - if "selector" in clash_source: - new_source.selector = clash_source["selector"] - new_source.mode = clash_source["mode"] - return {"FINISHED"} - - -class AddClashSet(bpy.types.Operator): - bl_idname = "bim.add_clash_set" - bl_label = "Add Clash Set" - - def execute(self, context): - new = bpy.context.scene.BIMProperties.clash_sets.add() - new.name = "New Clash Set" - new.tolerance = 0.01 - return {"FINISHED"} - - -class RemoveClashSet(bpy.types.Operator): - bl_idname = "bim.remove_clash_set" - bl_label = "Remove Clash Set" - index: bpy.props.IntProperty() - - def execute(self, context): - bpy.context.scene.BIMProperties.clash_sets.remove(self.index) - return {"FINISHED"} - - -class AddClashSource(bpy.types.Operator): - bl_idname = "bim.add_clash_source" - bl_label = "Add Clash Source" - group: bpy.props.StringProperty() - - def execute(self, context): - clash_set = bpy.context.scene.BIMProperties.clash_sets[bpy.context.scene.BIMProperties.active_clash_set_index] - source = getattr(clash_set, self.group).add() - return {"FINISHED"} - - -class RemoveClashSource(bpy.types.Operator): - bl_idname = "bim.remove_clash_source" - bl_label = "Remove Clash Source" - index: bpy.props.IntProperty() - group: bpy.props.StringProperty() - - def execute(self, context): - clash_set = bpy.context.scene.BIMProperties.clash_sets[bpy.context.scene.BIMProperties.active_clash_set_index] - getattr(clash_set, self.group).remove(self.index) - return {"FINISHED"} - - -class SelectClashSource(bpy.types.Operator): - bl_idname = "bim.select_clash_source" - bl_label = "Select Clash Source" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - index: bpy.props.IntProperty() - group: bpy.props.StringProperty() - - def execute(self, context): - clash_set = bpy.context.scene.BIMProperties.clash_sets[bpy.context.scene.BIMProperties.active_clash_set_index] - getattr(clash_set, self.group)[self.index].name = self.filepath - return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - - -class SelectClashResults(bpy.types.Operator): - bl_idname = "bim.select_clash_results" - bl_label = "Select Clash Results" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def execute(self, context): - bpy.context.scene.BIMProperties.clash_results_path = self.filepath - return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - - -class SelectSmartGroupedClashesPath(bpy.types.Operator): - bl_idname = "bim.select_smart_grouped_clashes_path" - bl_label = "Select Smart-Grouped Clashes Path" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def execute(self, context): - bpy.context.scene.BIMProperties.smart_grouped_clashes_path = self.filepath - return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - - -class ExecuteIfcClash(bpy.types.Operator): - bl_idname = "bim.execute_ifc_clash" - bl_label = "Execute IFC Clash" - filename_ext = ".bcf" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def invoke(self, context, event): - if ".json" not in bpy.data.filepath: - self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".bcf") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} - - def execute(self, context): - import ifcclash - - settings = ifcclash.IfcClashSettings() - if ".json" not in self.filepath: - self.filepath = bpy.path.ensure_ext(self.filepath, ".bcf") - settings.output = self.filepath - settings.logger = logging.getLogger("Clash") - settings.logger.setLevel(logging.DEBUG) - ifc_clasher = ifcclash.IfcClasher(settings) - - if bpy.context.scene.BIMProperties.should_create_clash_snapshots: - - def get_viewpoint_snapshot(self, viewpoint, mat): - camera = bpy.data.objects.get("IFC Clash Camera") - if not camera: - camera = bpy.data.objects.new("IFC Clash Camera", bpy.data.cameras.new("IFC Clash Camera")) - bpy.context.scene.collection.objects.link(camera) - camera.matrix_world = Matrix(mat) - bpy.context.scene.camera = camera - camera.data.angle = radians(60) - area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") - area.spaces[0].region_3d.view_perspective = "CAMERA" - area.spaces[0].shading.show_xray = True - bpy.context.scene.render.resolution_x = 480 - bpy.context.scene.render.resolution_y = 270 - bpy.context.scene.render.image_settings.file_format = "PNG" - bpy.context.scene.render.filepath = os.path.join( - bpy.context.scene.BIMProperties.data_dir, "snapshot.png" - ) - bpy.ops.render.opengl(write_still=True) - return bpy.context.scene.render.filepath - - ifcclash.IfcClasher.get_viewpoint_snapshot = get_viewpoint_snapshot - - ifc_clasher.clash_sets = [] - for clash_set in bpy.context.scene.BIMProperties.clash_sets: - self.a = [] - self.b = [] - for ab in ["a", "b"]: - for data in getattr(clash_set, ab): - clash_source = {"file": data.name} - if data.selector: - clash_source["selector"] = data.selector - clash_source["mode"] = data.mode - getattr(self, ab).append(clash_source) - ifc_clasher.clash_sets.append( - {"name": clash_set.name, "tolerance": clash_set.tolerance, "a": self.a, "b": self.b} - ) - ifc_clasher.clash() - ifc_clasher.export() - return {"FINISHED"} - - -class SelectIfcClashResults(bpy.types.Operator): - bl_idname = "bim.select_ifc_clash_results" - bl_label = "Select IFC Clash Results" - filename_ext = ".json" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def invoke(self, context, event): - self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} - - def execute(self, context): - self.filepath = bpy.path.ensure_ext(self.filepath, ".json") - with open(self.filepath) as f: - clash_sets = json.load(f) - clash_set_name = bpy.context.scene.BIMProperties.clash_sets[ - bpy.context.scene.BIMProperties.active_clash_set_index - ].name - global_ids = [] - for clash_set in clash_sets: - if clash_set["name"] != clash_set_name: - continue - if not "clashes" in clash_set.keys(): - self.report({"WARNING"}, "No clashes found for the selected Clash Set.") - return {"CANCELLED"} - for clash in clash_set["clashes"].values(): - global_ids.extend([clash["a_global_id"], clash["b_global_id"]]) - for obj in bpy.context.visible_objects: - global_id = obj.BIMObjectProperties.attributes.get("GlobalId") - if global_id and global_id.string_value in global_ids: - obj.select_set(True) - return {"FINISHED"} - - -class SmartClashGroup(bpy.types.Operator): - bl_idname = "bim.smart_clash_group" - bl_label = "Smart Group Clashes" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def execute(self, context): - import ifcclash - - settings = ifcclash.IfcClashSettings() - self.filepath = bpy.path.ensure_ext(bpy.context.scene.BIMProperties.clash_results_path, ".json") - settings.output = self.filepath - settings.logger = logging.getLogger("Clash") - settings.logger.setLevel(logging.DEBUG) - ifc_clasher = ifcclash.IfcClasher(settings) - - with open(self.filepath) as f: - clash_sets = json.load(f) - - # execute the smart grouping - save_path = bpy.path.ensure_ext(bpy.context.scene.BIMProperties.smart_grouped_clashes_path, ".json") - smart_grouped_clashes = ifc_clasher.smart_group_clashes( - clash_sets, bpy.context.scene.BIMProperties.smart_clash_grouping_max_distance - ) - - # save smart_groups to json - with open(save_path, "w") as f: - f.write(json.dumps(smart_grouped_clashes)) - - clash_set_name = bpy.context.scene.BIMProperties.clash_sets[ - bpy.context.scene.BIMProperties.active_clash_set_index - ].name - - # Reset the list of smart_clash_groups for the UI - bpy.context.scene.BIMProperties.smart_clash_groups.clear() - - for clash_set, smart_groups in smart_grouped_clashes.items(): - # Only select the clashes that correspond to the actively selected IFC Clash Set - if clash_set != clash_set_name: - continue - else: - for smart_group, global_id_pairs in smart_groups[0].items(): - new_group = bpy.context.scene.BIMProperties.smart_clash_groups.add() - new_group.number = f"{smart_group}" - - for pair in global_id_pairs: - for id in pair: - new_global_id = new_group.global_ids.add() - new_global_id.name = id - - return {"FINISHED"} - - -class LoadSmartGroupsForActiveClashSet(bpy.types.Operator): - bl_idname = "bim.load_smart_groups_for_active_clash_set" - bl_label = "Load Smart Groups for Active Clash Set" - - def execute(self, context): - smart_groups_path = bpy.path.ensure_ext(bpy.context.scene.BIMProperties.smart_grouped_clashes_path, ".json") - - clash_set_name = bpy.context.scene.BIMProperties.clash_sets[ - bpy.context.scene.BIMProperties.active_clash_set_index - ].name - - with open(smart_groups_path) as f: - smart_grouped_clashes = json.load(f) - - # Reset the list of smart_clash_groups for the UI - bpy.context.scene.BIMProperties.smart_clash_groups.clear() - - for clash_set, smart_groups in smart_grouped_clashes.items(): - # Only select the clashes that correspond to the actively selected IFC Clash Set - if clash_set != clash_set_name: - continue - else: - for smart_group, global_id_pairs in smart_groups[0].items(): - new_group = bpy.context.scene.BIMProperties.smart_clash_groups.add() - new_group.number = f"{smart_group}" - for pair in global_id_pairs: - for id in pair: - new_global_id = new_group.global_ids.add() - new_global_id.name = id - - return {"FINISHED"} - - -class SelectSmartGroup(bpy.types.Operator): - bl_idname = "bim.select_smart_group" - bl_label = "Select Smart Group" - - def execute(self, context): - # Select smart group in view - selected_smart_group = bpy.context.scene.BIMProperties.smart_clash_groups[ - bpy.context.scene.BIMProperties.active_smart_group_index - ] - # print(selected_smart_group.number) - - for obj in bpy.context.visible_objects: - global_id = obj.BIMObjectProperties.attributes.get("GlobalId") - if global_id: - for id in selected_smart_group.global_ids: - # print("Id: ", id) - # print("Global id: ", global_id.string_value) - if global_id.string_value in id.name: - # print("object match: ", global_id) - obj.select_set(True) - - return {"FINISHED"} - - class SelectIfcFile(bpy.types.Operator): bl_idname = "bim.select_ifc_file" bl_label = "Select IFC File" @@ -2082,88 +1622,6 @@ class RefreshDrawingList(bpy.types.Operator): return {"FINISHED"} -class BlenderClasher: - def process_clash_set(self): - import collision - - a_cm = collision.CollisionManager() - b_cm = collision.CollisionManager() - self.add_to_cm(a_cm, bpy.context.scene.BIMProperties.blender_clash_set_a) - self.add_to_cm(b_cm, bpy.context.scene.BIMProperties.blender_clash_set_b) - results = a_cm.in_collision_other(b_cm, return_data=True) - if not results[0]: - print("No clashes") - return - for contact in results[1]: - if contact.raw.penetration_depth < 0.01: - continue - print("-----") - print(contact.names) - print(contact.raw.normal) - print(contact.raw.pos) - - def add_to_cm(self, cm, object_names): - import numpy as np - import ifcclash - - for object_name in object_names: - name = object_name.name - obj = bpy.data.objects[name] - triangulated_mesh = self.triangulate_mesh(obj) - mesh = ifcclash.Mesh() - mesh.vertices = np.array([tuple(obj.matrix_world @ v.co) for v in triangulated_mesh.vertices]) - mesh.faces = np.array([tuple(p.vertices) for p in triangulated_mesh.polygons]) - cm.add_object(name, mesh) - - def triangulate_mesh(self, obj): - import bmesh - - mesh = obj.evaluated_get(bpy.context.evaluated_depsgraph_get()).to_mesh() - bm = bmesh.new() - bm.from_mesh(mesh) - bmesh.ops.triangulate(bm, faces=bm.faces) - bm.to_mesh(mesh) - bm.free() - del bm - return mesh - - -class SetBlenderClashSetA(bpy.types.Operator): - bl_idname = "bim.set_blender_clash_set_a" - bl_label = "Set Blender Clash Set A" - - def execute(self, context): - while len(bpy.context.scene.BIMProperties.blender_clash_set_a) > 0: - bpy.context.scene.BIMProperties.blender_clash_set_a.remove(0) - for obj in bpy.context.selected_objects: - new = bpy.context.scene.BIMProperties.blender_clash_set_a.add() - new.name = obj.name - return {"FINISHED"} - - -class SetBlenderClashSetB(bpy.types.Operator): - bl_idname = "bim.set_blender_clash_set_b" - bl_label = "Set Blender Clash Set B" - - def execute(self, context): - while len(bpy.context.scene.BIMProperties.blender_clash_set_b) > 0: - bpy.context.scene.BIMProperties.blender_clash_set_b.remove(0) - for obj in bpy.context.selected_objects: - new = bpy.context.scene.BIMProperties.blender_clash_set_b.add() - new.name = obj.name - return {"FINISHED"} - - -class ExecuteBlenderClash(bpy.types.Operator): - bl_idname = "bim.execute_blender_clash" - bl_label = "Execute Blender Clash" - - def execute(self, context): - blender_clasher = BlenderClasher() - blender_clasher.process_clash_set() - return {"FINISHED"} - - class CleanWireframes(bpy.types.Operator): bl_idname = "bim.clean_wireframes" bl_label = "Clean Wireframes" diff --git a/src/ifcblenderexport/blenderbim/bim/prop.py b/src/ifcblenderexport/blenderbim/bim/prop.py index d9019f2e0e..1579f181f0 100644 --- a/src/ifcblenderexport/blenderbim/bim/prop.py +++ b/src/ifcblenderexport/blenderbim/bim/prop.py @@ -26,12 +26,8 @@ from bpy.props import ( cwd = os.path.dirname(os.path.realpath(__file__)) diagram_scales_enum = [] -profiledef_enum = [] -availablematerialpsets_enum = [] titleblocks_enum = [] materialpsetnames_enum = [] -attributes_enum = [] -materialattributes_enum = [] contexts_enum = [] subcontexts_enum = [] target_views_enum = [] @@ -255,14 +251,22 @@ def getMaterialPsetNames(self, context): def getContexts(self, context): from blenderbim.bim.module.context.data import Data + if not Data.is_loaded: Data.load() results = [] for ifc_id, context in Data.contexts.items(): results.append((str(ifc_id), context["ContextType"], "")) for ifc_id2, subcontext in context["HasSubContexts"].items(): - results.append((str(ifc_id2), "{}/{}/{}".format( - subcontext["ContextType"], subcontext["ContextIdentifier"], subcontext["TargetView"]), "")) + results.append( + ( + str(ifc_id2), + "{}/{}/{}".format( + subcontext["ContextType"], subcontext["ContextIdentifier"], subcontext["TargetView"] + ), + "", + ) + ) return results @@ -372,7 +376,11 @@ class DrawingStyle(PropertyGroup): name: StringProperty(name="Name") raster_style: StringProperty(name="Raster Style") render_type: EnumProperty( - items=[("NONE", "None", ""), ("DEFAULT", "Default", ""), ("VIEWPORT", "Viewport", ""),], + items=[ + ("NONE", "None", ""), + ("DEFAULT", "Default", ""), + ("VIEWPORT", "Viewport", ""), + ], name="Render Type", default="VIEWPORT", ) @@ -397,8 +405,9 @@ class DocProperties(PropertyGroup): ifc_files: CollectionProperty(name="IFCs", type=StrProperty) drawing_styles: CollectionProperty(name="Drawing Styles", type=DrawingStyle) should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=toggleDecorations) - decorations_colour: FloatVectorProperty(name="Decorations Colour", subtype="COLOR", default=(1, 0, 0, 1), - min=0.0, max=1.0, size=4) + decorations_colour: FloatVectorProperty( + name="Decorations Colour", subtype="COLOR", default=(1, 0, 0, 1), min=0.0, max=1.0, size=4 + ) class BIMCameraProperties(PropertyGroup): @@ -448,7 +457,11 @@ class BIMTextProperties(PropertyGroup): name="Font Size", ) symbol: EnumProperty( - items=[("None", "None", ""), ("rectangle-tag", "Rectangle Tag", ""), ("door-tag", "Door Tag", ""),], + items=[ + ("None", "None", ""), + ("rectangle-tag", "Rectangle Tag", ""), + ("door-tag", "Door Tag", ""), + ], update=refreshFontSize, name="Symbol", ) @@ -456,30 +469,6 @@ class BIMTextProperties(PropertyGroup): variables: CollectionProperty(name="Variables", type=Variable) -class ClashSource(PropertyGroup): - name: StringProperty(name="File") - selector: StringProperty(name="Selector") - mode: EnumProperty( - items=[ - ("i", "Include", "Only the selected objects are included for clashing"), - ("e", "Exclude", "All objects except the selected objects are included for clashing"), - ], - name="Mode", - ) - - -class ClashSet(PropertyGroup): - name: StringProperty(name="Name") - tolerance: FloatProperty(name="Tolerance") - a: CollectionProperty(name="Group A", type=ClashSource) - b: CollectionProperty(name="Group B", type=ClashSource) - - -class SmartClashGroup(PropertyGroup): - number: StringProperty(name="Number") - global_ids: CollectionProperty(name="GlobalIDs", type=StrProperty) - - class BIMProperties(PropertyGroup): schema_dir: StringProperty(default=os.path.join(cwd, "schema") + os.path.sep, name="Schema Directory") data_dir: StringProperty(default=os.path.join(cwd, "data") + os.path.sep, name="Data Directory") @@ -494,16 +483,6 @@ class BIMProperties(PropertyGroup): section_plane_colour: FloatVectorProperty( name="Temporary Section Cutaway Colour", subtype="COLOR", default=(1, 0, 0), min=0.0, max=1.0 ) - blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty) - blender_clash_set_b: CollectionProperty(name="Blender Clash Set B", type=StrProperty) - clash_sets: CollectionProperty(name="Clash Sets", type=ClashSet) - should_create_clash_snapshots: BoolProperty(name="Create Snapshots", default=True) - clash_results_path: StringProperty(name="Clash Results Path") - smart_grouped_clashes_path: StringProperty(name="Smart Grouped Clashes Path") - active_clash_set_index: IntProperty(name="Active Clash Set Index") - smart_clash_groups: CollectionProperty(name="Smart Clash Groups", type=SmartClashGroup) - active_smart_group_index: IntProperty(name="Active Smart Group Index") - smart_clash_grouping_max_distance: IntProperty(name="Smart Clash Grouping Max Distance", default=3, soft_min=1, soft_max=10) area_unit: EnumProperty( default="SQUARE_METRE", items=[ diff --git a/src/ifcblenderexport/blenderbim/bim/ui.py b/src/ifcblenderexport/blenderbim/bim/ui.py index cf64e16933..e56b7bf0be 100644 --- a/src/ifcblenderexport/blenderbim/bim/ui.py +++ b/src/ifcblenderexport/blenderbim/bim/ui.py @@ -5,38 +5,6 @@ from bpy.types import Panel from bpy.props import StringProperty -class BIM_PT_object_structural(Panel): - bl_label = "IFC Structural Relationships" - bl_idname = "BIM_PT_object_structural" - bl_options = {"DEFAULT_CLOSED"} - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "object" - - @classmethod - def poll(cls, context): - return context.active_object is not None and hasattr(context.active_object, "BIMObjectProperties") - - def draw(self, context): - if context.active_object is None: - return - layout = self.layout - props = context.active_object.BIMObjectProperties - row = layout.row() - row.prop(props, "has_boundary_condition") - - if bpy.context.active_object.BIMObjectProperties.has_boundary_condition: - row = layout.row() - row.prop(props.boundary_condition, "name") - for index, attribute in enumerate(props.boundary_condition.attributes): - row = layout.row(align=True) - row.prop(attribute, "name", text="") - row.prop(attribute, "string_value", text="") - - row = layout.row() - row.prop(props, "structural_member_connection") - - class BIM_PT_drawings(Panel): bl_label = "SVG Drawings" bl_idname = "BIM_PT_drawings" @@ -341,24 +309,6 @@ class BIM_UL_topics(bpy.types.UIList): layout.label(text="", translate=False) -class BIM_UL_clash_sets(bpy.types.UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - ob = data - if item: - layout.prop(item, "name", text="", emboss=False) - else: - layout.label(text="", translate=False) - - -class BIM_UL_smart_groups(bpy.types.UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - ob = data - if item: - layout.label(text=str(item.number), translate=False, icon="NONE", icon_value=0) - else: - layout.label(text="", translate=False) - - class BIM_ADDON_preferences(bpy.types.AddonPreferences): bl_idname = "blenderbim" svg2pdf_command: StringProperty(name="SVG to PDF Command", description="E.g. [['inkscape', svg, '-o', pdf]]") @@ -387,94 +337,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): row.prop(self, "pdf_command") -class BIM_PT_ifcclash(Panel): - bl_label = "IFC Clash Sets" - bl_idname = "BIM_PT_ifcclash" - bl_options = {"DEFAULT_CLOSED"} - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "scene" - - def draw(self, context): - layout = self.layout - - scene = context.scene - props = scene.BIMProperties - - layout.label(text="Blender Clash:") - - row = layout.row(align=True) - row.operator("bim.set_blender_clash_set_a") - row.operator("bim.set_blender_clash_set_b") - - row = layout.row(align=True) - row.operator("bim.execute_blender_clash") - - layout.label(text="IFC Clash:") - - row = layout.row(align=True) - row.operator("bim.add_clash_set") - row.operator("bim.import_clash_sets", text="", icon="IMPORT") - row.operator("bim.export_clash_sets", text="", icon="EXPORT") - - if not props.clash_sets: - return - - 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] - - row = layout.row(align=True) - row.prop(clash_set, "name") - row.operator("bim.remove_clash_set", icon="X", text="").index = props.active_clash_set_index - - row = layout.row(align=True) - row.prop(clash_set, "tolerance") - - layout.label(text="Group A:") - row = layout.row() - row.operator("bim.add_clash_source").group = "a" - - for index, source in enumerate(clash_set.a): - row = layout.row(align=True) - row.prop(source, "name", text="") - op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="") - op.index = index - op.group = "a" - op = row.operator("bim.remove_clash_source", icon="X", text="") - op.index = index - op.group = "a" - - row = layout.row(align=True) - row.prop(source, "mode", text="") - row.prop(source, "selector", text="") - - layout.label(text="Group B:") - row = layout.row() - row.operator("bim.add_clash_source").group = "b" - - for index, source in enumerate(clash_set.b): - row = layout.row(align=True) - row.prop(source, "name", text="") - op = row.operator("bim.select_clash_source", icon="FILE_FOLDER", text="") - op.index = index - op.group = "b" - op = row.operator("bim.remove_clash_source", icon="X", text="") - op.index = index - op.group = "b" - - row = layout.row(align=True) - row.prop(source, "mode", text="") - row.prop(source, "selector", text="") - - row = layout.row() - row.prop(props, "should_create_clash_snapshots") - row = layout.row(align=True) - row.operator("bim.execute_ifc_clash") - row.operator("bim.select_ifc_clash_results") - - class BIM_PT_annotation_utilities(Panel): bl_idname = "BIM_PT_annotation_utilities" bl_label = "Annotation" @@ -550,46 +412,6 @@ class BIM_PT_annotation_utilities(Panel): layout.prop(props, "decorations_colour") -class BIM_PT_clash_manager(Panel): - bl_idname = "BIM_PT_clash_manager" - bl_label = "Clash Manager" - bl_space_type = "VIEW_3D" - bl_region_type = "UI" - bl_category = "BlenderBIM" - - def draw(self, context): - layout = self.layout - props = context.scene.BIMProperties - - row = layout.row() - layout.label(text="Select clash results to group:") - - row = layout.row(align=True) - row.prop(props, "clash_results_path", text="") - op = row.operator("bim.select_clash_results", icon="FILE_FOLDER", text="") - - row = layout.row() - layout.label(text="Select output path for smart-grouped clashes:") - - row = layout.row(align=True) - row.prop(props, "smart_grouped_clashes_path", text="") - op = row.operator("bim.select_smart_grouped_clashes_path", icon="FILE_FOLDER", text="") - - row = layout.row(align=True) - row.prop(props, "smart_clash_grouping_max_distance") - - row = layout.row(align=True) - row.operator("bim.smart_clash_group") - - row = layout.row(align=True) - row.operator("bim.load_smart_groups_for_active_clash_set") - - layout.template_list("BIM_UL_smart_groups", "", props, "smart_clash_groups", props, "active_smart_group_index") - - row = layout.row(align=True) - row.operator("bim.select_smart_group") - - class BIM_PT_misc_utilities(Panel): bl_idname = "BIM_PT_misc_utilities" bl_label = "Miscellaneous"