mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 01:41:57 +00:00
First implementation. Two types of raycast and basic snaping for faces,
edges and edge center.
This commit is contained in:
committed by
Bruno Perdigão
parent
2938c1fd0b
commit
94ca2423e9
@@ -69,6 +69,7 @@ classes = (
|
||||
wall.ChangeExtrusionDepth,
|
||||
wall.ChangeExtrusionXAngle,
|
||||
wall.ChangeLayerLength,
|
||||
wall.DrawPolylineWall,
|
||||
wall.FlipWall,
|
||||
wall.MergeWall,
|
||||
wall.RecalculateWall,
|
||||
|
||||
@@ -36,7 +36,10 @@ import blenderbim.core.model as core
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from math import pi, sin, cos, degrees
|
||||
from time import time
|
||||
from mathutils import Vector, Matrix
|
||||
from mathutils.bvhtree import BVHTree
|
||||
from bpy_extras import view3d_utils
|
||||
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
|
||||
from typing import Optional
|
||||
|
||||
@@ -279,6 +282,222 @@ def recalculate_dumb_wall_origin(wall, new_origin=None):
|
||||
child.matrix_parent_inverse = wall.matrix_world.inverted()
|
||||
|
||||
|
||||
class DrawPolylineWall(bpy.types.Operator):
|
||||
bl_idname = "bim.draw_polyline_wall"
|
||||
bl_label = "Draw Polyline Wall"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
ray_cast_type = bpy.props.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.space_data.type == 'VIEW_3D'
|
||||
|
||||
|
||||
def main(self, context, event):
|
||||
scene = context.scene
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
mouse_pos = event.mouse_region_x, event.mouse_region_y
|
||||
|
||||
offset = 0.4
|
||||
mouse_offset = (
|
||||
(-offset, offset), (0, offset), (offset, offset),
|
||||
(-offset, 0), (0, 0), (offset, 0),
|
||||
(-offset, -offset), (0, -offset), (offset, -offset)
|
||||
|
||||
)
|
||||
|
||||
# Plane to intersect. Default Container
|
||||
plane_origin = Vector((0, 0, 0))
|
||||
plane_normal = Vector((0, 0, 1))
|
||||
|
||||
|
||||
def get_viewport_ray_data():
|
||||
view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, mouse_pos)
|
||||
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, mouse_pos)
|
||||
ray_target = ray_origin + view_vector
|
||||
ray_direction = ray_target - ray_origin
|
||||
|
||||
return ray_origin, ray_target, ray_direction
|
||||
|
||||
|
||||
def get_object_ray_data(obj_matrix):
|
||||
matrix_inv = obj_matrix.inverted()
|
||||
ray_origin_obj = matrix_inv @ ray_origin
|
||||
ray_target_obj = matrix_inv @ ray_target
|
||||
ray_direction_obj = ray_target_obj - ray_origin_obj
|
||||
|
||||
return ray_origin_obj, ray_target_obj, ray_direction_obj
|
||||
|
||||
|
||||
def get_visible_objects():
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
for dup in depsgraph.object_instances:
|
||||
if dup.is_instance: # Real dupli instance
|
||||
obj = dup.instance_object
|
||||
yield (obj, dup.matrix_world.copy())
|
||||
else: # Usual object
|
||||
obj = dup.object
|
||||
yield (obj, obj.matrix_world.copy())
|
||||
|
||||
|
||||
def obj_ray_cast(obj):
|
||||
ray_origin_obj, _, ray_direction_obj = get_object_ray_data(obj.matrix_world.copy())
|
||||
success, location, normal, face_index = obj.ray_cast(ray_origin_obj, ray_direction_obj)
|
||||
if success:
|
||||
return location, normal, face_index
|
||||
else:
|
||||
return None, None, None
|
||||
|
||||
|
||||
def bvh_ray_cast(obj):
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
bvh = BVHTree.FromObject(obj, depsgraph)
|
||||
#BVH uses objects local matrix
|
||||
ray_origin_obj, _, ray_direction_obj = get_object_ray_data(obj.matrix_local.copy())
|
||||
location, normal, face_index, distance = bvh.ray_cast(ray_origin_obj, ray_direction_obj)
|
||||
if location:
|
||||
return location, normal, face_index
|
||||
else:
|
||||
return None, None, None
|
||||
pass
|
||||
|
||||
|
||||
def ray_cast_to_plane(mouse_pos, context, plane_origin, plane_normal):
|
||||
intersection = Vector ((0,0,0,))
|
||||
try:
|
||||
loc = view3d_utils.region_2d_to_location_3d(
|
||||
region, rv3d, mouse_pos, ray_direction
|
||||
)
|
||||
intersection = mathutils.geometry.intersect_line_plane(ray_target, loc, plane_origin, plane_normal)
|
||||
except:
|
||||
intersection = Vector((0,0,0))
|
||||
|
||||
if intersection == None:
|
||||
intersection = Vector((0,0,0))
|
||||
|
||||
return intersection
|
||||
|
||||
|
||||
def cast_rays_and_get_best_object():
|
||||
best_length_squared = 1.0
|
||||
best_obj = None
|
||||
best_hit = None
|
||||
best_face_index = None
|
||||
|
||||
for obj, matrix in get_visible_objects():
|
||||
if obj.type == 'MESH':
|
||||
hit, normal, face_index = obj_ray_cast(obj) if self.ray_cast_type == 'OBJ' else bvh_ray_cast(obj)
|
||||
if hit is not None:
|
||||
hit_world = matrix @ hit
|
||||
length_squared = (hit_world - ray_origin).length_squared
|
||||
if best_obj is None or length_squared < best_length_squared:
|
||||
best_length_squared = length_squared
|
||||
best_obj = obj
|
||||
best_hit = hit_world
|
||||
best_face_index = face_index
|
||||
|
||||
if best_obj is not None:
|
||||
return best_obj, best_hit, best_face_index
|
||||
|
||||
else:
|
||||
return None, None, None
|
||||
|
||||
|
||||
def get_snap_points(obj, face_index):
|
||||
snap_points = {}
|
||||
matrix = obj.matrix_world.copy()
|
||||
face = obj.data.polygons[face_index]
|
||||
vertices = []
|
||||
for i in face.vertices:
|
||||
vertices.append(matrix @ obj.data.vertices[i].co)
|
||||
|
||||
edges_center = []
|
||||
for v1, v2 in zip(vertices, vertices[1:] + [vertices[0]]):
|
||||
edges_center.append((v1-v2) / 2)
|
||||
|
||||
snap_points.update({tuple(vertex): "Vertices" for vertex in vertices})
|
||||
snap_points.update({tuple(edge_center): "Edge Center" for edge_center in edges_center})
|
||||
|
||||
return snap_points
|
||||
|
||||
|
||||
def select_snap_point(snap_points, hit, threshold):
|
||||
shortest_distance = None
|
||||
snap_point = None
|
||||
for point, snap_type in snap_points.items():
|
||||
point = Vector(point)
|
||||
distance = (point - hit).length
|
||||
if distance > threshold:
|
||||
continue
|
||||
if shortest_distance and distance < shortest_distance:
|
||||
shortest_distance = distance
|
||||
snap_point = (point, snap_type)
|
||||
|
||||
elif not shortest_distance:
|
||||
shortest_distance = distance
|
||||
snap_point = (point, snap_type)
|
||||
else:
|
||||
pass
|
||||
|
||||
return snap_point
|
||||
|
||||
|
||||
snap_threshold = 0.3
|
||||
ray_origin, ray_target, ray_direction = get_viewport_ray_data()
|
||||
obj, hit, face_index = cast_rays_and_get_best_object()
|
||||
intersection = ray_cast_to_plane(mouse_pos, context, plane_origin, plane_normal)
|
||||
|
||||
if obj is not None:
|
||||
original_obj = obj.original
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
original_obj.select_set(True)
|
||||
context.view_layer.objects.active = original_obj
|
||||
|
||||
snap_points = get_snap_points(obj, face_index)
|
||||
snap_point = select_snap_point(snap_points, hit, snap_threshold)
|
||||
if snap_point:
|
||||
scene.cursor.location = snap_point[0]
|
||||
else:
|
||||
scene.cursor.location = hit
|
||||
|
||||
else:
|
||||
pass
|
||||
# bpy.ops.object.select_all(action="DESELECT")
|
||||
# scene.cursor.location = intersection
|
||||
|
||||
|
||||
def modal(self, context, event):
|
||||
if event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
|
||||
return {'PASS_THROUGH'}
|
||||
elif event.type == 'MOUSEMOVE':
|
||||
start_time = time()
|
||||
self.main(context, event)
|
||||
operator_time = time() - start_time
|
||||
print(f"main function was finished in {operator_time:.2f} seconds")
|
||||
return {'RUNNING_MODAL'}
|
||||
elif event.type == 'O':
|
||||
self.ray_cast_type = 'OBJ'
|
||||
print("OBJ selected.")
|
||||
elif event.type == 'B':
|
||||
self.ray_cast_type = 'BVH'
|
||||
print("BVH selected.")
|
||||
elif event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||
print("CANCELLLL")
|
||||
return {'CANCELLED'}
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
def invoke(self, context, event):
|
||||
if context.space_data.type == 'VIEW_3D':
|
||||
context.window_manager.modal_handler_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
self.report({'WARNING'}, "Active space must be a View3d")
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
class DumbWallAligner:
|
||||
# An alignment shifts the origin of all walls to the closest point on the
|
||||
# local X axis of the reference wall. In addition, the Z rotation is copied.
|
||||
|
||||
@@ -50,6 +50,7 @@ class BimTool(WorkSpaceTool):
|
||||
("bim.hotkey", {"type": "K", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_K")]}),
|
||||
("bim.hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}),
|
||||
("bim.hotkey", {"type": "O", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_O")]}),
|
||||
("bim.hotkey", {"type": "P", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_P")]}),
|
||||
("bim.hotkey", {"type": "L", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_L")]}),
|
||||
("bim.hotkey", {"type": "Q", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Q")]}),
|
||||
("bim.hotkey", {"type": "R", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_R")]}),
|
||||
@@ -841,6 +842,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.props.y = self.y
|
||||
self.props.z = self.z
|
||||
|
||||
def hotkey_S_P(self):
|
||||
bpy.ops.bim.draw_polyline_wall("INVOKE_DEFAULT")
|
||||
|
||||
def hotkey_S_L(self):
|
||||
if AuthoringData.data["active_class"] in ("IfcOpeningElement",):
|
||||
if len(bpy.context.selected_objects) == 2:
|
||||
|
||||
Reference in New Issue
Block a user