New triangle interpolation method and fixes

- Triangle mode:
  - Add bind modal to set points
  - interpolate based on user defined
triangle bound to geometry
- changed: in geometry mode, points out of geometry should follow more consistantly (needs more testing)
- Better method names is UI
- fix : error when strokes are not fully selected
master
pullusb 2024-07-22 18:03:43 +02:00
parent fca531bf35
commit 94f24ad5f6
8 changed files with 487 additions and 25 deletions

View File

@ -1,7 +1,7 @@
bl_info = {
"name": "gp interpolate",
"author": "Christophe Seux, Samuel Bernou",
"version": (0, 7, 4),
"version": (0, 8, 0),
"blender": (3, 6, 0),
"location": "Sidebar > Gpencil Tab > Interpolate",
"description": "Interpolate Grease pencil strokes over 3D",

View File

@ -1,13 +1,15 @@
from gp_interpolate.interpolate_strokes import (operators,
properties,
debug,
bind_points,
#interpolate_simple
)
modules = (
properties,
operators,
debug
debug,
bind_points,
#interpolate_simple,
)

View File

@ -0,0 +1,384 @@
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)

View File

@ -174,13 +174,28 @@ class GP_OT_debug_geometry_interpolation_targets(bpy.types.Operator):
object_hit, hit_location, tri, tri_indices = ray_cast_point(square_co, origin, dg)
is_hit = 'HIT' if object_hit else ''
empty_at(name=f'{is_hit}search_{iteration}({ct})', pos=square_co, collection=debug_col, size=0.001, show_name=True) # type='SPHERE'
## Show all search square elements
# empty_at(name=f'{is_hit}{i}search_{iteration}({ct})', pos=square_co, collection=debug_col, size=0.001, show_name=False) # type='SPHERE'
## Show only hit location (at hit location)
if is_hit:
empty_at(name=f'{is_hit}{i}search_{iteration}({ct})', pos=hit_location, collection=debug_col, size=0.001, show_name=False) # type='SPHERE'
if object_hit: # and object_hit in col.all_objects[:]:
context.scene.cursor.location = hit_location
## Get location coplanar with triangle
hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri))
empty_at(name=f'{object_hit.name}_{i}-{iteration}', pos=hit_location, collection=debug_col, type='SPHERE', size=0.001, show_name=True)
## On location coplanar with face triangle
# hit_location = intersect_line_plane(origin, point_co_world, hit_location, triangle_normal(*tri))
## On view plane
view_vec = context.scene.camera.matrix_world.to_quaternion() @ Vector((0,0,1))
hit_location = intersect_line_plane(origin, point_co_world, hit_location, view_vec)
## An average of the two ?
# hit_location_1 = intersect_line_plane(origin, point_co_world, hit_location, triangle_normal(*tri))
# hit_location_2 = intersect_line_plane(origin, point_co_world, hit_location, view_vec)
# hit_location = (hit_location_1 + hit_location_2) / 2
empty_at(name=f'{object_hit.name}_{i}-{iteration}', pos=hit_location, collection=debug_col, type='SPHERE', size=0.001, show_name=False)
found = True
# print(f'{si}:{i} iteration {iteration}') # Dbg
break
@ -196,7 +211,7 @@ class GP_OT_debug_geometry_interpolation_targets(bpy.types.Operator):
else:
## Hit at original position
print(object_hit.name)
empty_at(name=f'{object_hit.name}_{i}', pos=hit_location, collection=debug_col, type='SPHERE', size=0.001, show_name=True)
empty_at(name=f'{object_hit.name}_{i}', pos=hit_location, collection=debug_col, type='SPHERE', size=0.001, show_name=False)
print('Done')
return {'FINISHED'}

View File

