gp_interpolate/interpolate_strokes/bind_points.py

385 lines
15 KiB
Python

import bpy
import numpy as np
from time import perf_counter, time, sleep
from mathutils import Vector, Matrix
from .. import utils
from mathutils.geometry import (barycentric_transform,
intersect_point_tri,
intersect_point_line,
intersect_line_plane,
tessellate_polygon)
import math
import gpu
from bpy_extras import view3d_utils
from gpu_extras.batch import batch_for_shader
def raycast_objects(context, event, dg):
"""
Execute ray cast
return object hit, hit world location, normal of hitted face and face index
"""
region = context.region
rv3d = context.region_data
coord = event.mouse_region_x, event.mouse_region_y
view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord)
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord)
ray_target = ray_origin + view_vector
def visible_objects_and_duplis():# -> Generator[tuple, Any, None]:
"""Loop over (object, matrix) pairs (mesh only)"""
# depsgraph = dg ## TRY to used passed depsgraph
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, matrix):
"""Wrapper for ray casting that moves the ray into object space"""
# get the ray relative to the object
matrix_inv = 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
# cast the ray
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
# cast rays and find the closest object
best_length_squared = -1.0
best_obj = None
face_normal = None
hit_loc = None
obj_face_index = None
for obj, matrix in visible_objects_and_duplis():
if obj.type == 'MESH':
hit, normal, face_index = obj_ray_cast(obj, matrix)
if hit is not None:
hit_world = matrix @ hit
# scene.cursor.location = hit_world
length_squared = (hit_world - ray_origin).length_squared
if best_obj is None or length_squared < best_length_squared:
# print('length_squared',length_squared)
best_length_squared = length_squared
best_obj = obj
face_normal = normal
hit_loc = hit_world
obj_face_index = face_index
if best_obj is not None:
best_original = best_obj.original
return best_original, hit_loc, face_normal, obj_face_index
return None, None, None, None
# -----------------
### Drawing
# -----------------
def circle_2d(coord, r, num_segments):
'''create circle, ref: http://slabode.exofire.net/circle_draw.shtml'''
cx, cy = coord
points = []
theta = 2 * 3.1415926 / num_segments
c = math.cos(theta) #precalculate the sine and cosine
s = math.sin(theta)
x = r # we start at angle = 0
y = 0
for i in range(num_segments):
#bgl.glVertex2f(x + cx, y + cy) # output vertex
points.append((x + cx, y + cy))
# apply the rotation matrix
t = x
x = c * x - s * y
y = s * t + c * y
return points
def draw_callback_px(self, context):
if context.area != self.current_area:
return
# 50% alpha, 2 pixel width line
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
gpu.state.blend_set('ALPHA')
gpu.state.line_width_set(2.0)
view_rot = context.region_data.view_rotation
if self.current_points:
for coord in self.current_points:
# circle3d = [(view_normal.to_track_quat('-Z', 'Z') @ cp) + coord for cp in self.circle_pts]
circle3d = [(view_rot @ cp) + coord for cp in self.mini_circle_pts]
circle3d.append(circle3d[0]) # Loop with last point
batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": circle3d})
shader.bind()
shader.uniform_float("color", (0.8, 0.8, 0.0, 0.9))
batch.draw(shader)
# Draw clicked path
if not self.point_list:
return
positions = [pt['co'] for pt in self.point_list]
line_color = color = (0.9, 0.1, 0.2, 0.8)
## Draw lines
if len(self.point_list) >= 3:
## Duplicate first position at last to loop triangle
positions += [self.point_list[0]['co']]
line_color = (0.9, 0.1, 0.3, 1.0)
batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": positions})
shader.bind()
shader.uniform_float("color", line_color)
batch.draw(shader)
# Draw circle on point aligned with view
# view_normal = context.region_data.view_matrix.inverted() @ Vector((0,0,1))
for pt in self.point_list:
coord = pt['co']
# circle3d = [(view_normal.to_track_quat('-Z', 'Z') @ cp) + coord for cp in self.circle_pts]
circle3d = [(view_rot @ cp) + coord for cp in self.circle_pts]
circle3d.append(circle3d[0]) # Loop with last point
batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": circle3d})
shader.bind()
shader.uniform_float("color", color)
batch.draw(shader)
# restore opengl defaults
gpu.state.line_width_set(1.0)
gpu.state.blend_set('NONE')
## POST_PIXEL for text
# -----------------
### Operator
# -----------------
class GP_OT_bind_points(bpy.types.Operator):
bl_idname = "gp.bind_points"
bl_label = "Bind Points"
bl_description = 'Bind points to use as reference for interpolation'
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
clear : bpy.props.BoolProperty(name='Clear', default=False, options={'SKIP_SAVE'})
def invoke(self, context, event):
# print('INVOKE')
self.debug = False
self.gp = context.object
self.settings = context.scene.gp_interpo_settings
self.point_list = []
self.current_points = []
wm = context.window_manager
if tri_dump := wm.get(f'tri_{self.gp.name}'):
if self.clear:
del wm[f'tri_{self.gp.name}']
return {'FINISHED'}
## load from current frame
## Dict to list -> cast to dict (get ID prop array)
self.point_list = [dict(tri_dump[str(i)]) for i in range(3)]
## Update world coordinate position at current frame
dg = bpy.context.evaluated_depsgraph_get()
for point in self.point_list:
ob = bpy.context.scene.objects.get(point['object'])
ob_eval = ob.evaluated_get(dg)
point['co'] = ob_eval.matrix_world @ ob_eval.data.vertices[point['index']].co
if self.clear:
return {'FINISHED'}
## Prepare circle 3D coordinate (create in invoke)
self.circle_pts = [Vector((p[0], p[1], 0)) for p in circle_2d((0,0), 0.01, 12)]
self.mini_circle_pts = [Vector((p[0], p[1], 0)) for p in circle_2d((0,0), 0.005, 12)]
# self._timer = wm.event_timer_add(0.01, window=context.window)
# draw in view space with 'POST_VIEW' and 'PRE_VIEW'
self.current_area = context.area
args = (self, context)
self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_VIEW')
context.area.header_text_set('Bind points | Enter: Valid | Backspace: remove last point | Esc / Right-Click: Cancel')
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}
def exit_modal(self, context, status='INFO', text=None):
# print('Exit modal') # Dbg
## Reset all drawing and report
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
context.area.header_text_set(None)
context.area.tag_redraw()
if text:
self.report({status}, text)
else:
## report standard info
self.report({'INFO'}, 'Done')
def get_closest_vert(self, object_hit, hit_location, _normal, face_index, dg):
ob_eval = object_hit.evaluated_get(dg)
## Get closest index on face and store
face = ob_eval.data.polygons[face_index]
## list(dict)
vertices_infos = [{
'object': object_hit.name, # store object 'name'
'index': vert_idx, # store vertex index
'co': ob_eval.matrix_world @ ob_eval.data.vertices[vert_idx].co # store initial absolute coordinate
}
# vertex: ob_eval.data.vertices[vert_idx],
for vert_idx in face.vertices]
## Filter vertices by closest to hit_location
vertices_infos.sort(key=lambda x: (x['co'] - hit_location).length)
return vertices_infos[0]
def bind(self, context):
## store points on scene/wm properties associated with GP object
context.window_manager[f'tri_{context.object.name}'] = {str(i) : d for i, d in enumerate(self.point_list)}
self.exit_modal(context, text='Bound!')
def modal(self, context, event):
context.area.tag_redraw()
if event.type in ('RIGHTMOUSE', 'ESC'):
context.area.header_text_set(f'Cancelling')
self.exit_modal(context, text='Cancelled')
return {'CANCELLED'}
if event.type in ('WHEELUPMOUSE', 'WHEELDOWNMOUSE', 'MIDDLEMOUSE'):
return {'PASS_THROUGH'}
## disable hint if too intensive
if event.type in {'MOUSEMOVE'}:
## permanent update on closest point position (too heavy to always compute ?)
dg = bpy.context.evaluated_depsgraph_get()
object_hit, hit_location, _normal, face_index = raycast_objects(context, event, bpy.context.evaluated_depsgraph_get())
if object_hit is None:
self.current_points = []
else:
pt = self.get_closest_vert(object_hit, hit_location, _normal, face_index, dg)
self.current_points = [pt['co']]
return {'PASS_THROUGH'}
elif event.type in ('BACK_SPACE', 'DEL') and event.value == 'PRESS':
if self.point_list:
self.report({'INFO'}, 'Removed last point')
self.point_list.pop()
elif event.type in ('RET', 'SPACE') and event.value == 'PRESS':
## Valid
if len(self.point_list) < 3:
self.exit_modal(context, status='ERROR', text='Not enough point selected, Cancelling')
return {'CANCELLED'}
else:
self.bind(context)
return {'FINISHED'}
elif event.type == 'LEFTMOUSE' and event.value == 'PRESS':
if len(self.point_list) >= 3:
# self.report({'WARNING'}, 'Already got 3 point')
self.bind(context)
return {'FINISHED'}
else:
## Raycast surface and store point
dg = bpy.context.evaluated_depsgraph_get()
## Basic rayvast (Do not consider object modifier or instance !)
# mouse = event.mouse_region_x, event.mouse_region_y
# view_mat = context.region_data.view_matrix.inverted()
# origin = view_mat.to_translation()
# depth3d = view_mat @ Vector((0, 0, -1))
# point = utils.region_to_location(mouse, depth3d)
# ray = (point - origin)
# hit, hit_location, normal, face_index, object_hit, matrix = bpy.context.scene.ray_cast(dg, origin, ray)
object_hit, hit_location, _normal, face_index = raycast_objects(context, event, dg)
## Also use triangle coordinate in triangle ?! using raycast with tesselated triangle infos
# object_hit, hit_location, tri, tri_indices = ray_cast_point(point, origin, dg)
if object_hit is None:
self.report({'WARNING'}, 'Nothing hit, Retry on a surface')
else:
# print('object_hit: ', object_hit, object_hit.is_evaluated)
# print('hit_location: ', hit_location)
# print('face_index: ', face_index)
# context.scene.cursor.location = hit_location # Dbg
### // get vert on-place
# ob_eval = object_hit.evaluated_get(dg)
# print('ob_eval: ', ob_eval)
# ## Get closest index on face and store
# face = ob_eval.data.polygons[face_index]
# ## Store list of tuples [(index, world_co, object_hit), ...]
# vertices_infos = [(vert_idx,
# ob_eval.matrix_world @ ob_eval.data.vertices[vert_idx].co,
# object_hit) # Store original object.
# for vert_idx in face.vertices]
# ## Filter vedrtices by closest to hit_location
# vertices_infos.sort(key=lambda x: (x['co'] - hit_location).length)
# vert = vertices_infos[0]
### get vert on-place //
vert = self.get_closest_vert(object_hit, hit_location, _normal, face_index, dg)
# print('vert: ', vert)
# if self.point_list and [x for x in self.point_list if vert[0] == x[0] and vert[3] == x[3]]:
if any(vert['index'] == x['index'] and vert['object'] == x['object'] for x in self.point_list):
self.report({'WARNING'}, "Cannot use same point twice !")
else:
self.point_list += [vert]
self.report({'INFO'}, f"Set point {len(self.point_list)}")
return {'RUNNING_MODAL'}
def execute(self, context):
return {"FINISHED"}
classes = (
GP_OT_bind_points,
)
def register():
for c in classes:
bpy.utils.register_class(c)
def unregister():
for c in reversed(classes):
bpy.utils.unregister_class(c)