diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py
index 044f7f644e..3d2b486cc8 100644
--- a/src/blenderbim/blenderbim/bim/module/project/__init__.py
+++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py
@@ -17,7 +17,7 @@
# along with BlenderBIM Add-on. If not, see .
import bpy
-from . import ui, prop, operator, workspace
+from . import ui, prop, operator, workspace, gizmo
classes = (
operator.AppendEntireLibrary,
@@ -57,6 +57,8 @@ classes = (
operator.QueryLinkedElement,
operator.EnableCulling,
operator.DisableCulling,
+ operator.CreateClippingPlane,
+ operator.RefreshClippingPlanes,
prop.LibraryElement,
prop.FilterCategory,
prop.Link,
@@ -70,6 +72,7 @@ classes = (
ui.BIM_UL_library,
ui.BIM_UL_filter_categories,
ui.BIM_UL_links,
+ gizmo.ClippingPlane,
)
diff --git a/src/blenderbim/blenderbim/bim/module/project/decorator.py b/src/blenderbim/blenderbim/bim/module/project/decorator.py
index 1aa80d2645..916687a8da 100644
--- a/src/blenderbim/blenderbim/bim/module/project/decorator.py
+++ b/src/blenderbim/blenderbim/bim/module/project/decorator.py
@@ -86,3 +86,103 @@ class ProjectDecorator:
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)
+
+
+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)
diff --git a/src/blenderbim/blenderbim/bim/module/project/gizmo.py b/src/blenderbim/blenderbim/bim/module/project/gizmo.py
new file mode 100644
index 0000000000..7e8bd77614
--- /dev/null
+++ b/src/blenderbim/blenderbim/bim/module/project/gizmo.py
@@ -0,0 +1,67 @@
+# BlenderBIM Add-on - OpenBIM Blender Add-on
+# Copyright (C) 2024 Dion Moult
+#
+# 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 .
+
+
+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
diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py
index 0f67cbf14a..a650cf99d9 100644
--- a/src/blenderbim/blenderbim/bim/module/project/operator.py
+++ b/src/blenderbim/blenderbim/bim/module/project/operator.py
@@ -41,7 +41,7 @@ from pathlib import Path
from mathutils import Vector, Matrix
from bpy.app.handlers import persistent
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):
@@ -1993,7 +1993,7 @@ class EnableCulling(bpy.types.Operator):
# Check if the object is too far away from the camera
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:
# The object is too far away, so consider it not visible
return False
@@ -2020,3 +2020,178 @@ class DisableCulling(bpy.types.Operator):
def execute(self, context):
LinksData.enable_culling = False
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)
diff --git a/src/blenderbim/blenderbim/bim/module/project/prop.py b/src/blenderbim/blenderbim/bim/module/project/prop.py
index cd968e8173..fd618f97d1 100644
--- a/src/blenderbim/blenderbim/bim/module/project/prop.py
+++ b/src/blenderbim/blenderbim/bim/module/project/prop.py
@@ -20,7 +20,7 @@ import bpy
import ifcopenshell.util.placement
from blenderbim.bim.module.project.data import ProjectData
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.props import (
PointerProperty,
@@ -171,6 +171,7 @@ class BIMProjectProperties(PropertyGroup):
template_file: EnumProperty(items=get_template_file, name="Template File")
use_relative_project_path: BoolProperty(name="Use Relative Project Path", default=False)
queried_obj: bpy.props.PointerProperty(type=bpy.types.Object)
+ clipping_planes: bpy.props.CollectionProperty(type=ObjProperty)
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))
diff --git a/src/blenderbim/blenderbim/bim/module/project/workspace.py b/src/blenderbim/blenderbim/bim/module/project/workspace.py
index 308d494146..e6bd3b9122 100644
--- a/src/blenderbim/blenderbim/bim/module/project/workspace.py
+++ b/src/blenderbim/blenderbim/bim/module/project/workspace.py
@@ -22,15 +22,15 @@ from blenderbim.bim.module.project.data import LinksData
class QueryTool(bpy.types.WorkSpaceTool):
- bl_space_type = 'VIEW_3D'
- bl_context_mode = 'OBJECT'
+ bl_space_type = "VIEW_3D"
+ bl_context_mode = "OBJECT"
bl_idname = "bim.query_tool"
bl_label = "Query Tool"
bl_description = "Fetch data about a linked IFC element"
bl_icon = "ops.generic.select_circle"
bl_widget = None
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")]}),
)