@ -75,6 +75,12 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
self.frames_to_jump = None
self.cancelled = False
## Remove interpolation_plane collection ! (unseen, but can be hit)
if interp_plane := bpy.data.objects.get('interpolation_plane'):
bpy.data.objects.remove(interp_plane)
if interp_col := bpy.data.collections.get('interpolation_tool'):
bpy.data.collections.remove(interp_col)
context.window_manager.modal_handler_add(self)
self._timer = context.window_manager.event_timer_add(0.01, window=context.window)
context.area.header_text_set('Starting interpolation | Esc: Cancel')
@ -127,14 +133,16 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
# (frame: {self.frames_to_jump[self.loop_count]})
if self.status == 'START':
if self.settings.method == 'TRI' and not context.window_manager.get(f'tri_{self.gp.name}'):
self.exit_modal(context, status='ERROR', text='Need to bind coordinate first. Use "Bind Tri Point" button')
return {'CANCELLED'}
scn = bpy.context.scene
## Determine on what key/keys to jump
self.frames_to_jump = following_keys(forward=self.next, all_keys=self.settings.use_animation)
if not len(self.frames_to_jump):
self.exit_modal(context, status='WARNING', text='No keyframe available in this direction')
return {'CANCELLED'}
# print('self.frames_to_jump: ', self.frames_to_jump)
self.gp = context.object
# matrix = np.array(self.gp.matrix_world, dtype='float64')
@ -154,6 +162,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
self.exit_modal(context, status='ERROR', text='No active frame')
return {'CANCELLED'}
## Get strokes to interpolate
tgt_strokes = [s for s in self.gp.data.layers.active.active_frame.strokes if s.select]
## If nothing selected in sculpt/paint, Select all before triggering
@ -169,6 +178,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
included_cols = [c.name for c in self.gp.users_collection]
target_obj = None
## Setup depending on method
if self.settings.method == 'BONE':
if not self.settings.target_rig or not self.settings.target_bone:
self.exit_modal(context, status='ERROR', text='No Bone selected')
@ -197,12 +207,6 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
target_obj = self.plane
elif self.settings.method == 'GEOMETRY':
## Remove interpolation_plane collection ! (unseen, but can be hit)
if interp_plane := bpy.data.objects.get('interpolation_plane'):
bpy.data.objects.remove(interp_plane)
if interp_col := bpy.data.collections.get('interpolation_tool'):
bpy.data.collections.remove(interp_col)
if col != context.scene.collection:
included_cols.append(col.name)
## Maybe include a plane just behind geo ? probably bad idea
@ -257,6 +261,20 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
# Override collection
col = intercol
if self.settings.method == 'TRI':
point_dict = context.window_manager.get(f'tri_{self.gp.name}')
## point_dict -> {'0': {'object': object_name_as_str, 'index': 450}, ...}
## Get triangle dumped in context.window_manager
self.object_hit_source = object_hit = [bpy.context.scene.objects.get(point_dict[str(i)]['object']) for i in range(3)]
self.tri_indices_source = tri_indices = [point_dict[str(i)]['index'] for i in range(3)] # List of vertices index corresponding to tri coordinates
tri = []
dg = bpy.context.evaluated_depsgraph_get()
for source_obj, idx in zip(object_hit, tri_indices):
ob_eval = source_obj.evaluated_get(dg)
tri.append(ob_eval.matrix_world @ ob_eval.data.vertices[idx].co)
dg = bpy.context.evaluated_depsgraph_get()
self.strokes_data = []
@ -271,6 +289,17 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
stroke_data = []
for i, point in enumerate(stroke.points):
point_co_world = world_co_3d[i]
if self.settings.method == 'TRI':
## Set hit location at same coordinate as point
# hit_location = point_co_world
## Set hit location on tri plane
hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri))
# In this case object_hit is a list of object !
stroke_data.append((stroke, point_co_world, object_hit, hit_location, tri, tri_indices))
continue
if target_obj:
object_hit, hit_location, tri, tri_indices = obj_ray_cast(target_obj, Vector(point_co_world), origin, dg)
else:
@ -290,8 +319,18 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
object_hit, hit_location, tri, tri_indices = ray_cast_point(square_co, origin, dg)
if object_hit:
## Get location coplanar with triangle
hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri))
## On location coplanar with face triangle
# hit_location = intersect_line_plane(origin, point_co_world, hit_location, triangle_normal(*tri))
## On view plane
view_vec = context.scene.camera.matrix_world.to_quaternion() @ Vector((0,0,1))
hit_location = intersect_line_plane(origin, point_co_world, hit_location, view_vec)
## An average of the two ?
# hit_location_1 = intersect_line_plane(origin, point_co_world, hit_location, triangle_normal(*tri))
# hit_location_2 = intersect_line_plane(origin, point_co_world, hit_location, view_vec)
# hit_location = (hit_location_1 + hit_location_2) / 2
found = True
# print(f'{si}:{i} iteration {iteration}') # Dbg
break
@ -318,6 +357,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
print(f'Scan time {self.scan_time:.4f}s')
# Copy stroke selection
bpy.ops.gpencil.select_linked() # Ensure whole stroke are selected before copy
bpy.ops.gpencil.copy()
# Jump frame and paste
@ -343,18 +383,26 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
arm=self.settings.target_rig,
set_rotation=self.settings.use_bone_rotation,
mesh=True)
dg = bpy.context.evaluated_depsgraph_get()
matrix_inv = np.array(self.gp.matrix_world.inverted(), dtype='float64')#.inverted()
new_strokes = self.gp.data.layers.active.active_frame.strokes[-len(self.strokes_data):]
if self.settings.method == 'TRI':
## All points have the same new frame triangle (tri_b)
tri_b = []
for source_obj, idx in zip(self.object_hit_source, self.tri_indices_source):
ob_eval = source_obj.evaluated_get(dg)
tri_b.append(ob_eval.matrix_world @ ob_eval.data.vertices[idx].co)
# for new_stroke, stroke_data in zip(new_strokes, self.strokes_data):
for new_stroke, stroke_data in zip(reversed(new_strokes), reversed(self.strokes_data)):
world_co_3d = []
for stroke, point_co, object_hit, hit_location, tri_a, tri_indices in stroke_data:
eval_ob = object_hit.evaluated_get(dg)
tri_b = [eval_ob.data.vertices[i].co for i in tri_indices]
tri_b = matrix_transform(tri_b, eval_ob.matrix_world)
if not self.settings.method == 'TRI':
eval_ob = object_hit.evaluated_get(dg)
tri_b = [eval_ob.data.vertices[i].co for i in tri_indices]
tri_b = matrix_transform(tri_b, eval_ob.matrix_world)
new_loc = barycentric_transform(hit_location, *tri_a, *tri_b)
# try:

