diff --git a/src/blenderbim/blenderbim/bim/module/model/__init__.py b/src/blenderbim/blenderbim/bim/module/model/__init__.py index 8444b46fc1..67e4fc014e 100644 --- a/src/blenderbim/blenderbim/bim/module/model/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/model/__init__.py @@ -69,6 +69,7 @@ classes = ( wall.ChangeExtrusionDepth, wall.ChangeExtrusionXAngle, wall.ChangeLayerLength, + wall.DrawPolylineWall, wall.FlipWall, wall.MergeWall, wall.RecalculateWall, diff --git a/src/blenderbim/blenderbim/bim/module/model/wall.py b/src/blenderbim/blenderbim/bim/module/model/wall.py index 726f35143a..35e24eca6d 100644 --- a/src/blenderbim/blenderbim/bim/module/model/wall.py +++ b/src/blenderbim/blenderbim/bim/module/model/wall.py @@ -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. diff --git a/src/blenderbim/blenderbim/bim/module/model/workspace.py b/src/blenderbim/blenderbim/bim/module/model/workspace.py index 5bda1b4916..30128d0c24 100644 --- a/src/blenderbim/blenderbim/bim/module/model/workspace.py +++ b/src/blenderbim/blenderbim/bim/module/model/workspace.py @@ -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: