GPencil Tools: Optimize Undo for Rotate Canvas
[blender-addons.git] / greasepencil_tools / line_reshape.py
blob0b9d0fb0d393975311257ab58394ccc33e91e4f5
1 # SPDX-FileCopyrightText: 2020-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 '''Based on GP_refine_stroke 0.2.4 - Author: Samuel Bernou'''
7 import bpy
9 ### --- Vector utils
11 def mean(*args):
12 '''
13 return mean of all passed value (multiple)
14 If it's a list or tuple return mean of it (only on first list passed).
15 '''
16 if isinstance(args[0], list) or isinstance(args[0], tuple):
17 return mean(*args[0])#send the first list UNPACKED (else infinite recursion as it always evaluate as list)
18 return sum(args) / len(args)
20 def vector_len_from_coord(a, b):
21 '''
22 Get two points (that has coordinate 'co' attribute) or Vectors (2D or 3D)
23 Return length as float
24 '''
25 from mathutils import Vector
26 if type(a) is Vector:
27 return (a - b).length
28 else:
29 return (a.co - b.co).length
31 def point_from_dist_in_segment_3d(a, b, ratio):
32 '''return the tuple coords of a point on 3D segment ab according to given ratio (some distance divided by total segment length)'''
33 ## ref:https://math.stackexchange.com/questions/175896/finding-a-point-along-a-line-a-certain-distance-away-from-another-point
34 # ratio = dist / seglength
35 return ( ((1 - ratio) * a[0] + (ratio*b[0])), ((1 - ratio) * a[1] + (ratio*b[1])), ((1 - ratio) * a[2] + (ratio*b[2])) )
37 def get_stroke_length(s):
38 '''return 3D total length of the stroke'''
39 all_len = 0.0
40 for i in range(0, len(s.points)-1):
41 #print(vector_len_from_coord(s.points[i],s.points[i+1]))
42 all_len += vector_len_from_coord(s.points[i],s.points[i+1])
43 return (all_len)
45 ### --- Functions
47 def to_straight_line(s, keep_points=True, influence=100, straight_pressure=True):
48 '''
49 keep points : if false only start and end point stay
50 straight_pressure : (not available with keep point) take the mean pressure of all points and apply to stroke.
51 '''
53 p_len = len(s.points)
54 if p_len <= 2: # 1 or 2 points only, cancel
55 return
57 if not keep_points:
58 if straight_pressure: mean_pressure = mean([p.pressure for p in s.points])#can use a foreach_get but might not be faster.
59 for i in range(p_len-2):
60 s.points.pop(index=1)
61 if straight_pressure:
62 for p in s.points:
63 p.pressure = mean_pressure
65 else:
66 A = s.points[0].co
67 B = s.points[-1].co
68 # ab_dist = vector_len_from_coord(A,B)
69 full_dist = get_stroke_length(s)
70 dist_from_start = 0.0
71 coord_list = []
73 for i in range(1, p_len-1):#all but first and last
74 dist_from_start += vector_len_from_coord(s.points[i-1],s.points[i])
75 ratio = dist_from_start / full_dist
76 # dont apply directly (change line as we measure it in loop)
77 coord_list.append( point_from_dist_in_segment_3d(A, B, ratio) )
79 # apply change
80 for i in range(1, p_len-1):
81 ## Direct super straight 100%
82 #s.points[i].co = coord_list[i-1]
84 ## With influence
85 s.points[i].co = point_from_dist_in_segment_3d(s.points[i].co, coord_list[i-1], influence / 100)
87 return
89 def get_last_index(context=None):
90 if not context:
91 context = bpy.context
92 return 0 if context.tool_settings.use_gpencil_draw_onback else -1
94 ### --- OPS
96 class GPENCIL_OT_straight_stroke(bpy.types.Operator):
97 bl_idname = "gpencil.straight_stroke"
98 bl_label = "Straight Stroke"
99 bl_description = "Make stroke a straight line between first and last point,\
100 \nTweak influence in the redo panel\
101 \nShift+click to reset infuence to 100%"
102 bl_options = {"REGISTER", "UNDO"}
104 @classmethod
105 def poll(cls, context):
106 return context.active_object is not None and context.object.type == 'GPENCIL'
107 #and context.mode in ('PAINT_GPENCIL', 'EDIT_GPENCIL')
109 influence_val : bpy.props.FloatProperty(name="Straight force", description="Straight interpolation percentage",
110 default=100, min=0, max=100, step=2, precision=1, subtype='PERCENTAGE', unit='NONE')
112 def execute(self, context):
113 gp = context.object.data
114 gpl = gp.layers
115 if not gpl:
116 return {"CANCELLED"}
118 if context.mode == 'PAINT_GPENCIL':
119 if not gpl.active or not gpl.active.active_frame:
120 self.report({'ERROR'}, 'No Grease pencil frame found')
121 return {"CANCELLED"}
123 if not len(gpl.active.active_frame.strokes):
124 self.report({'ERROR'}, 'No strokes found.')
125 return {"CANCELLED"}
127 s = gpl.active.active_frame.strokes[get_last_index(context)]
128 to_straight_line(s, keep_points=True, influence=self.influence_val)
130 elif context.mode == 'EDIT_GPENCIL':
131 ct = 0
132 for l in gpl:
133 if l.lock or l.hide or not l.active_frame:
134 # avoid locked, hidden, empty layers
135 continue
136 if gp.use_multiedit:
137 target_frames = [f for f in l.frames if f.select]
138 else:
139 target_frames = [l.active_frame]
141 for f in target_frames:
142 for s in f.strokes:
143 if s.select:
144 ct += 1
145 to_straight_line(s, keep_points=True, influence=self.influence_val)
147 if not ct:
148 self.report({'ERROR'}, 'No selected stroke found.')
149 return {"CANCELLED"}
151 ## filter method
152 # if context.mode == 'PAINT_GPENCIL':
153 # L, F, S = 'ACTIVE', 'ACTIVE', 'LAST'
154 # elif context.mode == 'EDIT_GPENCIL'
155 # L, F, S = 'ALL', 'ACTIVE', 'SELECT'
156 # if gp.use_multiedit: F = 'SELECT'
157 # else : return {"CANCELLED"}
158 # for s in strokelist(t_layer=L, t_frame=F, t_stroke=S):
159 # to_straight_line(s, keep_points=True, influence = self.influence_val)#, straight_pressure=True
161 return {"FINISHED"}
163 def draw(self, context):
164 layout = self.layout
165 layout.prop(self, "influence_val")
167 def invoke(self, context, event):
168 if context.mode not in ('PAINT_GPENCIL', 'EDIT_GPENCIL'):
169 return {"CANCELLED"}
170 if event.shift:
171 self.influence_val = 100
172 return self.execute(context)
175 def register():
176 bpy.utils.register_class(GPENCIL_OT_straight_stroke)
178 def unregister():
179 bpy.utils.unregister_class(GPENCIL_OT_straight_stroke)