diff --git a/src/ifcblenderexport/blenderbim/bim/__init__.py b/src/ifcblenderexport/blenderbim/bim/__init__.py index 7a4e7039cb..690f693251 100644 --- a/src/ifcblenderexport/blenderbim/bim/__init__.py +++ b/src/ifcblenderexport/blenderbim/bim/__init__.py @@ -144,6 +144,7 @@ if bpy is not None: operator.ExecuteIfcClash, operator.SelectIfcClashResults, operator.SmartClashGroup, + operator.IsolateSmartGroup, operator.SwitchContext, operator.RemoveContext, operator.OpenUpstream, @@ -245,6 +246,7 @@ if bpy is not None: prop.DocumentReference, prop.ClashSource, prop.ClashSet, + prop.SmartClashGroup, prop.Constraint, prop.Drawing, prop.Schedule, @@ -324,6 +326,7 @@ if bpy is not None: ui.BIM_PT_misc_utilities, ui.BIM_UL_generic, ui.BIM_UL_clash_sets, + ui.BIM_UL_smart_groups, ui.BIM_UL_constraints, ui.BIM_UL_document_information, ui.BIM_UL_document_references, diff --git a/src/ifcblenderexport/blenderbim/bim/operator.py b/src/ifcblenderexport/blenderbim/bim/operator.py index 2659246200..01c7853312 100644 --- a/src/ifcblenderexport/blenderbim/bim/operator.py +++ b/src/ifcblenderexport/blenderbim/bim/operator.py @@ -11,6 +11,7 @@ import ifcopenshell.util.selector import ifcopenshell.util.geolocation import ifcopenshell.util.pset import tempfile +import numpy as np from . import export_ifc from . import import_ifc from . import qto @@ -29,6 +30,8 @@ from mathutils import Vector, Matrix, Euler, geometry from math import radians, atan, tan, cos, sin from pathlib import Path from bpy.app.handlers import persistent +from sklearn.cluster import OPTICS +from collections import defaultdict colour_list = [ (0.651, 0.81, 0.892, 1), @@ -1889,20 +1892,74 @@ class SmartClashGroup(bpy.types.Operator): return {"RUNNING_MODAL"} def execute(self, context): + import ifcclash + + settings = ifcclash.IfcClashSettings() self.filepath = bpy.path.ensure_ext(self.filepath, ".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 here (but put the heavy lifting code somewhere else in a class) + save_path = r"C:\Users\vince\Desktop\SmartGrouping Demo\smart-groups.json" + smart_grouped_clashes = ifc_clasher.smart_group_clashes(clash_sets) + + # save smart_groups to json + with open(save_path, 'w') as f: + f.write(json.dumps(smart_grouped_clashes)) + + # TODO: load into BIM Properties for easy access + 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(): + #print(smart_group) + #print(type(smart_group)) + + print("Global Ids: ", global_id_pairs) + print(type(global_id_pairs)) + new_group = bpy.context.scene.BIMProperties.smart_clash_groups.add() + new_group.number = smart_group + + for pair in global_id_pairs: + print("id: ", pair) + for id in pair: + new_global_id = new_group.global_ids.add() + new_global_id.name = id + + return {"FINISHED"} + +class IsolateSmartGroup(bpy.types.Operator): + bl_idname = "bim.isolate_smart_group" + bl_label = "Isolate 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 smart_groups in clash_sets.values(): - for smart_group_list in smart_groups: - for smart_group, clashes in smart_group_list.items(): - for id in clashes: - if global_id.string_value in id: - print("object match: ", global_id) - obj.select_set(True) + 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"} diff --git a/src/ifcblenderexport/blenderbim/bim/prop.py b/src/ifcblenderexport/blenderbim/bim/prop.py index 8879ed87b6..19e6f2ea2b 100644 --- a/src/ifcblenderexport/blenderbim/bim/prop.py +++ b/src/ifcblenderexport/blenderbim/bim/prop.py @@ -803,6 +803,10 @@ class PresentationLayer(PropertyGroup): layer_frozen: BoolProperty(name="LayerFrozen", default=False) layer_blocked: BoolProperty(name="LayerBlocked", default=False) +class SmartClashGroup(PropertyGroup): + number: IntProperty(name="Number") + global_ids: CollectionProperty(name="GlobalIDs", type=StrProperty) + class Constraint(PropertyGroup): name: StringProperty(name="Name") @@ -1519,6 +1523,8 @@ class BIMProperties(PropertyGroup): blender_clash_set_b: CollectionProperty(name="Blender Clash Set B", type=StrProperty) clash_sets: CollectionProperty(name="Clash Sets", type=ClashSet) 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") constraints: CollectionProperty(name="Constraints", type=Constraint) active_constraint_index: IntProperty(name="Active Constraint Index") eastings: StringProperty(name="Eastings") diff --git a/src/ifcblenderexport/blenderbim/bim/ui.py b/src/ifcblenderexport/blenderbim/bim/ui.py index 13e1591454..78950996a9 100644 --- a/src/ifcblenderexport/blenderbim/bim/ui.py +++ b/src/ifcblenderexport/blenderbim/bim/ui.py @@ -2102,6 +2102,14 @@ class BIM_UL_clash_sets(bpy.types.UIList): 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) + #layout.prop(item, "number", text="", emboss=False, icon_value=icon) + else: + layout.label(text="", translate=False) class BIM_UL_constraints(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): @@ -2370,6 +2378,12 @@ class BIM_PT_clash_manager(Panel): row = layout.row(align=True) row.operator("bim.smart_clash_group") + + layout.template_list('BIM_UL_smart_groups', '', props, 'smart_clash_groups', props, 'active_smart_group_index') + + # TODO: add operator to isolate smart groups one by one in the viewport + row = layout.row(align=True) + row.operator("bim.isolate_smart_group") class BIM_PT_misc_utilities(Panel): bl_idname = "BIM_PT_misc_utilities" diff --git a/src/ifcclash/ifcclash.py b/src/ifcclash/ifcclash.py index 320a9ef5d8..0233b6b84a 100644 --- a/src/ifcclash/ifcclash.py +++ b/src/ifcclash/ifcclash.py @@ -10,7 +10,8 @@ import json import sys import argparse import logging - +from sklearn.cluster import OPTICS +from collections import defaultdict class Mesh: faces: [] @@ -267,6 +268,63 @@ class IfcClasher: if element.ObjectPlacement.RelativePlacement.RefDirection: element.ObjectPlacement.RelativePlacement.RefDirection.DirectionRatios = (1.0, 0.0, 0.0) + def smart_group_clashes(self, clash_sets): + count_of_input_clashes = 0 + count_of_clash_sets = 0 + count_of_smart_groups = 0 + count_of_final_clash_sets = 0 + + count_of_clash_sets = len(clash_sets) + + for clash_set in clash_sets: + if not "clashes" in clash_set.keys(): + print(f"Skipping clash set [{clash_set['name']}] since it contains no clash results.") + continue + clashes = clash_set["clashes"] + count_of_input_clashes += len(clashes) + + positions = [] + for clash in clashes.values(): + positions.append(clash["position"]) + + data = np.array(positions) + + # INPUTS + # set the desired maximum distance between the grouped points + max_distance_between_grouped_points = 3 + model = OPTICS(min_samples=2, max_eps=max_distance_between_grouped_points) + model.fit_predict(data) + pred = model.fit_predict(data) + + # Insert the smart groups into the clashes + if len(pred) == len(clashes.values()): + i = 0 + for clash in clashes.values(): + clash["smart_group"] = int(pred[i]) + i += 1 + + # Create JSON with smart_groups that contain GlobalIDs + output_clash_sets = defaultdict(list) + for clash_set in clash_sets: + if not "clashes" in clash_set.keys(): + continue + smart_groups = defaultdict(list) + for clash_id, content in clash_set['clashes'].items(): + if "smart_group" in content: + object_id_list = list() + # Clash has been grouped, let's extract it. + object_id_list.append(content['a_global_id']) + object_id_list.append(content['b_global_id']) + smart_groups[content['smart_group']].append(object_id_list) + count_of_smart_groups += len(smart_groups) + output_clash_sets[clash_set["name"]].append(smart_groups) + + count_of_final_clash_sets = len(output_clash_sets) + print(f"Took {count_of_input_clashes} clashes in {count_of_clash_sets} clash sets and turned", + f"them into {count_of_smart_groups} smart groups in {count_of_final_clash_sets} clash sets") + + return output_clash_sets + class IfcClashSettings: def __init__(self):