From 708c61e33a1ab5feece0bfb3de60834e3ad16f76 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 1 Mar 2024 14:57:23 +0500 Subject: [PATCH] Clipping planes with cappings Added new experimental section that allows you to cut objects based on current clipping planes in the scene. The difference from other clipping plane methods - it actually (temporarily) edits meshes geometry and adds cappings at the cut. So, the clipped meshes can be seen both on render and cutted parts won't get in the way as they do with temporary section cutaways (shader based method) Example - https://imgur.com/a/SOqovOM --- src/blenderbim/blenderbim/bim/__init__.py | 3 + src/blenderbim/blenderbim/bim/operator.py | 126 +++++++++++++++++++++- src/blenderbim/blenderbim/bim/prop.py | 1 + src/blenderbim/blenderbim/bim/ui.py | 17 +++ 4 files changed, 146 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 3fc0ea758a..387e832647 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -99,6 +99,7 @@ classes = [ operator.BIM_OT_open_webbrowser, operator.BIM_OT_remove_section_plane, operator.BIM_OT_show_description, + operator.ClippingPlaneCutWithCappings, operator.EditBlenderCollection, operator.FileAssociate, operator.FileUnassociate, @@ -106,6 +107,7 @@ classes = [ operator.OpenUri, operator.ReloadIfcFile, operator.RemoveIfcFile, + operator.RevertClippingPlaneCut, operator.SelectDataDir, operator.SelectIfcFile, operator.SelectSchemaDir, @@ -175,6 +177,7 @@ classes = [ ui.BIM_PT_tab_sandbox, # TODO: move this somewhere else and clean it up ui.BIM_PT_section_plane, + ui.BIM_PT_section_with_cappings, ] for mod in modules.values(): diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index 799846df72..40b519a725 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -18,6 +18,7 @@ import os import bpy +import bmesh import json import time import logging @@ -39,6 +40,8 @@ from blenderbim.bim.helper import get_enum_items from mathutils import Vector, Matrix, Euler from math import radians from pathlib import Path +from collections import namedtuple +from typing import List class SetTab(bpy.types.Operator): @@ -417,7 +420,7 @@ class BIM_OT_add_section_plane(bpy.types.Operator): group.interface.new_socket(name="Line Decorator", in_out="INPUT", socket_type="NodeSocketFloat") group.interface.new_socket(name="Value", in_out="OUTPUT", socket_type="NodeSocketFloat") group.interface.new_socket(name="Line Decorator", in_out="OUTPUT", socket_type="NodeSocketFloat") - else: + else: group.inputs.new("NodeSocketFloat", "Value") group.inputs["Value"].default_value = 1.0 # Mandatory multiplier for the last node group group.inputs.new("NodeSocketVector", "Vector") @@ -975,3 +978,124 @@ class BIM_OT_show_description(bpy.types.Operator): @classmethod def description(cls, context, properties): return properties.description + + +CuttingPlaneData = namedtuple("CuttingPlaneData", ["co", "normal"]) + + +class ClippingPlaneCutWithCappings(bpy.types.Operator): + bl_idname = "bim.clipping_plane_cut_with_cappings" + bl_label = "Cut With Clipping Planes" + bl_description = "Cut selected objects with clipping planes and create cappings" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + cutting_planes = [p.obj for p in context.scene.BIMProjectProperties.clipping_planes] + if not cutting_planes: + self.report({"INFO"}, "No cutting planes found.") + return {"FINISHED"} + cutting_planes_data = self.get_cutting_plane_data(cutting_planes) + + wm = context.window_manager + objects_processed, t0 = 0, time.time() + wm.progress_begin(0, len(context.selected_objects)) + for obj_i, obj in enumerate(context.selected_objects): + if obj.type != "MESH": + continue + + if obj in cutting_planes: + continue + + RevertClippingPlaneCut.revert_object_mesh(self, obj) + + # localize matrix to consider object's transform + ws_to_ls = obj.matrix_world.inverted() + rotation = ws_to_ls.to_quaternion() + + mesh = obj.data + bm = tool.Blender.get_bmesh_for_mesh(mesh) + object_changed = False + + for plane_data in cutting_planes_data: + geom = bm.verts[:] + bm.edges[:] + bm.faces[:] + plane_co = ws_to_ls @ plane_data.co + plane_no = rotation @ plane_data.normal + # clear_outer -> remove everything in direction of the normal + results = bmesh.ops.bisect_plane( + bm, + geom=geom, + dist=10e-4, + plane_co=plane_co, + plane_no=plane_no, + clear_outer=True, + ) + edges_to_fill = [e for e in results["geom_cut"] if isinstance(e, bmesh.types.BMEdge)] + object_changed = object_changed or edges_to_fill + bmesh.ops.contextual_create(bm, geom=edges_to_fill) + + # don't swap mesh if it wasn't affected by any of the cutting planes + if object_changed: + temp_mesh = bpy.data.meshes.new("temp_cut") + temp_mesh.BIMMeshProperties.replaced_mesh = mesh + for material in mesh.materials: + temp_mesh.materials.append(material) + obj.data = temp_mesh + tool.Blender.apply_bmesh(temp_mesh, bm, obj) + + objects_processed += 1 + wm.progress_update(obj_i) + + self.report({"INFO"}, f"{objects_processed} processed - {time.time()-t0:.3f} sec") + + return {"FINISHED"} + + def get_cutting_plane_data(self, cutting_planes: List[bpy.types.Object]) -> List[CuttingPlaneData]: + cutting_planes_data = [] + + for obj in cutting_planes: + cutting_matrix = obj.matrix_world + plane_data = CuttingPlaneData(cutting_matrix.translation, cutting_matrix.col[2].to_3d()) + cutting_planes_data.append(plane_data) + + return cutting_planes_data + + # NOTE: unused, will be used later for cutting boxes support + def get_box_cutting_plane_data(self, obj: bpy.types.Object) -> List[CuttingPlaneData]: + matrix_world = obj.matrix_world + rotation = matrix_world.to_quaternion() # avoid scale for normals + cutting_planes_data = [] + for p in obj.data.polygons: + plane_data = CuttingPlaneData(matrix_world @ p.center, rotation @ p.normal) + cutting_planes_data.append(plane_data) + return cutting_planes_data + + +class RevertClippingPlaneCut(bpy.types.Operator): + bl_idname = "bim.revert_clipping_plane_cut" + bl_label = "Revert Clipping Plane Cut" + bl_description = "Revert clipping plane cut (switch back to the original mesh)" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + obj = context.active_object + wm = context.window_manager + + objects_processed, t0 = 0, time.time() + wm.progress_begin(0, len(context.selected_objects)) + for obj_i, obj in enumerate(context.selected_objects): + if obj.type != "MESH": + continue + self.revert_object_mesh(obj) + objects_processed += 1 + wm.progress_update(obj_i) + wm.progress_end() + + self.report({"INFO"}, f"{objects_processed} processed - {time.time()-t0:.3f} sec") + return {"FINISHED"} + + def revert_object_mesh(self, obj): + mesh = obj.data + replaced_mesh = mesh.BIMMeshProperties.replaced_mesh + if replaced_mesh: + obj.data = replaced_mesh + tool.Blender.remove_data_block(mesh, skip_unlink=True) diff --git a/src/blenderbim/blenderbim/bim/prop.py b/src/blenderbim/blenderbim/bim/prop.py index 6e00811b26..d2309ac97b 100644 --- a/src/blenderbim/blenderbim/bim/prop.py +++ b/src/blenderbim/blenderbim/bim/prop.py @@ -475,6 +475,7 @@ class BIMMeshProperties(PropertyGroup): ifc_parameters: CollectionProperty(name="IFC Parameters", type=IfcParameter) material_checksum: StringProperty(name="Material Checksum", default="[]") mesh_checksum: StringProperty(name="Mesh Checksum", default="") + replaced_mesh: PointerProperty(type=bpy.types.Mesh, description="Original mesh to revert section cutaway") class BIMFacet(PropertyGroup): diff --git a/src/blenderbim/blenderbim/bim/ui.py b/src/blenderbim/blenderbim/bim/ui.py index 13a0f9e6a6..bdc74c516a 100644 --- a/src/blenderbim/blenderbim/bim/ui.py +++ b/src/blenderbim/blenderbim/bim/ui.py @@ -111,6 +111,23 @@ class BIM_PT_section_plane(Panel): row.operator("bim.remove_section_plane") +class BIM_PT_section_with_cappings(Panel): + bl_label = "Section Cutaways With Cappings" + bl_idname = "BIM_PT_section_with_cappings" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "output" + bl_options = {"DEFAULT_CLOSED"} + bl_parent_id = "BIM_PT_tab_sandbox" + + def draw(self, context): + layout = self.layout + wm = context.window_manager + row = layout.row(align=True) + row.operator("bim.clipping_plane_cut_with_cappings", icon="XRAY", text="Cut") + row.operator("bim.revert_clipping_plane_cut", icon="FILE_REFRESH", text="Revert Cut") + + class BIM_UL_generic(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: