OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / utils / space_3d.py

343 lines · python ref:
import bpy
import math
import mathutils
import bmesh
import sys
from mathutils import Vector, Matrix
from bpy_extras import view3d_utils
from .. utility.math import dimensions, coords_to_center


def centroid(points):
    x = [p[0] for p in points]
    y = [p[1] for p in points]
    z = [p[2] for p in points]
    centroid = (sum(x) / len(points), sum(y) / len(points), sum(z) / len(points))

    return centroid


def scale(origin, point, value):
        ox, oy, oz = origin
        px, py, pz = point

        px = (px-ox)*value+ox
        py = (py-oy)*value+oy
        pz = (pz-oz)*value+oz

        return px, py, pz


def transform3D(point, location1, location2):
        px, py, pz = point
        x = [0, 0, 0]

        x[0] = location1[0] - px
        x[1] = location1[1] - py
        x[2] = location1[2] - pz
        px = location2[0] - x[0]
        py = location2[1] - x[1]
        pz = location2[2] - x[2]

        return px, py, pz


def rotate_z(origin, point, angle):
    ox, oy, oz = origin
    px, py, pz = point

    # Z rotation
    qx = ox + math.cos(angle) * (px - ox) - math.sin(angle) * (py - oy)
    qy = oy + math.sin(angle) * (px - ox) + math.cos(angle) * (py - oy)
    qz = pz

    return qx, qy, qz


def rotate_x(origin, point, angle):
    ox, oy, oz = origin
    px, py, pz = point

    # X rotation
    qx = px
    qz = oz + math.sin(angle) * (pz - oz) - math.cos(angle) * (py - oy)
    qy = oy + math.cos(angle) * (pz - oz) + math.sin(angle) * (py - oy)

    return qx, qy, qz


def rotate_y(origin, point, angle):
    ox, oy, oz = origin
    px, py, pz = point

    # Y rotation
    qy = py
    qx = ox + math.sin(angle) * (px - ox) - math.cos(angle) * (pz - oz)
    qz = oz + math.cos(angle) * (px - ox) + math.sin(angle) * (pz - oz)

    return qx, qy, qz


def get_3D_point_from_mouse(mouse_pos: Vector, context: bpy.context, point: Vector, normal: Vector):
        '''Point = The planes origin\n
           Normal = The direction the plane is facing'''

        # get the context arguments
        region = context.region
        rv3d = context.region_data

        intersection = Vector((0,0,0))
        try:
            #Camera Origin
            origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, mouse_pos)

            #Mouse origin
            mouse = view3d_utils.region_2d_to_vector_3d(region, rv3d, mouse_pos)

            #Camera Origin + Mouse
            ray_origin = origin + mouse

            #From the mouse to the viewport
            loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_origin - origin)

            #Ray to plane
            intersection = mathutils.geometry.intersect_line_plane(ray_origin, loc, point, normal)

        except:
            intersection = Vector((0,0,0))

        if(intersection == None):
            intersection = Vector((0,0,0))

        return intersection


def scene_ray_cast(context, event):
    '''Raycast from mouse into scene.'''

    view_layer = context.view_layer if bpy.app.version[:2] < (2, 91) else context.view_layer.depsgraph
    mouse_pos = (event.mouse_region_x, event.mouse_region_y)
    origin = view3d_utils.region_2d_to_origin_3d(bpy.context.region, bpy.context.region_data, mouse_pos)
    direction = view3d_utils.region_2d_to_vector_3d(bpy.context.region, bpy.context.region_data, mouse_pos)

    hit, location, normal, index, object, matrix = context.scene.ray_cast(view_layer, origin, direction)
    return hit, location, normal, index, object, matrix


def get_2d_point_from_3d_point(context, point3d):
    ''' Get a 2D screen point from a 3D point.\n
        Params:\n
        \tcontext : type = bpy.context
        \tpoint3d : type = Vector, desc = the 3D space location
        Returns -> Tuple (0, 0) : ELSE -> None'''

    # Validate
    if hasattr(context, 'region'):
        if hasattr(context, 'space_data'):
            if hasattr(context.space_data, 'region_3d'):
                region = context.region
                rv3d = context.space_data.region_3d
                return view3d_utils.location_3d_to_region_2d(region, rv3d, point3d)

    # Context is incorrect
    return None


