diff --git a/__init__.py b/__init__.py index c0c5ab1..95f597f 100755 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "GP Interpolate", "author": "Christophe Seux, Samuel Bernou", - "version": (0, 8, 2), + "version": (0, 8, 3), "blender": (4, 0, 2), "location": "Sidebar > Gpencil Tab > Interpolate", "description": "Interpolate Grease pencil strokes over 3D", diff --git a/interpolate_strokes/operators.py b/interpolate_strokes/operators.py index 2389449..e3e9bcc 100644 --- a/interpolate_strokes/operators.py +++ b/interpolate_strokes/operators.py @@ -1,26 +1,18 @@ import bpy -import numpy as np -from time import perf_counter, time, sleep +from time import perf_counter, time from mathutils import Vector, Matrix -from ..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 ..utils import (plane_on_bone, + ray_cast_point, + obj_ray_cast, + triangle_normal, + search_square, + get_gp_draw_plane, + create_plane, + following_keys) from mathutils.geometry import (barycentric_transform, - intersect_point_tri, - intersect_point_line, - intersect_line_plane, - tessellate_polygon) + intersect_line_plane) class GP_OT_interpolate_stroke_base(bpy.types.Operator): @@ -59,11 +51,12 @@ class GP_OT_interpolate_stroke_base(bpy.types.Operator): for prop, attr, old_val in self.store: setattr(prop, attr, old_val) - def exit_modal(self, context, status='INFO', text=None, cancelled=False): + def exit(self, context, status='INFO', text=None, cancelled=False): context.area.header_text_set(None) wm = context.window_manager wm.progress_end() # Pgs - wm.event_timer_remove(self.timer) + if self.timer: + wm.event_timer_remove(self.timer) self.restore() if self.debug: if self.scan_time is not None: @@ -84,15 +77,12 @@ class GP_OT_interpolate_stroke_base(bpy.types.Operator): self.report({status}, text) else: self.report({'INFO'}, mess) + + if status == 'INFO': + return {'FINISHED'} + return {'CANCELLED'} def get_stroke_to_interpolate(self, context): - if not self.gp.data.layers.active: - self.exit_modal(context, status='ERROR', text='No active layer') - return {'CANCELLED'} - if not self.gp.data.layers.active.active_frame: - 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] @@ -103,8 +93,8 @@ class GP_OT_interpolate_stroke_base(bpy.types.Operator): tgt_strokes = self.gp.data.layers.active.active_frame.strokes if not tgt_strokes: - self.exit_modal(context, status='ERROR', text='No stroke selected !') - return {'CANCELLED'} + return self.exit(context, status='ERROR', text='No stroke selected!') + return tgt_strokes ## For now, operators have their own invoke @@ -121,6 +111,7 @@ class GP_OT_interpolate_stroke_base(bpy.types.Operator): self.settings = context.scene.gp_interpo_settings self.frames_to_jump = None self.cancelled = False + self.timer = None ## Remove interpolation_plane collection ! (unseen, but can be hit) if interp_plane := bpy.data.objects.get('interpolation_plane'): @@ -131,10 +122,28 @@ class GP_OT_interpolate_stroke_base(bpy.types.Operator): ## Determine on what key/keys to jump self.frames_to_jump = following_keys(forward=self.next, animation=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'} + return self.exit(context, status='WARNING', text='No keyframe available in this direction') + + active_layer = self.gp.data.layers.active + ## Change active layer if strokes are selected only on this layer + layer_count = 0 + + layers = [l for l in self.gp.data.layers + if (not l.lock and l.active_frame) + and next((s for s in l.active_frame.strokes if s.select), None)] + - self.timer = context.window_manager.event_timer_add(0.01, window=context.window) + if not layers: + return self.exit(context, status='ERROR', text='No stroke selected!') + + elif len(layers) > 1: + return self.exit(context, status='ERROR', text='Strokes selected accross multiple layers!') + + ## Set active layer + self.gp.data.layers.active = layers[0] + + # TODO: Expose timer (in preferences ?) to let user more time to see result between frames + self.timer = context.window_manager.event_timer_add(0.05, window=context.window) ## Converted to modal from "operator_single" @@ -199,8 +208,7 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): ## 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') - return {'CANCELLED'} + return self.exit(context, status='ERROR', text='No Bone selected') included_cols.append('interpolation_tool') @@ -230,8 +238,8 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): elif self.settings.method == 'OBJECT': if not self.settings.target_object: - self.exit_modal(context, status='ERROR', text='No Object selected') - return {'CANCELLED'} + return self.exit(context, status='ERROR', text='No Object selected') + col = scn.collection # Reset collection filter target_obj = self.settings.target_object if target_obj.library: @@ -288,8 +296,7 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): # For debugging, select only point. bpy.ops.gpencil.select_all(action='DESELECT') point.select = True - self.exit_modal(context, status='ERROR', text=f'Stroke {stroke_index} point {point_index} could not find underlying geometry') - return {'CANCELLED'} + return self.exit(context, status='ERROR', text=f'Stroke {stroke_index} point {point_index} could not find underlying geometry') stroke_data.append((stroke, point_co_world, object_hit, hit_location, tri, tri_indices)) @@ -319,8 +326,7 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): if event.type in {'RIGHTMOUSE', 'ESC'}: context.area.header_text_set(f'Cancelling') - self.exit_modal(context, text='Cancelling', cancelled=True) - return {'CANCELLED'} + return self.exit(context, status='WARNING', text='Cancelling', cancelled=True) ## -- LOOPTIMER if event.type == 'TIMER': @@ -348,7 +354,6 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): other_strokes = [s for l in self.gp.data.layers if l.active_frame and not l.lock for s in l.active_frame.strokes if not s.select] occluded_points = [] - # For new_stroke, stroke_data in zip(new_strokes, self.strokes_data): for new_stroke, stroke_data in zip(list(new_strokes), list(self.strokes_data)): world_co_3d = [] for stroke, point_co, object_hit, hit_location, tri_a, tri_indices in stroke_data: @@ -369,7 +374,7 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): for i, point in enumerate(new_stroke.points): point_co = world_co_3d[i] vec_direction = point_co - origin - ## Raycast with slightly reduced distance (avoid occlusion on same source) + ## Raycast with slightly reduced distance (avoid occlusion on initial surface) n_hit, _, _, _, _, _ = scn.ray_cast(dg, origin, vec_direction, distance=vec_direction.length - 0.001) if n_hit: occluded_points.append(point) @@ -389,8 +394,7 @@ class GP_OT_interpolate_stroke(GP_OT_interpolate_stroke_base): self.loop_count += 1 if self.loop_count >= len(self.frames_to_jump): - self.exit_modal(context) - return {'FINISHED'} + return self.exit(context) bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) # context.area.tag_redraw() diff --git a/interpolate_strokes/operators_triangle.py b/interpolate_strokes/operators_triangle.py index 1b1dbb3..7f8f292 100644 --- a/interpolate_strokes/operators_triangle.py +++ b/interpolate_strokes/operators_triangle.py @@ -1,26 +1,12 @@ import bpy -import numpy as np -from time import perf_counter, time, sleep +from time import perf_counter, time from mathutils import Vector, Matrix -from ..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 ..utils import (triangle_normal, + get_gp_draw_plane) from mathutils.geometry import (barycentric_transform, - intersect_point_tri, - intersect_point_line, - intersect_line_plane, - tessellate_polygon) + intersect_line_plane) from .operators import GP_OT_interpolate_stroke_base @@ -38,19 +24,16 @@ class GP_OT_interpolate_stroke_tri(GP_OT_interpolate_stroke_base): ## START if 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'} + return self.exit(context, status='ERROR', text='Need to bind coordinate first. Use "Bind Tri Point" button') scn = bpy.context.scene origin = scn.camera.matrix_world.to_translation() - + tgt_strokes = self.get_stroke_to_interpolate(context) if isinstance(tgt_strokes, set): return tgt_strokes - target_obj = None - ## Prepare context manager self.store_list = [ # (context.view_layer.objects, 'active', self.gp), @@ -97,8 +80,9 @@ class GP_OT_interpolate_stroke_tri(GP_OT_interpolate_stroke_base): self.scan_time = time()-self.start print(f'Scan time {self.scan_time:.4f}s') + # Ensure whole stroke are selected before copy + bpy.ops.gpencil.select_linked() # Copy stroke selection - bpy.ops.gpencil.select_linked() # Ensure whole stroke are selected before copy bpy.ops.gpencil.copy() # Jump frame and paste @@ -115,8 +99,7 @@ class GP_OT_interpolate_stroke_tri(GP_OT_interpolate_stroke_base): if event.type in {'RIGHTMOUSE', 'ESC'}: context.area.header_text_set(f'Cancelling') - self.exit_modal(context, text='Cancelling', cancelled=True) - return {'CANCELLED'} + return self.exit(context, status='WARNING', text='Cancelling', cancelled=True) ## -- LOOPTIMER if event.type == 'TIMER': @@ -129,7 +112,8 @@ class GP_OT_interpolate_stroke_tri(GP_OT_interpolate_stroke_base): bpy.ops.gpencil.paste() dg = bpy.context.evaluated_depsgraph_get() - matrix_inv = np.array(self.gp.matrix_world.inverted(), dtype='float64') + + ## List of newly pasted strokes (using range) new_strokes = self.gp.data.layers.active.active_frame.strokes[-len(self.strokes_data):] ## Get user triangle position at current frame @@ -138,27 +122,22 @@ class GP_OT_interpolate_stroke_tri(GP_OT_interpolate_stroke_base): ob_eval = source_obj.evaluated_get(dg) tri_b.append(ob_eval.matrix_world @ ob_eval.data.vertices[idx].co) - ## Apply - for new_stroke, stroke_data in zip(reversed(new_strokes), reversed(self.strokes_data)): + for new_stroke, stroke_data in zip(list(new_strokes), list(self.strokes_data)): world_co_3d = [] - # for stroke, point_co, object_hit, hit_location, tri_a, indices in stroke_data: for hit_location, tri_a in stroke_data: new_loc = barycentric_transform(hit_location, *tri_a, *tri_b) world_co_3d.append(new_loc) ## Reproject on plane new_world_co_3d = [intersect_line_plane(origin, p, plane_co, plane_no) for p in world_co_3d] - new_local_co_3d = matrix_transform(new_world_co_3d, matrix_inv) - - nb_points = len(new_stroke.points) - new_stroke.points.foreach_set('co', new_local_co_3d.reshape(nb_points*3)) + new_local_co_3d = [co for coord in new_world_co_3d for co in self.gp.matrix_world.inverted() @ coord] + new_stroke.points.foreach_set('co', new_local_co_3d) new_stroke.points.update() ## Setup next loop and redraw self.loop_count += 1 if self.loop_count >= len(self.frames_to_jump): - self.exit_modal(context) - return {'FINISHED'} + return self.exit(context) bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) diff --git a/ui.py b/ui.py index 0cf86dc..aafdc94 100755 --- a/ui.py +++ b/ui.py @@ -33,7 +33,7 @@ class GP_PT_interpolate(bpy.types.Panel): next_text = f'{scn.frame_current} > {scn.frame_preview_end if scn.use_preview_range else scn.frame_end}' row = col.row(align=True) - # row.scale_y = 1.2 + row.scale_y = 1.2 direction_button_row = row.row(align=True) direction_button_row.scale_x = 3 ops_id = "gp.interpolate_stroke_tri" if settings.method == 'TRI' else "gp.interpolate_stroke"