GPencil Tools: Add rolling timeline
[blender-addons.git] / curve_simplify.py
blob0109530a13e7cd6dafe8a24fb3587ac642d837c6
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 bl_info = {
20 "name": "Simplify Curves+",
21 "author": "testscreenings, Michael Soluyanov",
22 "version": (1, 1, 2),
23 "blender": (2, 80, 0),
24 "location": "3D View, Dopesheet & Graph Editors",
25 "description": "Simplify Curves: 3dview, Dopesheet, Graph. Distance Merge: 3d view curve edit",
26 "warning": "",
27 "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/simplify_curves.html",
28 "category": "Add Curve",
31 """
32 This script simplifies Curve objects and animation F-Curves
33 This script will also Merge by Distance 3d view curves in edit mode
34 """
36 import bpy
37 from bpy.props import (
38 BoolProperty,
39 EnumProperty,
40 FloatProperty,
41 IntProperty,
43 import mathutils
44 from math import (
45 sin,
46 pow,
48 from bpy.types import Operator
51 def error_handlers(self, op_name, errors, reports="ERROR"):
52 if self and reports:
53 self.report({'INFO'},
54 reports + ": some operations could not be performed "
55 "(See Console for more info)")
57 print("\n[Simplify Curves]\nOperator: {}\nErrors: {}\n".format(op_name, errors))
60 # Check for curve
62 # ### simplipoly algorithm ###
64 # get SplineVertIndices to keep
65 def simplypoly(splineVerts, options):
66 # main vars
67 newVerts = [] # list of vertindices to keep
68 points = splineVerts # list of 3dVectors
69 pointCurva = [] # table with curvatures
70 curvatures = [] # averaged curvatures per vert
71 for p in points:
72 pointCurva.append([])
73 order = options[3] # order of sliding beziercurves
74 k_thresh = options[2] # curvature threshold
75 dis_error = options[6] # additional distance error
77 # get curvatures per vert
78 for i, point in enumerate(points[: -(order - 1)]):
79 BVerts = points[i: i + order]
80 for b, BVert in enumerate(BVerts[1: -1]):
81 deriv1 = getDerivative(BVerts, 1 / (order - 1), order - 1)
82 deriv2 = getDerivative(BVerts, 1 / (order - 1), order - 2)
83 curva = getCurvature(deriv1, deriv2)
84 pointCurva[i + b + 1].append(curva)
86 # average the curvatures
87 for i in range(len(points)):
88 avgCurva = sum(pointCurva[i]) / (order - 1)
89 curvatures.append(avgCurva)
91 # get distancevalues per vert - same as Ramer-Douglas-Peucker
92 # but for every vert
93 distances = [0.0] # first vert is always kept
94 for i, point in enumerate(points[1: -1]):
95 dist = altitude(points[i], points[i + 2], points[i + 1])
96 distances.append(dist)
97 distances.append(0.0) # last vert is always kept
99 # generate list of vert indices to keep
100 # tested against averaged curvatures and distances of neighbour verts
101 newVerts.append(0) # first vert is always kept
102 for i, curv in enumerate(curvatures):
103 if (curv >= k_thresh * 0.01 or distances[i] >= dis_error * 0.1):
104 newVerts.append(i)
105 newVerts.append(len(curvatures) - 1) # last vert is always kept
107 return newVerts
110 # get binomial coefficient
111 def binom(n, m):
112 b = [0] * (n + 1)
113 b[0] = 1
114 for i in range(1, n + 1):
115 b[i] = 1
116 j = i - 1
117 while j > 0:
118 b[j] += b[j - 1]
119 j -= 1
120 return b[m]
123 # get nth derivative of order(len(verts)) bezier curve
124 def getDerivative(verts, t, nth):
125 order = len(verts) - 1 - nth
126 QVerts = []
128 if nth:
129 for i in range(nth):
130 if QVerts:
131 verts = QVerts
132 derivVerts = []
133 for i in range(len(verts) - 1):
134 derivVerts.append(verts[i + 1] - verts[i])
135 QVerts = derivVerts
136 else:
137 QVerts = verts
139 if len(verts[0]) == 3:
140 point = Vector((0, 0, 0))
141 if len(verts[0]) == 2:
142 point = Vector((0, 0))
144 for i, vert in enumerate(QVerts):
145 point += binom(order, i) * pow(t, i) * pow(1 - t, order - i) * vert
146 deriv = point
148 return deriv
151 # get curvature from first, second derivative
152 def getCurvature(deriv1, deriv2):
153 if deriv1.length == 0: # in case of points in straight line
154 curvature = 0
155 return curvature
156 curvature = (deriv1.cross(deriv2)).length / pow(deriv1.length, 3)
157 return curvature
160 # ### Ramer-Douglas-Peucker algorithm ###
162 # get altitude of vert
163 def altitude(point1, point2, pointn):
164 edge1 = point2 - point1
165 edge2 = pointn - point1
166 if edge2.length == 0:
167 altitude = 0
168 return altitude
169 if edge1.length == 0:
170 altitude = edge2.length
171 return altitude
172 alpha = edge1.angle(edge2)
173 altitude = sin(alpha) * edge2.length
174 return altitude
177 # iterate through verts
178 def iterate(points, newVerts, error):
179 new = []
180 for newIndex in range(len(newVerts) - 1):
181 bigVert = 0
182 alti_store = 0
183 for i, point in enumerate(points[newVerts[newIndex] + 1: newVerts[newIndex + 1]]):
184 alti = altitude(points[newVerts[newIndex]], points[newVerts[newIndex + 1]], point)
185 if alti > alti_store:
186 alti_store = alti
187 if alti_store >= error:
188 bigVert = i + 1 + newVerts[newIndex]
189 if bigVert:
190 new.append(bigVert)
191 if new == []:
192 return False
193 return new
196 # get SplineVertIndices to keep
197 def simplify_RDP(splineVerts, options):
198 # main vars
199 error = options[4]
201 # set first and last vert
202 newVerts = [0, len(splineVerts) - 1]
204 # iterate through the points
205 new = 1
206 while new is not False:
207 new = iterate(splineVerts, newVerts, error)
208 if new:
209 newVerts += new
210 newVerts.sort()
211 return newVerts
214 # ### CURVE GENERATION ###
216 # set bezierhandles to auto
217 def setBezierHandles(newCurve):
218 # Faster:
219 for spline in newCurve.data.splines:
220 for p in spline.bezier_points:
221 p.handle_left_type = 'AUTO'
222 p.handle_right_type = 'AUTO'
225 # get array of new coords for new spline from vertindices
226 def vertsToPoints(newVerts, splineVerts, splineType):
227 # main vars
228 newPoints = []
230 # array for BEZIER spline output
231 if splineType == 'BEZIER':
232 for v in newVerts:
233 newPoints += splineVerts[v].to_tuple()
235 # array for nonBEZIER output
236 else:
237 for v in newVerts:
238 newPoints += (splineVerts[v].to_tuple())
239 if splineType == 'NURBS':
240 newPoints.append(1) # for nurbs w = 1
241 else: # for poly w = 0
242 newPoints.append(0)
243 return newPoints
246 # ### MAIN OPERATIONS ###
248 def main(context, obj, options, curve_dimension):
249 mode = options[0]
250 output = options[1]
251 degreeOut = options[5]
252 keepShort = options[7]
253 bpy.ops.object.select_all(action='DESELECT')
254 scene = context.scene
255 splines = obj.data.splines.values()
257 # create curvedatablock
258 curve = bpy.data.curves.new("Simple_" + obj.name, type='CURVE')
259 curve.dimensions = curve_dimension
261 # go through splines
262 for spline_i, spline in enumerate(splines):
263 # test if spline is a long enough
264 if len(spline.points) >= 3 or keepShort:
265 # check what type of spline to create
266 if output == 'INPUT':
267 splineType = spline.type
268 else:
269 splineType = output
271 # get vec3 list to simplify
272 if spline.type == 'BEZIER': # get bezierverts
273 splineVerts = [splineVert.co.copy()
274 for splineVert in spline.bezier_points.values()]
276 else: # verts from all other types of curves
277 splineVerts = [splineVert.co.to_3d()
278 for splineVert in spline.points.values()]
280 # simplify spline according to mode
281 if mode == 'DISTANCE':
282 newVerts = simplify_RDP(splineVerts, options)
284 if mode == 'CURVATURE':
285 newVerts = simplypoly(splineVerts, options)
287 # convert indices into vectors3D
288 newPoints = vertsToPoints(newVerts, splineVerts, splineType)
290 # create new spline
291 newSpline = curve.splines.new(type=splineType)
293 # put newPoints into spline according to type
294 if splineType == 'BEZIER':
295 newSpline.bezier_points.add(int(len(newPoints) * 0.33))
296 newSpline.bezier_points.foreach_set('co', newPoints)
297 else:
298 newSpline.points.add(int(len(newPoints) * 0.25 - 1))
299 newSpline.points.foreach_set('co', newPoints)
301 # set degree of outputNurbsCurve
302 if output == 'NURBS':
303 newSpline.order_u = degreeOut
305 # splineoptions
306 newSpline.use_endpoint_u = spline.use_endpoint_u
308 # create new object and put into scene
309 newCurve = bpy.data.objects.new("Simple_" + obj.name, curve)
310 coll = context.view_layer.active_layer_collection.collection
311 coll.objects.link(newCurve)
312 newCurve.select_set(True)
314 context.view_layer.objects.active = newCurve
315 newCurve.matrix_world = obj.matrix_world
317 # set bezierhandles to auto
318 setBezierHandles(newCurve)
320 return
323 # get preoperator fcurves
324 def getFcurveData(obj):
325 fcurves = []
326 for fc in obj.animation_data.action.fcurves:
327 if fc.select:
328 fcVerts = [vcVert.co.to_3d()
329 for vcVert in fc.keyframe_points.values()]
330 fcurves.append(fcVerts)
331 return fcurves
334 def selectedfcurves(obj):
335 fcurves_sel = []
336 for i, fc in enumerate(obj.animation_data.action.fcurves):
337 if fc.select:
338 fcurves_sel.append(fc)
339 return fcurves_sel
342 # fCurves Main
343 def fcurves_simplify(context, obj, options, fcurves):
344 # main vars
345 mode = options[0]
347 # get indices of selected fcurves
348 fcurve_sel = selectedfcurves(obj)
350 # go through fcurves
351 for fcurve_i, fcurve in enumerate(fcurves):
352 # test if fcurve is long enough
353 if len(fcurve) >= 3:
354 # simplify spline according to mode
355 if mode == 'DISTANCE':
356 newVerts = simplify_RDP(fcurve, options)
358 if mode == 'CURVATURE':
359 newVerts = simplypoly(fcurve, options)
361 # convert indices into vectors3D
362 newPoints = []
364 # this is different from the main() function for normal curves, different api...
365 for v in newVerts:
366 newPoints.append(fcurve[v])
368 # remove all points from curve first
369 for i in range(len(fcurve) - 1, 0, -1):
370 fcurve_sel[fcurve_i].keyframe_points.remove(fcurve_sel[fcurve_i].keyframe_points[i])
371 # put newPoints into fcurve
372 for v in newPoints:
373 fcurve_sel[fcurve_i].keyframe_points.insert(frame=v[0], value=v[1])
374 return
377 # ### MENU append ###
379 def menu_func(self, context):
380 self.layout.operator("graph.simplify")
383 def menu(self, context):
384 self.layout.operator("curve.simplify", text="Curve Simplify", icon="CURVE_DATA")
388 # ### ANIMATION CURVES OPERATOR ###
390 class GRAPH_OT_simplify(Operator):
391 bl_idname = "graph.simplify"
392 bl_label = "Simplify F-Curves"
393 bl_description = ("Simplify selected Curves\n"
394 "Does not operate on short Splines (less than 3 points)")
395 bl_options = {'REGISTER', 'UNDO'}
397 # Properties
398 opModes = [
399 ('DISTANCE', 'Distance', 'Distance-based simplification (Poly)'),
400 ('CURVATURE', 'Curvature', 'Curvature-based simplification (RDP)')]
401 mode: EnumProperty(
402 name="Mode",
403 description="Choose algorithm to use",
404 items=opModes
406 k_thresh: FloatProperty(
407 name="k",
408 min=0, soft_min=0,
409 default=0, precision=5,
410 description="Threshold"
412 pointsNr: IntProperty(
413 name="n",
414 min=5, soft_min=5,
415 max=16, soft_max=9,
416 default=5,
417 description="Degree of curve to get averaged curvatures"
419 error: FloatProperty(
420 name="Error",
421 description="Maximum allowed distance error",
422 min=0.0, soft_min=0.0,
423 default=0, precision=5,
424 step = 0.1
426 degreeOut: IntProperty(
427 name="Degree",
428 min=3, soft_min=3,
429 max=7, soft_max=7,
430 default=5,
431 description="Degree of new curve"
433 dis_error: FloatProperty(
434 name="Distance error",
435 description="Maximum allowed distance error in Blender Units",
436 min=0, soft_min=0,
437 default=0.0, precision=5
439 fcurves = []
441 def draw(self, context):
442 layout = self.layout
443 col = layout.column()
445 col.label(text="Distance Error:")
446 col.prop(self, "error", expand=True)
448 @classmethod
449 def poll(cls, context):
450 # Check for animdata
451 obj = context.active_object
452 fcurves = False
453 if obj:
454 animdata = obj.animation_data
455 if animdata:
456 act = animdata.action
457 if act:
458 fcurves = act.fcurves
459 return (obj and fcurves)
461 def execute(self, context):
462 options = [
463 self.mode, # 0
464 self.mode, # 1
465 self.k_thresh, # 2
466 self.pointsNr, # 3
467 self.error, # 4
468 self.degreeOut, # 6
469 self.dis_error # 7
472 obj = context.active_object
474 if not self.fcurves:
475 self.fcurves = getFcurveData(obj)
477 fcurves_simplify(context, obj, options, self.fcurves)
479 return {'FINISHED'}
482 # ### Curves OPERATOR ###
483 class CURVE_OT_simplify(Operator):
484 bl_idname = "curve.simplify"
485 bl_label = "Simplify Curves"
486 bl_description = ("Simplify the existing Curve based upon the chosen settings\n"
487 "Notes: Needs an existing Curve object,\n"
488 "Outputs a new Curve with the Simple prefix in the name")
489 bl_options = {'REGISTER', 'UNDO'}
491 # Properties
492 opModes = [
493 ('DISTANCE', 'Distance', 'Distance-based simplification (Poly)'),
494 ('CURVATURE', 'Curvature', 'Curvature-based simplification (RDP)')
496 mode: EnumProperty(
497 name="Mode",
498 description="Choose algorithm to use",
499 items=opModes
501 SplineTypes = [
502 ('INPUT', 'Input', 'Same type as input spline'),
503 ('NURBS', 'Nurbs', 'NURBS'),
504 ('BEZIER', 'Bezier', 'BEZIER'),
505 ('POLY', 'Poly', 'POLY')
507 output: EnumProperty(
508 name="Output splines",
509 description="Type of splines to output",
510 items=SplineTypes
512 k_thresh: FloatProperty(
513 name="k",
514 min=0, soft_min=0,
515 default=0, precision=5,
516 description="Threshold"
518 pointsNr: IntProperty(
519 name="n",
520 min=5, soft_min=5,
521 max=9, soft_max=9,
522 default=5,
523 description="Degree of curve to get averaged curvatures"
525 error: FloatProperty(
526 name="Error",
527 description="Maximum allowed distance error in Blender Units",
528 min=0, soft_min=0,
529 default=0.0, precision=5,
530 step = 0.1
532 degreeOut: IntProperty(
533 name="Degree",
534 min=3, soft_min=3,
535 max=7, soft_max=7,
536 default=5,
537 description="Degree of new curve"
539 dis_error: FloatProperty(
540 name="Distance error",
541 description="Maximum allowed distance error in Blender Units",
542 min=0, soft_min=0,
543 default=0.0
545 keepShort: BoolProperty(
546 name="Keep short splines",
547 description="Keep short splines (less than 3 points)",
548 default=True
551 def draw(self, context):
552 layout = self.layout
553 col = layout.column()
555 col.label(text="Distance Error:")
556 col.prop(self, "error", expand=True)
557 col.prop(self, "output", text="Output", icon="OUTLINER_OB_CURVE")
558 if self.output == "NURBS":
559 col.prop(self, "degreeOut", expand=True)
560 col.separator()
561 col.prop(self, "keepShort", expand=True)
563 @classmethod
564 def poll(cls, context):
565 obj = context.active_object
566 return (obj and obj.type == 'CURVE')
568 def execute(self, context):
569 options = [
570 self.mode, # 0
571 self.output, # 1
572 self.k_thresh, # 2
573 self.pointsNr, # 3
574 self.error, # 4
575 self.degreeOut, # 5
576 self.dis_error, # 6
577 self.keepShort # 7
579 try:
580 bpy.ops.object.mode_set(mode='OBJECT')
581 obj = context.active_object
582 curve_dimension = obj.data.dimensions
584 main(context, obj, options, curve_dimension)
585 except Exception as e:
586 error_handlers(self, "curve.simplify", e, "Simplify Curves")
587 return {'CANCELLED'}
589 return {'FINISHED'}
591 ## Initial use Curve Remove Doubles ##
593 def main_rd(context, distance = 0.01):
595 selected_Curves = context.selected_objects
596 if bpy.ops.object.mode_set.poll():
597 bpy.ops.object.mode_set(mode='EDIT')
599 bezier_dellist = []
600 dellist = []
602 for curve in selected_Curves:
603 for spline in curve.data.splines:
604 if spline.type == 'BEZIER':
605 if len(spline.bezier_points) > 1:
606 for i in range(0, len(spline.bezier_points)):
608 if i == 0:
609 ii = len(spline.bezier_points) - 1
610 else:
611 ii = i - 1
613 dot = spline.bezier_points[i];
614 dot1 = spline.bezier_points[ii];
616 while dot1 in bezier_dellist and i != ii:
617 ii -= 1
618 if ii < 0:
619 ii = len(spline.bezier_points)-1
620 dot1 = spline.bezier_points[ii]
622 if dot.select_control_point and dot1.select_control_point and (i!=0 or spline.use_cyclic_u):
624 if (dot.co-dot1.co).length < distance:
625 # remove points and recreate hangles
626 dot1.handle_right_type = "FREE"
627 dot1.handle_right = dot.handle_right
628 dot1.co = (dot.co + dot1.co) / 2
629 bezier_dellist.append(dot)
631 else:
632 # Handles that are on main point position converts to vector,
633 # if next handle are also vector
634 if dot.handle_left_type == 'VECTOR' and (dot1.handle_right - dot1.co).length < distance:
635 dot1.handle_right_type = "VECTOR"
636 if dot1.handle_right_type == 'VECTOR' and (dot.handle_left - dot.co).length < distance:
637 dot.handle_left_type = "VECTOR"
638 else:
639 if len(spline.points) > 1:
640 for i in range(0, len(spline.points)):
642 if i == 0:
643 ii = len(spline.points) - 1
644 else:
645 ii = i - 1
647 dot = spline.points[i];
648 dot1 = spline.points[ii];
650 while dot1 in dellist and i != ii:
651 ii -= 1
652 if ii < 0:
653 ii = len(spline.points)-1
654 dot1 = spline.points[ii]
656 if dot.select and dot1.select and (i!=0 or spline.use_cyclic_u):
658 if (dot.co-dot1.co).length < distance:
659 dot1.co = (dot.co + dot1.co) / 2
660 dellist.append(dot)
662 bpy.ops.curve.select_all(action = 'DESELECT')
664 for dot in bezier_dellist:
665 dot.select_control_point = True
667 for dot in dellist:
668 dot.select = True
670 bezier_count = len(bezier_dellist)
671 count = len(dellist)
673 bpy.ops.curve.delete(type = 'VERT')
675 bpy.ops.curve.select_all(action = 'DESELECT')
677 return bezier_count + count
681 class Curve_OT_CurveRemvDbs(bpy.types.Operator):
682 """Merge consecutive points that are near to each other"""
683 bl_idname = 'curve.remove_double'
684 bl_label = 'Merge By Distance'
685 bl_options = {'REGISTER', 'UNDO'}
687 distance: bpy.props.FloatProperty(name = 'Distance', default = 0.01, soft_min = 0.001, step = 0.1)
689 @classmethod
690 def poll(cls, context):
691 obj = context.active_object
692 return (obj and obj.type == 'CURVE')
694 def execute(self, context):
695 removed=main_rd(context, self.distance)
696 self.report({'INFO'}, "Removed %d bezier points" % removed)
697 return {'FINISHED'}
699 def menu_func_rd(self, context):
700 self.layout.operator(Curve_OT_CurveRemvDbs.bl_idname, text='Merge By Distance')
702 # Register
703 classes = [
704 GRAPH_OT_simplify,
705 CURVE_OT_simplify,
706 Curve_OT_CurveRemvDbs,
710 def register():
711 from bpy.utils import register_class
712 for cls in classes:
713 register_class(cls)
715 #bpy.types.GRAPH_MT_channel.append(menu_func)
716 #bpy.types.DOPESHEET_MT_channel.append(menu_func)
717 bpy.types.VIEW3D_MT_curve_add.append(menu)
718 bpy.types.VIEW3D_MT_edit_curve_context_menu.prepend(menu)
719 bpy.types.VIEW3D_MT_edit_curve_context_menu.prepend(menu_func_rd)
722 def unregister():
723 from bpy.utils import unregister_class
724 for cls in reversed(classes):
725 unregister_class(cls)
727 #bpy.types.GRAPH_MT_channel.remove(menu_func)
728 #bpy.types.DOPESHEET_MT_channel.remove(menu_func)
729 bpy.types.VIEW3D_MT_curve_add.remove(menu)
730 bpy.types.VIEW3D_MT_edit_curve_context_menu.remove(menu)
731 bpy.types.VIEW3D_MT_edit_curve_context_menu.remove(menu_func_rd)
733 if __name__ == "__main__":
734 register()