gp_interpolate/interpolate_strokes/debug.py

232 lines
9.8 KiB
Python

import bpy
import numpy as np
from time import perf_counter, time, sleep
from mathutils import Vector, Matrix
from ..utils import empty_at
from gp_interpolate.utils import (matrix_transform,
plane_on_bone,
ray_cast_point,
obj_ray_cast,
intersect_with_tesselated_plane,
triangle_normal,
search_square,
get_gp_draw_plane,
create_plane,
following_keys,
index_list_from_bools,
attr_set)
from mathutils.geometry import (barycentric_transform,
intersect_point_tri,
intersect_point_line,
intersect_line_plane,
tessellate_polygon)
class GP_OT_debug_geometry_interpolation_targets(bpy.types.Operator):
bl_idname = "gp.debug_geometry_interpolation_targets"
bl_label = "Debug geometry interpolation target"
bl_description = 'Identify position geometry used for interpolating select point at current frame'
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
if context.active_object and context.object.type == 'GPENCIL'\
and context.mode in ('EDIT_GPENCIL', 'SCULPT_GPENCIL', 'PAINT_GPENCIL'):
return True
cls.poll_message_set("Need a Grease pencil object in Edit or Sculpt mode")
return False
clear : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
def apply_and_store(self):
# self.store = []
# item = (prop, attr, [new_val])
for item in self.store_list:
prop, attr = item[:2]
self.store.append( (prop, attr, getattr(prop, attr)) )
if len(item) >= 3:
setattr(prop, attr, item[2])
def restore(self):
for prop, attr, old_val in self.store:
setattr(prop, attr, old_val)
def exit_ops(self, context, status='INFO', text=None):
if text:
self.report({status}, text)
def execute(self, context):
debug_col_name = '.gp_interpolation_debug'
debug_col = context.scene.collection.children.get(debug_col_name)
if not debug_col:
debug_col = bpy.data.collections.get(debug_col_name)
if self.clear:
if not debug_col:
return {'CANCELLED'}
## Clear debug collection and remove
for ob in reversed(debug_col.objects):
bpy.data.objects.remove(ob)
bpy.data.collections.remove(debug_col)
return {'FINISHED'}
if not debug_col:
debug_col = bpy.data.collections.new(debug_col_name)
if debug_col.name not in context.scene.collection.children:
context.scene.collection.children.link(debug_col)
## Clear debug collection
for ob in reversed(debug_col.objects):
bpy.data.objects.remove(ob)
# return {'FINISHED'}
scn = bpy.context.scene
self.gp = context.object
# matrix = np.array(self.gp.matrix_world, dtype='float64')
# origin = np.array(scn.camera.matrix_world.to_translation(), 'float64')
matrix = self.gp.matrix_world
origin = scn.camera.matrix_world.to_translation()
settings = context.scene.gp_interpo_settings
col = settings.target_collection
if not col:
col = scn.collection
# print('----')
if not self.gp.data.layers.active:
self.exit_ops(context, status='ERROR', text='No active layer')
return {'CANCELLED'}
if not self.gp.data.layers.active.active_frame:
self.exit_ops(context, status='ERROR', text='No active frame')
return {'CANCELLED'}
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
if not tgt_strokes and context.mode in ('SCULPT_GPENCIL', 'PAINT_GPENCIL'):
for s in self.gp.data.layers.active.active_frame.strokes:
s.select = True
tgt_strokes = self.gp.data.layers.active.active_frame.strokes
if not tgt_strokes:
self.exit_ops(context, status='ERROR', text='No stroke selected !')
return {'CANCELLED'}
included_cols = [c.name for c in self.gp.users_collection]
target_obj = None
if settings.method == 'GEOMETRY':
if col != context.scene.collection:
included_cols.append(col.name)
## Maybe include a plane just behind geo ? probably bad idea
elif settings.method == 'OBJECT':
if not settings.target_object:
self.exit_ops(context, status='ERROR', text='No Object selected')
return {'CANCELLED'}
col = scn.collection # Reset collection filter
target_obj = settings.target_object
if target_obj.library:
## Look if an override exists in scene to use instead of default object
if (override := next((o for o in scn.objects if o.name == target_obj.name and o.override_library), None)):
target_obj = override
## Get only active point
points = [p for p in tgt_strokes[0].points if p.select]
print('points: ', points)
dg = bpy.context.evaluated_depsgraph_get()
# nb_points = len(stroke.points)
## Batch get absolute coordinate
# local_co = np.empty(nb_points * 3, dtype='float64')
# stroke.points.foreach_get('co', local_co)
# local_co_3d = local_co.reshape((nb_points, 3))
# world_co_3d = matrix_transform(local_co.reshape((nb_points, 3)), matrix)
stroke_data = []
for i, point in enumerate(points):
point_co_world = matrix @ point.co
if target_obj:
object_hit, hit_location, tri, tri_indices = obj_ray_cast(target_obj, Vector(point_co_world), origin, dg)
else:
# Scene raycast
object_hit, hit_location, tri, tri_indices = ray_cast_point(point_co_world, origin, dg)
## Increasing search range
if not object_hit: # or object_hit not in col.all_objects[:]:
found = False
for iteration in range(1, 6):
ct = 0
for square_co in search_square(point_co_world, factor=settings.search_range * iteration):
ct += 1
if target_obj:
object_hit, hit_location, tri, tri_indices = obj_ray_cast(target_obj, Vector(square_co), origin, dg)
else:
# scene raycast
object_hit, hit_location, tri, tri_indices = ray_cast_point(square_co, origin, dg)
is_hit = 'HIT' if object_hit else ''
## 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
## 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
if found:
break
if not found:
context.scene.cursor.location = point_co_world
self.exit_ops(context, status='ERROR', text=f'point {i} could not find underlying geometry')
return {'CANCELLED'}
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=False)
print('Done')
return {'FINISHED'}
classes = (
GP_OT_debug_geometry_interpolation_targets,
)
def register():
for c in classes:
bpy.utils.register_class(c)
def unregister():
for c in reversed(classes):
bpy.utils.unregister_class(c)