View File

@ -18,8 +18,9 @@ class GP_PG_interpolate_settings(PropertyGroup):
name='Method',
items= (
('GEOMETRY', 'Geometry', 'Directly follow underlying geometry', 0),
('BONE', 'Bone', 'Pick an armature bone and follow it', 1),
('OBJECT', 'Object', 'Directly follow a specific object, even if occluded', 2),
('OBJECT', 'Object Geometry', 'Same as Geometry mode, but target only a specific object, even if occluded (ignore all the others)', 1),
('BONE', 'Bone', 'Pick an armature bone and follow it', 2),
('TRI', 'Triangle', 'Interpolate based on triangle traced manually over geometry', 3),
),
default='GEOMETRY',
description='Select method for interpolating strokes'

12
ui.py
View File

@ -53,6 +53,7 @@ class GP_PT_interpolate(bpy.types.Panel):
col.prop(settings, 'remove_occluded')
elif settings.method == 'OBJECT':
col.prop(settings, 'search_range')
col.prop(settings, 'target_object', text='Object')
col.separator()
@ -76,6 +77,17 @@ class GP_PT_interpolate(bpy.types.Panel):
row = col.row(align=True)
row.operator('gp.debug_geometry_interpolation_targets', text='Debug points')
row.operator('gp.debug_geometry_interpolation_targets', text='', icon='X').clear = True
if context.scene.gp_interpo_settings.method == 'TRI':
col.separator()
row=layout.row(align=True)
wm = bpy.context.window_manager
binded = context.object and context.object.type == 'GPENCIL' and wm.get(f'tri_{context.object.name}')
txt = 'Show Tri Points' if binded else 'Bind Tri Points'
row.operator('gp.bind_points', text=txt, icon='MESH_DATA')
if binded:
row.operator('gp.bind_points', text='', icon='TRASH').clear = True
classes = (
GP_PT_interpolate,

View File

@ -124,7 +124,7 @@ def ray_cast_point(point, origin, depsgraph):
tri, tri_indices = get_tri_from_face(hit_location, face_index, object_hit, depsgraph)
return object_hit, np.array(hit_location), tri, tri_indices
return object_hit, hit_location, tri, tri_indices
def obj_ray_cast(obj, point, origin, depsgraph):
"""Wrapper for ray casting that moves the ray into object space"""
@ -142,7 +142,7 @@ def obj_ray_cast(obj, point, origin, depsgraph):
# Get hit location world_space
hit_location = obj.matrix_world @ location
tri, tri_indices = get_tri_from_face(hit_location, face_index, obj, depsgraph)
return obj, np.array(hit_location), tri, tri_indices
return obj, hit_location, tri, tri_indices
def empty_at(name='Empty', pos=(0,0,0), collection=None, type='PLAIN_AXES', size=1, show_name=False):