def ray_cast_objects(context, origin_world, direction_world, objects, evaluated=True) -> tuple:
    '''Cast a ray on a list of MESH objects\n
        returns: result, location, normal, index, object, matrix'''

    depsgraph = context.evaluated_depsgraph_get()

    if evaluated:
        hit, location, normal, index, obj, matrix = context.scene.ray_cast(context.view_layer if bpy.app.version[:2] < (2, 91) else depsgraph, origin_world, direction_world)

        if not hit:
            return False, Vector((0, 0, 0)), Vector((0, 0,-1)), -1, None, Matrix()

        if obj in set(objects):
            return hit, location, normal, index, obj, matrix

    cast = None
    hit_object = None
    distance = None
    to_clear = []
    removeable_objects = []
    removeable_meshes = []
    removeable_nodes = []

    cast_mesh = bpy.data.meshes.new('tmp')
    cast_obj = bpy.data.objects.new('tmp', cast_mesh)
    bpy.context.collection.objects.link(cast_obj)
    removeable_meshes.append(cast_mesh)
    removeable_objects.append(cast_obj)

        # 4.5+ hack to ensure object evaluation
    if bpy.app.version >= (4, 5, 0):
        geo = cast_obj.modifiers.new('tmp', 'NODES')
        tree = bpy.data.node_groups.new('tmp', 'GeometryNodeTree')
        input = tree.nodes.new('NodeGroupInput')
        output = tree.nodes.new('NodeGroupOutput')
        tree.interface.new_socket('in', in_out='INPUT', socket_type='NodeSocketGeometry')
        tree.interface.new_socket('out', in_out='OUTPUT', socket_type='NodeSocketGeometry')
        tree.links.new(input.outputs[0], output.inputs[0])

        geo.node_group = tree
        removeable_nodes.append(tree)

    bm = bmesh.new()
    bmesh.ops.create_cube(bm)
    bm.to_mesh(cast_mesh)
    bm.free()
    verts_co = [v.co.copy() for v in cast_mesh.vertices]

    def quad_cast(mesh, origin, direction):
        distance = sys.float_info.max
        hit = False
        vec = Vector()
        normal = Vector((0, 0, -1))
        index = -1
        for p in mesh.polygons:

            v1 = mesh.vertices[p.vertices[0]].co
            v2 = mesh.vertices[p.vertices[1]].co
            v3 = mesh.vertices[p.vertices[2]].co

            point = mathutils.geometry.intersect_ray_tri(v1, v2, v3, direction, origin, True)
            if point is not None:
                dist = (point - origin).magnitude
                if dist < distance:
                    hit = True
                    vec = point
                    normal = p.normal
                    index = p.index
                    distance = dist

            else:
                v1 = mesh.vertices[p.vertices[2]].co
                v2 = mesh.vertices[p.vertices[3]].co
                v3 = mesh.vertices[p.vertices[0]].co

                point = mathutils.geometry.intersect_ray_tri(v1, v2, v3, direction, origin, True)

                if point is not None:
                    dist = (point - origin).magnitude
                    if dist < distance:
                        hit = True
                        vec = point
                        normal = p.normal
                        index = p.index
                        distance = dist

        return hit, vec, normal, index

    for obj in objects:
        inverted = obj.matrix_world.inverted()
        orig = inverted @ origin_world
        direction = inverted @ (direction_world + origin_world) - orig
        use_evaluated = evaluated

        bounds = obj.bound_box

        if obj.mode == 'EDIT':
            obj.update_from_editmode()

        if not use_evaluated:
            tmp = bpy.data.objects.new('tmp', obj.data)
            context.collection.objects.link(tmp)
            bounds = [Vector(v) for v in tmp.bound_box]
            bpy.data.objects.remove(tmp)

        center = coords_to_center(bounds)
        sca = dimensions(bounds)
        matrix = Matrix.Translation(center) @ Matrix.Diagonal((*sca, 1))

        for v, co in zip(cast_mesh.vertices, verts_co):
            v.co = matrix @ co

        hit, *_ = quad_cast(cast_mesh, orig, direction)

        if not hit:
            continue

        if obj.mode == 'EDIT' and obj.type == 'MESH':
            obj.update_from_editmode()
            cast_obj.data = obj.data

            hit, location, normal, index = cast_obj.ray_cast(orig, direction)

            if hit:
                location = obj.matrix_world @ location
                dist = location - origin_world

                if dist < distance or not distance:
                    distance = dist
                    cast = (hit, location, normal, index)
                    hit_object = obj
                    hit_mesh = obj.data

        elif obj.mode == 'OBJECT' and obj.type == 'MESH':
            if bpy.app.version >= (4, 5, 0) and obj.data.is_editmode and not obj.modifiers:
                cast_obj.data = obj.data
                hit, location, normal, index = cast_obj.ray_cast(orig, direction)
            else:
                hit, location, normal, index = obj.ray_cast(orig, direction, depsgraph=depsgraph if use_evaluated else None)

            if hit:
                location = obj.matrix_world @ location
                dist = location - origin_world

                if dist < distance or not distance:
                    distance = dist
                    cast = (hit, location, normal, index)
                    hit_object = obj

        else:
            eval_obj = obj.evaluated_get(depsgraph) if use_evaluated else obj
            temp_mesh = bpy.data.meshes.new_from_object(eval_obj)
            cast_obj.data = temp_mesh

            hit, location, normal, index  = cast_obj.ray_cast(orig, direction)

            removeable_meshes.append(temp_mesh)

            if hit:
                location = obj.matrix_world @ location
                dist = location - origin_world

                if dist < distance or not distance:
                    distance = dist
                    cast = (hit, location, normal, index)
                    hit_object = obj

    if not cast:
        cast = (False, Vector((0,0,0)), Vector((0,0,-1)), -1, None, Matrix())

    elif cast[0] and cast[3] < 0: # invalid index from screw modifier
        hit, location, normal, index = cast
        inverted = hit_object.matrix_world.inverted()
        orig = inverted @ origin_world
        direction = inverted @ (direction_world + origin_world) - orig

        mesh = bpy.data.meshes.new_from_object(hit_object.evaluated_get(depsgraph))
        removeable_meshes.append(mesh)
        cast_obj.data = mesh
        _, _, _, index = cast_obj.ray_cast(orig, direction)

        normal = (hit_object.matrix_world.inverted().transposed() @ normal).normalized()
        cast = True, location, normal, index, hit_object, hit_object.matrix_world.copy()

    else:
        hit, location, normal, index = cast
        normal = (hit_object.matrix_world.inverted().transposed() @ normal).normalized()
        cast = True, location, normal, index, hit_object, hit_object.matrix_world.copy()

    for obj in removeable_objects: bpy.data.objects.remove(obj)
    for mesh in  removeable_meshes: bpy.data.meshes.remove(mesh)
    for node in  removeable_nodes: bpy.data.node_groups.remove(node)

    for obj in to_clear:
        obj.to_mesh_clear()

    return cast