diff --git a/interpolate_strokes/operators.py b/interpolate_strokes/operators.py index 288ae5c..f63c007 100644 --- a/interpolate_strokes/operators.py +++ b/interpolate_strokes/operators.py @@ -1,163 +1,23 @@ import bpy import numpy as np - -from math import tan, acos, degrees from time import perf_counter from mathutils import Vector, Matrix -from gp_interpolate import utils +from gp_interpolate.utils import (matrix_transform, + plane_on_bone, + ray_cast_point, + intersect_with_tesselated_plane, + triangle_normal, + search_square, + get_gp_draw_plane) -from mathutils.geometry import (barycentric_transform, intersect_point_tri, - intersect_point_line, intersect_line_plane, tessellate_polygon) +from mathutils.geometry import (barycentric_transform, + intersect_point_tri, + intersect_point_line, + intersect_line_plane, + tessellate_polygon) -def get_gp_draw_plane(obj=None): - ''' return tuple with plane coordinate and normal - of the curent drawing according to geometry''' - - if obj is None: - obj = bpy.context.object - - settings = bpy.context.scene.tool_settings - orient = settings.gpencil_sculpt.lock_axis #'VIEW', 'AXIS_Y', 'AXIS_X', 'AXIS_Z', 'CURSOR' - loc = settings.gpencil_stroke_placement_view3d #'ORIGIN', 'CURSOR', 'SURFACE', 'STROKE' - - mat = obj.matrix_world - plane_no = Vector((0.0, 0.0, 1.0)) - plane_co = mat.to_translation() - - # -> orientation - if orient == 'VIEW': - mat = bpy.context.scene.camera.matrix_world - # -> placement - if loc == "CURSOR": - plane_co = bpy.context.scene.cursor.location - mat = bpy.context.scene.cursor.matrix - - elif orient == 'AXIS_Y':#front (X-Z) - plane_no = Vector((0,1,0)) - - elif orient == 'AXIS_X':#side (Y-Z) - plane_no = Vector((1,0,0)) - - elif orient == 'AXIS_Z':#top (X-Y) - plane_no = Vector((0,0,1)) - - plane_no.rotate(mat) - - return plane_co, plane_no - -def triangle_normal(a, b, c): - x = a[1] * b[2] - a[2] * b[1] - y = a[2] * b[0] - a[0] * b[2] - z = a[0] * b[1] - a[1] * b[0] - - return np.array([x, y, z], dtype='float64') - -def plane_coords(size=1): - v = size * 0.5 - return np.array([(-v, v, 0), (v, v, 0), (v, -v, 0), (-v, -v, 0)], dtype='float64') - -def matrix_transform(coords, matrix): - coords_4d = np.column_stack((coords, np.ones(len(coords), dtype='float64'))) - return np.einsum('ij,aj->ai', matrix, coords_4d)[:, :-1] - -def vector_normalized(vec): - return vec / np.sqrt(np.sum(vec**2)) - -def vector_magnitude(vec): - return np.sqrt(vec.dot(vec)) - -def search_square(point, factor=0.05, cam=None): - if cam is None: - cam = bpy.context.scene.camera - - plane = plane_coords() - mat = cam.matrix_world.copy() - mat.translation = point - depth = vector_magnitude(point - cam.matrix_world.to_translation()) - mat_scale = Matrix.Scale(tan(cam.data.angle*0.5)*depth*factor, 4) - - return matrix_transform(plane, mat @ mat_scale) - -def ray_cast_point(point, origin, depsgraph): - ray = (point - origin)#.normalized() - hit, hit_location, normal, face_index, object_hit, matrix = bpy.context.scene.ray_cast(depsgraph, origin, ray) - - if not hit: - return None, None, None, None - - eval_ob = object_hit.evaluated_get(depsgraph) - - face = eval_ob.data.polygons[face_index] - vertices = [eval_ob.data.vertices[i] for i in face.vertices] - face_co = matrix_transform([v.co for v in vertices], eval_ob.matrix_world) - - tri = None - for tri_idx in tessellate_polygon([face_co]): - tri = [face_co[i] for i in tri_idx] - tri_indices = [vertices[i].index for i in tri_idx] - if intersect_point_tri(hit_location, *tri): - break - - return object_hit, np.array(hit_location), tri, tri_indices - - -def plane_on_bone(bone, arm=None, cam=None, set_rotation=True): - if cam is None: - cam = bpy.context.scene.camera - - if arm is None: - arm = bone.id_data - - plane = plane_coords() - mat = cam.matrix_world.copy() - - - if set_rotation: - head_world_coord = arm.matrix_world @ bone.head - mat.translation = head_world_coord - - ## Apply 2d bone rotation facing camera - - # Get 2d camera space coords (NDC: normalized device coordinate, 0,0 is bottom-left) - head_2d, tail_2d = utils.get_bone_head_tail_2d(bone, cam=cam) - vec_from_corner_2d = (tail_2d - head_2d).normalized() - up_vec_2d = Vector((0,1)) - # angle = acos(up_vec_2d.dot(vec_from_corner_2d)) ## equivalent but not signed! - angle = up_vec_2d.angle_signed(vec_from_corner_2d) - - ## Axis camera aim (seem slightly off) - # rot_axis = Vector((0, 0, -1)) - # rot_axis.rotate(cam.matrix_world) - - ## Axis camera origin -> pivot - rot_axis = head_world_coord - cam.matrix_world.translation - - mat = utils.rotate_matrix_around_pivot(mat, angle, head_world_coord, rot_axis) - - else: - ## Use mid bone to better follow movement - mat.translation = arm.matrix_world @ ((bone.tail + bone.head) / 2) # Mid bone - - mat_scale = Matrix.Scale(10, 4) - return matrix_transform(plane, mat @ mat_scale) - -def intersect_with_tesselated_plane(point, origin, face_co): - ''' - face_co: World face coordinates - ''' - - tri = None - for tri_idx in tessellate_polygon([face_co]): - tri = [face_co[i] for i in tri_idx] - tri_indices = [i for i in tri_idx] - hit_location = intersect_line_plane(origin, point, sum((Vector(v) for v in tri), Vector()) / 3, triangle_normal(*tri)) - if intersect_point_tri(hit_location, *tri): - break - - return np.array(hit_location), tri, tri_indices - def following_key(forward=True): direction = 1 if forward else -1 cur_frame = bpy.context.scene.frame_current diff --git a/utils.py b/utils.py index b6fcb7c..d458c7e 100644 --- a/utils.py +++ b/utils.py @@ -1,8 +1,127 @@ import bpy import math +import numpy as np + +from math import tan from mathutils import Vector, Matrix from bpy_extras.object_utils import world_to_camera_view +from mathutils.geometry import (barycentric_transform, intersect_point_tri, + intersect_point_line, intersect_line_plane, tessellate_polygon) + +# --- Vector + +def triangle_normal(a, b, c): + x = a[1] * b[2] - a[2] * b[1] + y = a[2] * b[0] - a[0] * b[2] + z = a[0] * b[1] - a[1] * b[0] + + return np.array([x, y, z], dtype='float64') + +def plane_coords(size=1): + v = size * 0.5 + return np.array([(-v, v, 0), (v, v, 0), (v, -v, 0), (-v, -v, 0)], dtype='float64') + +def matrix_transform(coords, matrix): + coords_4d = np.column_stack((coords, np.ones(len(coords), dtype='float64'))) + return np.einsum('ij,aj->ai', matrix, coords_4d)[:, :-1] + +def vector_normalized(vec): + return vec / np.sqrt(np.sum(vec**2)) + +def vector_magnitude(vec): + return np.sqrt(vec.dot(vec)) + +def search_square(point, factor=0.05, cam=None): + if cam is None: + cam = bpy.context.scene.camera + + plane = plane_coords() + mat = cam.matrix_world.copy() + mat.translation = point + depth = vector_magnitude(point - cam.matrix_world.to_translation()) + mat_scale = Matrix.Scale(tan(cam.data.angle*0.5)*depth*factor, 4) + + return matrix_transform(plane, mat @ mat_scale) + +def ray_cast_point(point, origin, depsgraph): + ray = (point - origin)#.normalized() + hit, hit_location, normal, face_index, object_hit, matrix = bpy.context.scene.ray_cast(depsgraph, origin, ray) + + if not hit: + return None, None, None, None + + eval_ob = object_hit.evaluated_get(depsgraph) + + face = eval_ob.data.polygons[face_index] + vertices = [eval_ob.data.vertices[i] for i in face.vertices] + face_co = matrix_transform([v.co for v in vertices], eval_ob.matrix_world) + + tri = None + for tri_idx in tessellate_polygon([face_co]): + tri = [face_co[i] for i in tri_idx] + tri_indices = [vertices[i].index for i in tri_idx] + if intersect_point_tri(hit_location, *tri): + break + + return object_hit, np.array(hit_location), tri, tri_indices + + +def plane_on_bone(bone, arm=None, cam=None, set_rotation=True): + if cam is None: + cam = bpy.context.scene.camera + + if arm is None: + arm = bone.id_data + + plane = plane_coords() + mat = cam.matrix_world.copy() + + + if set_rotation: + head_world_coord = arm.matrix_world @ bone.head + mat.translation = head_world_coord + + ## Apply 2d bone rotation facing camera + + # Get 2d camera space coords (NDC: normalized device coordinate, 0,0 is bottom-left) + head_2d, tail_2d = utils.get_bone_head_tail_2d(bone, cam=cam) + vec_from_corner_2d = (tail_2d - head_2d).normalized() + up_vec_2d = Vector((0,1)) + # angle = acos(up_vec_2d.dot(vec_from_corner_2d)) ## equivalent but not signed! + angle = up_vec_2d.angle_signed(vec_from_corner_2d) + + ## Axis camera aim (seem slightly off) + # rot_axis = Vector((0, 0, -1)) + # rot_axis.rotate(cam.matrix_world) + + ## Axis camera origin -> pivot + rot_axis = head_world_coord - cam.matrix_world.translation + + mat = utils.rotate_matrix_around_pivot(mat, angle, head_world_coord, rot_axis) + + else: + ## Use mid bone to better follow movement + mat.translation = arm.matrix_world @ ((bone.tail + bone.head) / 2) # Mid bone + + mat_scale = Matrix.Scale(10, 4) + return matrix_transform(plane, mat @ mat_scale) + +def intersect_with_tesselated_plane(point, origin, face_co): + ''' + face_co: World face coordinates + ''' + + tri = None + for tri_idx in tessellate_polygon([face_co]): + tri = [face_co[i] for i in tri_idx] + tri_indices = [i for i in tri_idx] + hit_location = intersect_line_plane(origin, point, sum((Vector(v) for v in tri), Vector()) / 3, triangle_normal(*tri)) + if intersect_point_tri(hit_location, *tri): + break + + return np.array(hit_location), tri, tri_indices + def get_bone_head_tail_2d(posebone, scene=None, cam=None) -> tuple[Vector, Vector]: '''Get 2D vectors in camera view of bone head and tails @@ -49,4 +168,43 @@ def rotate_matrix_around_pivot(matrix, angle, pivot, axis): # Combine the transformations : The order of multiplication is important new_matrix = translate_back @ rot_matrix @ translate_to_origin @ matrix - return new_matrix \ No newline at end of file + return new_matrix + + +# --- GREASE PENCIL + +def get_gp_draw_plane(obj=None): + ''' return tuple with plane coordinate and normal + of the curent drawing according to geometry''' + + if obj is None: + obj = bpy.context.object + + settings = bpy.context.scene.tool_settings + orient = settings.gpencil_sculpt.lock_axis #'VIEW', 'AXIS_Y', 'AXIS_X', 'AXIS_Z', 'CURSOR' + loc = settings.gpencil_stroke_placement_view3d #'ORIGIN', 'CURSOR', 'SURFACE', 'STROKE' + + mat = obj.matrix_world + plane_no = Vector((0.0, 0.0, 1.0)) + plane_co = mat.to_translation() + + # -> orientation + if orient == 'VIEW': + mat = bpy.context.scene.camera.matrix_world + # -> placement + if loc == "CURSOR": + plane_co = bpy.context.scene.cursor.location + mat = bpy.context.scene.cursor.matrix + + elif orient == 'AXIS_Y':#front (X-Z) + plane_no = Vector((0,1,0)) + + elif orient == 'AXIS_X':#side (Y-Z) + plane_no = Vector((1,0,0)) + + elif orient == 'AXIS_Z':#top (X-Y) + plane_no = Vector((0,0,1)) + + plane_no.rotate(mat) + + return plane_co, plane_no \ No newline at end of file