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
This commit is contained in:
Andrej730
2024-03-01 14:57:23 +05:00
parent a75f5ef66f
commit 708c61e33a
4 changed files with 146 additions and 1 deletions
@@ -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():
+125 -1
View File
@@ -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)
+1
View File
@@ -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):
+17
View File
@@ -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: