mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
Basic implementation of clipping planes
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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")]}),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user