Basic implementation of clipping planes

This commit is contained in:
Dion Moult
2024-02-26 23:10:45 +11:00
parent 6f56d78e83
commit e0eea85cfb
6 changed files with 353 additions and 7 deletions
@@ -17,7 +17,7 @@
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>. # along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy import bpy
from . import ui, prop, operator, workspace from . import ui, prop, operator, workspace, gizmo
classes = ( classes = (
operator.AppendEntireLibrary, operator.AppendEntireLibrary,
@@ -57,6 +57,8 @@ classes = (
operator.QueryLinkedElement, operator.QueryLinkedElement,
operator.EnableCulling, operator.EnableCulling,
operator.DisableCulling, operator.DisableCulling,
operator.CreateClippingPlane,
operator.RefreshClippingPlanes,
prop.LibraryElement, prop.LibraryElement,
prop.FilterCategory, prop.FilterCategory,
prop.Link, prop.Link,
@@ -70,6 +72,7 @@ classes = (
ui.BIM_UL_library, ui.BIM_UL_library,
ui.BIM_UL_filter_categories, ui.BIM_UL_filter_categories,
ui.BIM_UL_links, ui.BIM_UL_links,
gizmo.ClippingPlane,
) )
@@ -86,3 +86,103 @@ class ProjectDecorator:
if selected_edges: if selected_edges:
self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges) self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges)
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris) self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris)
class ClippingPlaneDecorator:
installed = None
@classmethod
def install(cls, context):
if cls.installed:
cls.uninstall()
handler = cls()
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
@classmethod
def uninstall(cls):
try:
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
except ValueError:
pass
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def __call__(self, context):
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
def transparent_color(color, alpha=0.1):
color = [i for i in color]
color[3] = alpha
return color
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
selected_vertices = []
selected_edges = []
selected_tris = []
unselected_vertices = []
unselected_edges = []
unselected_tris = []
for clipping_plane in context.scene.BIMProjectProperties.clipping_planes:
obj = clipping_plane.obj
if not obj or not obj.data:
continue
if obj.mode == "EDIT":
continue # A profile decorator or something else is used here.
bm = bmesh.new()
bm.from_mesh(obj.data)
obj.data.calc_loop_triangles()
if obj.select_get():
offset = len(selected_vertices)
selected_vertices.extend([tuple(obj.matrix_world @ v.co) for v in bm.verts])
selected_edges.extend([tuple([v.index + offset for v in e.verts]) for e in bm.edges])
selected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
else:
offset = len(unselected_vertices)
unselected_vertices.extend([tuple(obj.matrix_world @ v.co) for v in bm.verts])
unselected_edges.extend([tuple([v.index + offset for v in e.verts]) for e in bm.edges])
unselected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
verts = [
tuple(obj.matrix_world @ Vector((0, 0, 0))),
tuple(obj.matrix_world @ Vector((0, 0, -0.5))),
tuple(obj.matrix_world @ Vector((-0.05, 0, -0.45))),
tuple(obj.matrix_world @ Vector((0.05, 0, -0.45))),
tuple(obj.matrix_world @ Vector((0, -0.05, -0.45))),
tuple(obj.matrix_world @ Vector((0, 0.05, -0.45))),
]
edges = [(0, 1), (1, 2), (1, 3), (1, 4), (1, 5)]
color = selected_elements_color if obj in context.selected_objects else special_elements_color
self.draw_batch("LINES", verts, color, edges)
if obj.mode != "EDIT":
bm.free()
if unselected_edges:
self.draw_batch("LINES", unselected_vertices, special_elements_color, unselected_edges)
self.draw_batch("TRIS", unselected_vertices, transparent_color(special_elements_color), unselected_tris)
if selected_edges:
self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges)
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris)
@@ -0,0 +1,67 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import bpy
from bpy.types import GizmoGroup
from mathutils import Matrix
class ClippingPlane(GizmoGroup):
bl_idname = "OBJECT_GGT_bim_clipping_plane"
bl_label = "Clipping Plane"
bl_space_type = "VIEW_3D"
bl_region_type = "WINDOW"
bl_options = {"3D", "PERSISTENT"}
@classmethod
def poll(cls, context):
obj = context.object
return (
context.selected_objects
and obj.name.startswith("ClippingPlane")
and obj in [sp.obj for sp in context.scene.BIMProjectProperties.clipping_planes]
)
def setup(self, context):
self.obj = None
self.offset = 0
self.mw = Matrix()
self.last_mw = Matrix()
self.gizmo = self.gizmos.new("GIZMO_GT_arrow_3d")
def move_get_x():
return self.offset
def move_set_x(value):
self.obj.matrix_world.col[3] = self.mw.col[3] + (self.mw.col[2] * self.offset)
self.last_mw = self.obj.matrix_world.copy()
self.offset = value
self.gizmo.target_set_handler("offset", get=move_get_x, set=move_set_x)
def refresh(self, context):
if self.obj != context.object or self.last_mw != context.object.matrix_world:
self.obj = context.object
self.offset = 0
self.mw = context.object.matrix_world.copy()
obj = context.object
mw = context.object.matrix_world.normalized()
mw.col[3] -= mw.col[2] * self.offset
self.gizmo.matrix_basis = mw
@@ -41,7 +41,7 @@ from pathlib import Path
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
from bpy.app.handlers import persistent from bpy.app.handlers import persistent
from blenderbim.bim.module.project.data import LinksData from blenderbim.bim.module.project.data import LinksData
from blenderbim.bim.module.project.decorator import ProjectDecorator from blenderbim.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator
class NewProject(bpy.types.Operator): class NewProject(bpy.types.Operator):
@@ -1993,7 +1993,7 @@ class EnableCulling(bpy.types.Operator):
# Check if the object is too far away from the camera # Check if the object is too far away from the camera
object_center = obj.matrix_world.translation object_center = obj.matrix_world.translation
distance_threshold = 900 # 30m squared distance_threshold = 900 # 30m squared
if (camera_position - object_center).length_squared > distance_threshold: if (camera_position - object_center).length_squared > distance_threshold:
# The object is too far away, so consider it not visible # The object is too far away, so consider it not visible
return False return False
@@ -2020,3 +2020,178 @@ class DisableCulling(bpy.types.Operator):
def execute(self, context): def execute(self, context):
LinksData.enable_culling = False LinksData.enable_culling = False
return {"FINISHED"} return {"FINISHED"}
class RefreshClippingPlanes(bpy.types.Operator):
bl_idname = "bim.refresh_clipping_planes"
bl_label = "Refresh Clipping Planes"
bl_options = {"REGISTER"}
def __init__(self):
self.total_planes = 0
def invoke(self, context, event):
context.window_manager.modal_handler_add(self)
return {"RUNNING_MODAL"}
def modal(self, context, event):
should_refresh = False
self.clean_deleted_planes(context)
for clipping_plane in context.scene.BIMProjectProperties.clipping_planes:
if clipping_plane.obj and self.is_moved(clipping_plane.obj):
should_refresh = True
break
total_planes = len(context.scene.BIMProjectProperties.clipping_planes)
if should_refresh or total_planes != self.total_planes:
self.refresh_clipping_planes(context)
for clipping_plane in context.scene.BIMProjectProperties.clipping_planes:
if clipping_plane.obj:
tool.Geometry.record_object_position(clipping_plane.obj)
self.total_planes = total_planes
return {"PASS_THROUGH"}
def clean_deleted_planes(self, context):
while True:
for i, clipping_plane in enumerate(context.scene.BIMProjectProperties.clipping_planes):
if clipping_plane.obj:
try:
clipping_plane.obj.name
except:
context.scene.BIMProjectProperties.clipping_planes.remove(i)
break
else:
context.scene.BIMProjectProperties.clipping_planes.remove(i)
break
else:
break
def is_moved(self, obj):
if not obj.BIMObjectProperties.location_checksum:
return True # Let's be conservative
loc_check = np.frombuffer(eval(obj.BIMObjectProperties.location_checksum))
rot_check = np.frombuffer(eval(obj.BIMObjectProperties.rotation_checksum))
loc_real = np.array(obj.matrix_world.translation).flatten()
rot_real = np.array(obj.matrix_world.to_3x3()).flatten()
if np.allclose(loc_check, loc_real, atol=1e-4) and np.allclose(rot_check, rot_real, atol=1e-2):
return False
return True
def refresh_clipping_planes(self, context):
import bmesh
from itertools import cycle
area = next(a for a in bpy.context.screen.areas if a.type == "VIEW_3D")
region = next(r for r in area.regions if r.type == "WINDOW")
data = region.data
if not len(context.scene.BIMProjectProperties.clipping_planes):
data.use_clip_planes = False
else:
with bpy.context.temp_override(area=area, region=region):
bpy.ops.view3d.clip_border()
clip_planes = []
for clipping_plane in bpy.context.scene.BIMProjectProperties.clipping_planes:
obj = clipping_plane.obj
if not obj:
continue
print('doing', obj)
bm = bmesh.new()
bm.from_mesh(obj.data)
world_matrix = obj.matrix_world
bm.faces.ensure_lookup_table()
face = bm.faces[0]
center = world_matrix @ face.calc_center_median()
print('center', center)
normal = world_matrix.to_3x3() @ face.normal * -1
center += normal * -0.01
print('normal', normal)
print('new center', center)
normal.normalize()
distance = -center.dot(normal)
clip_plane = (normal.x, normal.y, normal.z, distance)
clip_planes.append(clip_plane)
bm.free()
clip_planes = cycle(clip_planes)
data.clip_planes = [tuple(next(clip_planes)) for i in range(0, 6)]
data.update()
region.tag_redraw()
[a.tag_redraw() for a in bpy.context.screen.areas]
return {"FINISHED"}
class CreateClippingPlane(bpy.types.Operator):
bl_idname = "bim.create_clipping_plane"
bl_label = "Create Clipping Plane"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.area.type == "VIEW_3D"
def execute(self, context):
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d
# Clean up deleted planes
if len(bpy.context.scene.BIMProjectProperties.clipping_planes) > 5:
self.report({"INFO"}, "Maximum of six clipping planes allowed.")
return {"FINISHED"}
for area in bpy.context.screen.areas:
if area.type == "VIEW_3D":
area.tag_redraw()
region = context.region
rv3d = context.region_data
coord = (self.mouse_x, self.mouse_y)
origin = region_2d_to_origin_3d(region, rv3d, coord)
direction = region_2d_to_vector_3d(region, rv3d, coord)
hit, location, normal, face_index, obj, matrix = self.ray_cast(context, origin, direction)
if not hit:
self.report({"INFO"}, "No object found.")
return {"FINISHED"}
print(hit, location, normal, face_index, obj, matrix)
vertices = [(-0.5, -0.5, 0), (0.5, -0.5, 0), (0.5, 0.5, 0), (-0.5, 0.5, 0)]
faces = [(0, 1, 2, 3)]
mesh = bpy.data.meshes.new(name="ClippingPlane")
mesh.from_pydata(vertices, [], faces)
mesh.update()
plane_obj = bpy.data.objects.new("ClippingPlane", mesh)
bpy.context.collection.objects.link(plane_obj)
z_axis = Vector((0, 0, 1))
rotation_matrix = z_axis.rotation_difference(normal).to_matrix().to_4x4()
plane_obj.matrix_world = rotation_matrix
plane_obj.matrix_world.translation = location
bpy.context.scene.cursor.location = location
new = bpy.context.scene.BIMProjectProperties.clipping_planes.add()
new.obj = plane_obj
ClippingPlaneDecorator.install(bpy.context)
bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
return {"FINISHED"}
def ray_cast(self, context, origin, direction):
depsgraph = context.evaluated_depsgraph_get()
result = context.scene.ray_cast(depsgraph, origin, direction)
return result
def invoke(self, context, event):
self.mouse_x = event.mouse_region_x
self.mouse_y = event.mouse_region_y
return self.execute(context)
@@ -20,7 +20,7 @@ import bpy
import ifcopenshell.util.placement import ifcopenshell.util.placement
from blenderbim.bim.module.project.data import ProjectData from blenderbim.bim.module.project.data import ProjectData
from blenderbim.bim.ifc import IfcStore from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.prop import StrProperty from blenderbim.bim.prop import StrProperty, ObjProperty
from bpy.types import PropertyGroup from bpy.types import PropertyGroup
from bpy.props import ( from bpy.props import (
PointerProperty, PointerProperty,
@@ -171,6 +171,7 @@ class BIMProjectProperties(PropertyGroup):
template_file: EnumProperty(items=get_template_file, name="Template File") template_file: EnumProperty(items=get_template_file, name="Template File")
use_relative_project_path: BoolProperty(name="Use Relative Project Path", default=False) use_relative_project_path: BoolProperty(name="Use Relative Project Path", default=False)
queried_obj: bpy.props.PointerProperty(type=bpy.types.Object) queried_obj: bpy.props.PointerProperty(type=bpy.types.Object)
clipping_planes: bpy.props.CollectionProperty(type=ObjProperty)
def get_library_element_index(self, lib_element): def get_library_element_index(self, lib_element):
return next((i for i in range(len(self.library_elements)) if self.library_elements[i] == lib_element)) return next((i for i in range(len(self.library_elements)) if self.library_elements[i] == lib_element))
@@ -22,15 +22,15 @@ from blenderbim.bim.module.project.data import LinksData
class QueryTool(bpy.types.WorkSpaceTool): class QueryTool(bpy.types.WorkSpaceTool):
bl_space_type = 'VIEW_3D' bl_space_type = "VIEW_3D"
bl_context_mode = 'OBJECT' bl_context_mode = "OBJECT"
bl_idname = "bim.query_tool" bl_idname = "bim.query_tool"
bl_label = "Query Tool" bl_label = "Query Tool"
bl_description = "Fetch data about a linked IFC element" bl_description = "Fetch data about a linked IFC element"
bl_icon = "ops.generic.select_circle" bl_icon = "ops.generic.select_circle"
bl_widget = None bl_widget = None
bl_keymap = ( bl_keymap = (
("bim.query_linked_element", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None), ("bim.query_linked_element", {"type": "LEFTMOUSE", "value": "PRESS"}, None),
# ("bim.project_hotkey", {"type": "C", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_C")]}), # ("bim.project_hotkey", {"type": "C", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_C")]}),
) )