1 # SPDX-License-Identifier: GPL-2.0-or-later
4 "name": "Simplify Curves+",
5 "author": "testscreenings, Michael Soluyanov",
8 "location": "3D View, Dopesheet & Graph Editors",
9 "description": "Simplify Curves: 3dview, Dopesheet, Graph. Distance Merge: 3d view curve edit",
11 "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/simplify_curves.html",
12 "category": "Add Curve",
16 This script simplifies Curve objects and animation F-Curves
17 This script will also Merge by Distance 3d view curves in edit mode
21 from bpy
.props
import (
32 from bpy
.types
import Operator
35 def error_handlers(self
, op_name
, errors
, reports
="ERROR"):
38 reports
+ ": some operations could not be performed "
39 "(See Console for more info)")
41 print("\n[Simplify Curves]\nOperator: {}\nErrors: {}\n".format(op_name
, errors
))
46 # ### simplipoly algorithm ###
48 # get SplineVertIndices to keep
49 def simplypoly(splineVerts
, options
):
51 newVerts
= [] # list of vertindices to keep
52 points
= splineVerts
# list of 3dVectors
53 pointCurva
= [] # table with curvatures
54 curvatures
= [] # averaged curvatures per vert
57 order
= options
[3] # order of sliding beziercurves
58 k_thresh
= options
[2] # curvature threshold
59 dis_error
= options
[6] # additional distance error
61 # get curvatures per vert
62 for i
, point
in enumerate(points
[: -(order
- 1)]):
63 BVerts
= points
[i
: i
+ order
]
64 for b
, BVert
in enumerate(BVerts
[1: -1]):
65 deriv1
= getDerivative(BVerts
, 1 / (order
- 1), order
- 1)
66 deriv2
= getDerivative(BVerts
, 1 / (order
- 1), order
- 2)
67 curva
= getCurvature(deriv1
, deriv2
)
68 pointCurva
[i
+ b
+ 1].append(curva
)
70 # average the curvatures
71 for i
in range(len(points
)):
72 avgCurva
= sum(pointCurva
[i
]) / (order
- 1)
73 curvatures
.append(avgCurva
)
75 # get distancevalues per vert - same as Ramer-Douglas-Peucker
77 distances
= [0.0] # first vert is always kept
78 for i
, point
in enumerate(points
[1: -1]):
79 dist
= altitude(points
[i
], points
[i
+ 2], points
[i
+ 1])
80 distances
.append(dist
)
81 distances
.append(0.0) # last vert is always kept
83 # generate list of vert indices to keep
84 # tested against averaged curvatures and distances of neighbour verts
85 newVerts
.append(0) # first vert is always kept
86 for i
, curv
in enumerate(curvatures
):
87 if (curv
>= k_thresh
* 0.01 or distances
[i
] >= dis_error
* 0.1):
89 newVerts
.append(len(curvatures
) - 1) # last vert is always kept
94 # get binomial coefficient
98 for i
in range(1, n
+ 1):
107 # get nth derivative of order(len(verts)) bezier curve
108 def getDerivative(verts
, t
, nth
):
109 order
= len(verts
) - 1 - nth
117 for i
in range(len(verts
) - 1):
118 derivVerts
.append(verts
[i
+ 1] - verts
[i
])
123 if len(verts
[0]) == 3:
124 point
= Vector((0, 0, 0))
125 if len(verts
[0]) == 2:
126 point
= Vector((0, 0))
128 for i
, vert
in enumerate(QVerts
):
129 point
+= binom(order
, i
) * pow(t
, i
) * pow(1 - t
, order
- i
) * vert
135 # get curvature from first, second derivative
136 def getCurvature(deriv1
, deriv2
):
137 if deriv1
.length
== 0: # in case of points in straight line
140 curvature
= (deriv1
.cross(deriv2
)).length
/ pow(deriv1
.length
, 3)
144 # ### Ramer-Douglas-Peucker algorithm ###
146 # get altitude of vert
147 def altitude(point1
, point2
, pointn
):
148 edge1
= point2
- point1
149 edge2
= pointn
- point1
150 if edge2
.length
== 0:
153 if edge1
.length
== 0:
154 altitude
= edge2
.length
156 alpha
= edge1
.angle(edge2
)
157 altitude
= sin(alpha
) * edge2
.length
161 # iterate through verts
162 def iterate(points
, newVerts
, error
):
164 for newIndex
in range(len(newVerts
) - 1):
167 for i
, point
in enumerate(points
[newVerts
[newIndex
] + 1: newVerts
[newIndex
+ 1]]):
168 alti
= altitude(points
[newVerts
[newIndex
]], points
[newVerts
[newIndex
+ 1]], point
)
169 if alti
> alti_store
:
171 if alti_store
>= error
:
172 bigVert
= i
+ 1 + newVerts
[newIndex
]
180 # get SplineVertIndices to keep
181 def simplify_RDP(splineVerts
, options
):
185 # set first and last vert
186 newVerts
= [0, len(splineVerts
) - 1]
188 # iterate through the points
190 while new
is not False:
191 new
= iterate(splineVerts
, newVerts
, error
)
198 # ### CURVE GENERATION ###
200 # set bezierhandles to auto
201 def setBezierHandles(newCurve
):
203 for spline
in newCurve
.data
.splines
:
204 for p
in spline
.bezier_points
:
205 p
.handle_left_type
= 'AUTO'
206 p
.handle_right_type
= 'AUTO'
209 # get array of new coords for new spline from vertindices
210 def vertsToPoints(newVerts
, splineVerts
, splineType
):
214 # array for BEZIER spline output
215 if splineType
== 'BEZIER':
217 newPoints
+= splineVerts
[v
].to_tuple()
219 # array for nonBEZIER output
222 newPoints
+= (splineVerts
[v
].to_tuple())
223 if splineType
== 'NURBS':
224 newPoints
.append(1) # for nurbs w = 1
225 else: # for poly w = 0
230 # ### MAIN OPERATIONS ###
232 def main(context
, obj
, options
, curve_dimension
):
235 degreeOut
= options
[5]
236 keepShort
= options
[7]
237 bpy
.ops
.object.select_all(action
='DESELECT')
238 scene
= context
.scene
239 splines
= obj
.data
.splines
.values()
241 # create curvedatablock
242 curve
= bpy
.data
.curves
.new("Simple_" + obj
.name
, type='CURVE')
243 curve
.dimensions
= curve_dimension
246 for spline_i
, spline
in enumerate(splines
):
247 # test if spline is a long enough
248 if len(spline
.points
) >= 3 or keepShort
:
249 # check what type of spline to create
250 if output
== 'INPUT':
251 splineType
= spline
.type
255 # get vec3 list to simplify
256 if spline
.type == 'BEZIER': # get bezierverts
257 splineVerts
= [splineVert
.co
.copy()
258 for splineVert
in spline
.bezier_points
.values()]
260 else: # verts from all other types of curves
261 splineVerts
= [splineVert
.co
.to_3d()
262 for splineVert
in spline
.points
.values()]
264 # simplify spline according to mode
265 if mode
== 'DISTANCE':
266 newVerts
= simplify_RDP(splineVerts
, options
)
268 if mode
== 'CURVATURE':
269 newVerts
= simplypoly(splineVerts
, options
)
271 # convert indices into vectors3D
272 newPoints
= vertsToPoints(newVerts
, splineVerts
, splineType
)
275 newSpline
= curve
.splines
.new(type=splineType
)
277 # put newPoints into spline according to type
278 if splineType
== 'BEZIER':
279 newSpline
.bezier_points
.add(int(len(newPoints
) * 0.33))
280 newSpline
.bezier_points
.foreach_set('co', newPoints
)
282 newSpline
.points
.add(int(len(newPoints
) * 0.25 - 1))
283 newSpline
.points
.foreach_set('co', newPoints
)
285 # set degree of outputNurbsCurve
286 if output
== 'NURBS':
287 newSpline
.order_u
= degreeOut
290 newSpline
.use_endpoint_u
= spline
.use_endpoint_u
292 # create new object and put into scene
293 newCurve
= bpy
.data
.objects
.new("Simple_" + obj
.name
, curve
)
294 coll
= context
.view_layer
.active_layer_collection
.collection
295 coll
.objects
.link(newCurve
)
296 newCurve
.select_set(True)
298 context
.view_layer
.objects
.active
= newCurve
299 newCurve
.matrix_world
= obj
.matrix_world
301 # set bezierhandles to auto
302 setBezierHandles(newCurve
)
307 # get preoperator fcurves
308 def getFcurveData(obj
):
310 for fc
in obj
.animation_data
.action
.fcurves
:
312 fcVerts
= [vcVert
.co
.to_3d()
313 for vcVert
in fc
.keyframe_points
.values()]
314 fcurves
.append(fcVerts
)
318 def selectedfcurves(obj
):
320 for i
, fc
in enumerate(obj
.animation_data
.action
.fcurves
):
322 fcurves_sel
.append(fc
)
327 def fcurves_simplify(context
, obj
, options
, fcurves
):
331 # get indices of selected fcurves
332 fcurve_sel
= selectedfcurves(obj
)
335 for fcurve_i
, fcurve
in enumerate(fcurves
):
336 # test if fcurve is long enough
338 # simplify spline according to mode
339 if mode
== 'DISTANCE':
340 newVerts
= simplify_RDP(fcurve
, options
)
342 if mode
== 'CURVATURE':
343 newVerts
= simplypoly(fcurve
, options
)
345 # convert indices into vectors3D
348 # this is different from the main() function for normal curves, different api...
350 newPoints
.append(fcurve
[v
])
352 # remove all points from curve first
353 for i
in range(len(fcurve
) - 1, 0, -1):
354 fcurve_sel
[fcurve_i
].keyframe_points
.remove(fcurve_sel
[fcurve_i
].keyframe_points
[i
])
355 # put newPoints into fcurve
357 fcurve_sel
[fcurve_i
].keyframe_points
.insert(frame
=v
[0], value
=v
[1])
361 # ### MENU append ###
363 def menu_func(self
, context
):
364 self
.layout
.operator("graph.simplify")
367 def menu(self
, context
):
368 self
.layout
.operator("curve.simplify", text
="Curve Simplify", icon
="CURVE_DATA")
372 # ### ANIMATION CURVES OPERATOR ###
374 class GRAPH_OT_simplify(Operator
):
375 bl_idname
= "graph.simplify"
376 bl_label
= "Simplify F-Curves"
377 bl_description
= ("Simplify selected Curves\n"
378 "Does not operate on short Splines (less than 3 points)")
379 bl_options
= {'REGISTER', 'UNDO'}
383 ('DISTANCE', 'Distance', 'Distance-based simplification (Poly)'),
384 ('CURVATURE', 'Curvature', 'Curvature-based simplification (RDP)')]
387 description
="Choose algorithm to use",
390 k_thresh
: FloatProperty(
393 default
=0, precision
=5,
394 description
="Threshold"
396 pointsNr
: IntProperty(
401 description
="Degree of curve to get averaged curvatures"
403 error
: FloatProperty(
405 description
="Maximum allowed distance error",
406 min=0.0, soft_min
=0.0,
407 default
=0, precision
=5,
410 degreeOut
: IntProperty(
415 description
="Degree of new curve"
417 dis_error
: FloatProperty(
418 name
="Distance error",
419 description
="Maximum allowed distance error in Blender Units",
421 default
=0.0, precision
=5
425 def draw(self
, context
):
427 col
= layout
.column()
429 col
.label(text
="Distance Error:")
430 col
.prop(self
, "error", expand
=True)
433 def poll(cls
, context
):
435 obj
= context
.active_object
438 animdata
= obj
.animation_data
440 act
= animdata
.action
442 fcurves
= act
.fcurves
443 return (obj
and fcurves
)
445 def execute(self
, context
):
456 obj
= context
.active_object
459 self
.fcurves
= getFcurveData(obj
)
461 fcurves_simplify(context
, obj
, options
, self
.fcurves
)
466 # ### Curves OPERATOR ###
467 class CURVE_OT_simplify(Operator
):
468 bl_idname
= "curve.simplify"
469 bl_label
= "Simplify Curves"
470 bl_description
= ("Simplify the existing Curve based upon the chosen settings\n"
471 "Notes: Needs an existing Curve object,\n"
472 "Outputs a new Curve with the Simple prefix in the name")
473 bl_options
= {'REGISTER', 'UNDO'}
477 ('DISTANCE', 'Distance', 'Distance-based simplification (Poly)'),
478 ('CURVATURE', 'Curvature', 'Curvature-based simplification (RDP)')
482 description
="Choose algorithm to use",
486 ('INPUT', 'Input', 'Same type as input spline'),
487 ('NURBS', 'Nurbs', 'NURBS'),
488 ('BEZIER', 'Bezier', 'BEZIER'),
489 ('POLY', 'Poly', 'POLY')
491 output
: EnumProperty(
492 name
="Output splines",
493 description
="Type of splines to output",
496 k_thresh
: FloatProperty(
499 default
=0, precision
=5,
500 description
="Threshold"
502 pointsNr
: IntProperty(
507 description
="Degree of curve to get averaged curvatures"
509 error
: FloatProperty(
511 description
="Maximum allowed distance error in Blender Units",
513 default
=0.0, precision
=5,
516 degreeOut
: IntProperty(
521 description
="Degree of new curve"
523 dis_error
: FloatProperty(
524 name
="Distance error",
525 description
="Maximum allowed distance error in Blender Units",
529 keepShort
: BoolProperty(
530 name
="Keep short splines",
531 description
="Keep short splines (less than 3 points)",
535 def draw(self
, context
):
537 col
= layout
.column()
539 col
.label(text
="Distance Error:")
540 col
.prop(self
, "error", expand
=True)
541 col
.prop(self
, "output", text
="Output", icon
="OUTLINER_OB_CURVE")
542 if self
.output
== "NURBS":
543 col
.prop(self
, "degreeOut", expand
=True)
545 col
.prop(self
, "keepShort", expand
=True)
548 def poll(cls
, context
):
549 obj
= context
.active_object
550 return (obj
and obj
.type == 'CURVE')
552 def execute(self
, context
):
564 bpy
.ops
.object.mode_set(mode
='OBJECT')
565 obj
= context
.active_object
566 curve_dimension
= obj
.data
.dimensions
568 main(context
, obj
, options
, curve_dimension
)
569 except Exception as e
:
570 error_handlers(self
, "curve.simplify", e
, "Simplify Curves")
575 ## Initial use Curve Remove Doubles ##
577 def main_rd(context
, distance
= 0.01):
579 selected_Curves
= context
.selected_objects
580 if bpy
.ops
.object.mode_set
.poll():
581 bpy
.ops
.object.mode_set(mode
='EDIT')
586 for curve
in selected_Curves
:
587 for spline
in curve
.data
.splines
:
588 if spline
.type == 'BEZIER':
589 if len(spline
.bezier_points
) > 1:
590 for i
in range(0, len(spline
.bezier_points
)):
593 ii
= len(spline
.bezier_points
) - 1
597 dot
= spline
.bezier_points
[i
];
598 dot1
= spline
.bezier_points
[ii
];
600 while dot1
in bezier_dellist
and i
!= ii
:
603 ii
= len(spline
.bezier_points
)-1
604 dot1
= spline
.bezier_points
[ii
]
606 if dot
.select_control_point
and dot1
.select_control_point
and (i
!=0 or spline
.use_cyclic_u
):
608 if (dot
.co
-dot1
.co
).length
< distance
:
609 # remove points and recreate hangles
610 dot1
.handle_right_type
= "FREE"
611 dot1
.handle_right
= dot
.handle_right
612 dot1
.co
= (dot
.co
+ dot1
.co
) / 2
613 bezier_dellist
.append(dot
)
616 # Handles that are on main point position converts to vector,
617 # if next handle are also vector
618 if dot
.handle_left_type
== 'VECTOR' and (dot1
.handle_right
- dot1
.co
).length
< distance
:
619 dot1
.handle_right_type
= "VECTOR"
620 if dot1
.handle_right_type
== 'VECTOR' and (dot
.handle_left
- dot
.co
).length
< distance
:
621 dot
.handle_left_type
= "VECTOR"
623 if len(spline
.points
) > 1:
624 for i
in range(0, len(spline
.points
)):
627 ii
= len(spline
.points
) - 1
631 dot
= spline
.points
[i
];
632 dot1
= spline
.points
[ii
];
634 while dot1
in dellist
and i
!= ii
:
637 ii
= len(spline
.points
)-1
638 dot1
= spline
.points
[ii
]
640 if dot
.select
and dot1
.select
and (i
!=0 or spline
.use_cyclic_u
):
642 if (dot
.co
-dot1
.co
).length
< distance
:
643 dot1
.co
= (dot
.co
+ dot1
.co
) / 2
646 bpy
.ops
.curve
.select_all(action
= 'DESELECT')
648 for dot
in bezier_dellist
:
649 dot
.select_control_point
= True
654 bezier_count
= len(bezier_dellist
)
657 bpy
.ops
.curve
.delete(type = 'VERT')
659 bpy
.ops
.curve
.select_all(action
= 'DESELECT')
661 return bezier_count
+ count
665 class Curve_OT_CurveRemvDbs(bpy
.types
.Operator
):
666 """Merge consecutive points that are near to each other"""
667 bl_idname
= 'curve.remove_double'
668 bl_label
= 'Merge By Distance'
669 bl_options
= {'REGISTER', 'UNDO'}
671 distance
: bpy
.props
.FloatProperty(name
= 'Distance', default
= 0.01, soft_min
= 0.001, step
= 0.1)
674 def poll(cls
, context
):
675 obj
= context
.active_object
676 return (obj
and obj
.type == 'CURVE')
678 def execute(self
, context
):
679 removed
=main_rd(context
, self
.distance
)
680 self
.report({'INFO'}, "Removed %d bezier points" % removed
)
683 def menu_func_rd(self
, context
):
684 self
.layout
.operator(Curve_OT_CurveRemvDbs
.bl_idname
, text
='Merge By Distance')
690 Curve_OT_CurveRemvDbs
,
695 from bpy
.utils
import register_class
699 #bpy.types.GRAPH_MT_channel.append(menu_func)
700 #bpy.types.DOPESHEET_MT_channel.append(menu_func)
701 bpy
.types
.VIEW3D_MT_curve_add
.append(menu
)
702 bpy
.types
.VIEW3D_MT_edit_curve_context_menu
.prepend(menu
)
703 bpy
.types
.VIEW3D_MT_edit_curve_context_menu
.prepend(menu_func_rd
)
707 from bpy
.utils
import unregister_class
708 for cls
in reversed(classes
):
709 unregister_class(cls
)
711 #bpy.types.GRAPH_MT_channel.remove(menu_func)
712 #bpy.types.DOPESHEET_MT_channel.remove(menu_func)
713 bpy
.types
.VIEW3D_MT_curve_add
.remove(menu
)
714 bpy
.types
.VIEW3D_MT_edit_curve_context_menu
.remove(menu
)
715 bpy
.types
.VIEW3D_MT_edit_curve_context_menu
.remove(menu_func_rd
)
717 if __name__
== "__main__":