sun_position: fix warning from deleted prop in User Preferences
[blender-addons.git] / curve_simplify.py
blob2817aef26aeb687b24b023e6a7ff6c1ea812c137
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 "wiki_url": "https://docs.blender.org/manual/en/dev/addons/"
28 "add_curve/simplify_curves.html",
29 "category": "Add Curve",
32 """
33 This script simplifies Curve objects and animation F-Curves
34 This script will also Merge by Distance 3d view curves in edit mode
35 """
37 import bpy
38 from bpy.props import (
39 BoolProperty,
40 EnumProperty,
41 FloatProperty,
42 IntProperty,
44 import mathutils
45 from math import (
46 sin,
47 pow,
49 from bpy.types import Operator
52 def error_handlers(self, op_name, errors, reports="ERROR"):
53 if self and reports:
54 self.report({'INFO'},
55 reports + ": some operations could not be performed "
56 "(See Console for more info)")
58 print("\n[Simplify Curves]\nOperator: {}\nErrors: {}\n".format(op_name, errors))
61 # Check for curve
63 # ### simplipoly algorithm ###
65 # get SplineVertIndices to keep
66 def simplypoly(splineVerts, options):
67 # main vars
68 newVerts = [] # list of vertindices to keep
69 points = splineVerts # list of 3dVectors
70 pointCurva = [] # table with curvatures
71 curvatures = [] # averaged curvatures per vert
72 for p in points:
73 pointCurva.append([])
74 order = options[3] # order of sliding beziercurves
75 k_thresh = options[2] # curvature threshold
76 dis_error = options[6] # additional distance error
78 # get curvatures per vert
79 for i, point in enumerate(points[: -(order - 1)]):
80 BVerts = points[i: i + order]
81 for b, BVert in enumerate(BVerts[1: -1]):
82 deriv1 = getDerivative(BVerts, 1 / (order - 1), order - 1)
83 deriv2 = getDerivative(BVerts, 1 / (order - 1), order - 2)
84 curva = getCurvature(deriv1, deriv2)
85 pointCurva[i + b + 1].append(curva)
87 # average the curvatures
88 for i in range(len(points)):
89 avgCurva = sum(pointCurva[i]) / (order - 1)
90 curvatures.append(avgCurva)
92 # get distancevalues per vert - same as Ramer-Douglas-Peucker
93 # but for every vert
94 distances = [0.0] # first vert is always kept
95 for i, point in enumerate(points[1: -1]):
96 dist = altitude(points[i], points[i + 2], points[i + 1])
97 distances.append(dist)
98 distances.append(0.0) # last vert is always kept
100 # generate list of vert indices to keep
101 # tested against averaged curvatures and distances of neighbour verts
102 newVerts.append(0) # first vert is always kept
103 for i, curv in enumerate(curvatures):
104 if (curv >= k_thresh * 0.01 or distances[i] >= dis_error * 0.1):
105 newVerts.append(i)
106 newVerts.append(len(curvatures) - 1) # last vert is always kept
108 return newVerts
111 # get binomial coefficient
112 def binom(n, m):
113 b = [0] * (n + 1)
114 b[0] = 1
115 for i in range(1, n + 1):
116 b[i] = 1
117 j = i - 1
118 while j > 0:
119 b[j] += b[j - 1]
120 j -= 1
121 return b[m]
124 # get nth derivative of order(len(verts)) bezier curve
125 def getDerivative(verts, t, nth):
126 order = len(verts) - 1 - nth
127 QVerts = []
129 if nth:
130 for i in range(nth):
131 if QVerts:
132 verts = QVerts
133 derivVerts = []
134 for i in range(len(verts) - 1):
135 derivVerts.append(verts[i + 1] - verts[i])
136 QVerts = derivVerts
137 else:
138 QVerts = verts
140 if len(verts[0]) == 3:
141 point = Vector((0, 0, 0))
142 if len(verts[0]) == 2:
143 point = Vector((0, 0))
145 for i, vert in enumerate(QVerts):
146 point += binom(order, i) * pow(t, i) * pow(1 - t, order - i) * vert
147 deriv = point
149 return deriv
152 # get curvature from first, second derivative
153 def getCurvature(deriv1, deriv2):
154 if deriv1.length == 0: # in case of points in straight line
155 curvature = 0
156 return curvature
157 curvature = (deriv1.cross(deriv2)).length / pow(deriv1.length, 3)
158 return curvature
161 # ### Ramer-Douglas-Peucker algorithm ###
163 # get altitude of vert
164 def altitude(point1, point2, pointn):
165 edge1 = point2 - point1
166 edge2 = pointn - point1
167 if edge2.length == 0:
168 altitude = 0
169 return altitude
170 if edge1.length == 0:
171 altitude = edge2.length
172 return altitude
173 alpha = edge1.angle(edge2)
174 altitude = sin(alpha) * edge2.length
175 return altitude
178 # iterate through verts
179 def iterate(points, newVerts, error):
180 new = []
181 for newIndex in range(len(newVerts) - 1):
182 bigVert = 0
183 alti_store = 0
184 for i, point in enumerate(points[newVerts[newIndex] + 1: newVerts[newIndex + 1]]):
185 alti = altitude(points[newVerts[newIndex]], points[newVerts[newIndex + 1]], point)
186 if alti > alti_store:
187 alti_store = alti
188 if alti_store >= error:
189 bigVert = i + 1 + newVerts[newIndex]
190 if bigVert:
191 new.append(bigVert)
192 if new == []:
193 return False
194 return new
197 # get SplineVertIndices to keep
198 def simplify_RDP(splineVerts, options):
199 # main vars
200 error = options[4]
202 # set first and last vert
203 newVerts = [0, len(splineVerts) - 1]
205 # iterate through the points
206 new = 1
207 while new is not False:
208 new = iterate(splineVerts, newVerts, error)
209 if new:
210 newVerts += new
211 newVerts.sort()
212 return newVerts
215 # ### CURVE GENERATION ###
217 # set bezierhandles to auto
218 def setBezierHandles(newCurve):
219 # Faster:
220 for spline in newCurve.data.splines:
221 for p in spline.bezier_points:
222 p.handle_left_type = 'AUTO'
223 p.handle_right_type = 'AUTO'
226 # get array of new coords for new spline from vertindices
227 def vertsToPoints(newVerts, splineVerts, splineType):
228 # main vars
229 newPoints = []
231 # array for BEZIER spline output
232 if splineType == 'BEZIER':
233 for v in newVerts:
234 newPoints += splineVerts[v].to_tuple()
236 # array for nonBEZIER output
237 else:
238 for v in newVerts:
239 newPoints += (splineVerts[v].to_tuple())
240 if splineType == 'NURBS':
241 newPoints.append(1) # for nurbs w = 1
242 else: # for poly w = 0
243 newPoints.append(0)
244 return newPoints
247 # ### MAIN OPERATIONS ###
249 def main(context, obj, options, curve_dimension):
250 mode = options[0]
251 output = options[1]
252 degreeOut = options[5]
253 keepShort = options[7]
254 bpy.ops.object.select_all(action='DESELECT')
255 scene = context.scene
256 splines = obj.data.splines.values()
258 # create curvedatablock
259 curve = bpy.data.curves.new("Simple_" + obj.name, type='CURVE')
260 curve.dimensions = curve_dimension
262 # go through splines
263 for spline_i, spline in enumerate(splines):
264 # test if spline is a long enough
265 if len(spline.points) >= 3 or keepShort:
266 # check what type of spline to create
267 if output == 'INPUT':
268 splineType = spline.type
269 else:
270 splineType = output
272 # get vec3 list to simplify
273 if spline.type == 'BEZIER': # get bezierverts
274 splineVerts = [splineVert.co.copy()
275 for splineVert in spline.bezier_points.values()]
277 else: # verts from all other types of curves
278 splineVerts = [splineVert.co.to_3d()
279 for splineVert in spline.points.values()]
281 # simplify spline according to mode
282 if mode == 'DISTANCE':
283 newVerts = simplify_RDP(splineVerts, options)
285 if mode == 'CURVATURE':
286 newVerts = simplypoly(splineVerts, options)
288 # convert indices into vectors3D
289 newPoints = vertsToPoints(newVerts, splineVerts, splineType)
291 # create new spline
292 newSpline = curve.splines.new(type=splineType)
294 # put newPoints into spline according to type
295 if splineType == 'BEZIER':
296 newSpline.bezier_points.add(int(len(newPoints) * 0.33))
297 newSpline.bezier_points.foreach_set('co', newPoints)
298 else:
299 newSpline.points.add(int(len(newPoints) * 0.25 - 1))
300 newSpline.points.foreach_set('co', newPoints)
302 # set degree of outputNurbsCurve
303 if output == 'NURBS':
304 newSpline.order_u = degreeOut
306 # splineoptions
307 newSpline.use_endpoint_u = spline.use_endpoint_u
309 # create new object and put into scene
310 newCurve = bpy.data.objects.new("Simple_" + obj.name, curve)
311 coll = context.view_layer.active_layer_collection.collection
312 coll.objects.link(newCurve)
313 newCurve.select_set(True)
315 context.view_layer.objects.active = newCurve
316 newCurve.matrix_world = obj.matrix_world
318 # set bezierhandles to auto
319 setBezierHandles(newCurve)
321 return
324 # get preoperator fcurves
325 def getFcurveData(obj):
326 fcurves = []
327 for fc in obj.animation_data.action.fcurves:
328 if fc.select:
329 fcVerts = [vcVert.co.to_3d()
330 for vcVert in fc.keyframe_points.values()]
331 fcurves.append(fcVerts)
332 return fcurves
335 def selectedfcurves(obj):
336 fcurves_sel = []
337 for i, fc in enumerate(obj.animation_data.action.fcurves):
338 if fc.select:
339 fcurves_sel.append(fc)
340 return fcurves_sel
343 # fCurves Main
344 def fcurves_simplify(context, obj, options, fcurves):
345 # main vars
346 mode = options[0]
348 # get indices of selected fcurves
349 fcurve_sel = selectedfcurves(obj)
351 # go through fcurves
352 for fcurve_i, fcurve in enumerate(fcurves):
353 # test if fcurve is long enough
354 if len(fcurve) >= 3:
355 # simplify spline according to mode
356 if mode == 'DISTANCE':
357 newVerts = simplify_RDP(fcurve, options)
359 if mode == 'CURVATURE':
360 newVerts = simplypoly(fcurve, options)
362 # convert indices into vectors3D
363 newPoints = []
365 # this is different from the main() function for normal curves, different api...
366 for v in newVerts:
367 newPoints.append(fcurve[v])
369 # remove all points from curve first
370 for i in range(len(fcurve) - 1, 0, -1):
371 fcurve_sel[fcurve_i].keyframe_points.remove(fcurve_sel[fcurve_i].keyframe_points[i])
372 # put newPoints into fcurve
373 for v in newPoints:
374 fcurve_sel[fcurve_i].keyframe_points.insert(frame=v[0], value=v[1])
375 return
378 # ### MENU append ###
380 def menu_func(self, context):
381 self.layout.operator("graph.simplify")
384 def menu(self, context):
385 self.layout.operator("curve.simplify", text="Curve Simplify", icon="CURVE_DATA")
389 # ### ANIMATION CURVES OPERATOR ###
391 class GRAPH_OT_simplify(Operator):
392 bl_idname = "graph.simplify"
393 bl_label = "Simplify F-Curves"
394 bl_description = ("Simplify selected Curves\n"
395 "Does not operate on short Splines (less than 3 points)")
396 bl_options = {'REGISTER', 'UNDO'}
398 # Properties
399 opModes = [
400 ('DISTANCE', 'Distance', 'Distance-based simplification (Poly)'),
401 ('CURVATURE', 'Curvature', 'Curvature-based simplification (RDP)')]
402 mode: EnumProperty(
403 name="Mode",
404 description="Choose algorithm to use",
405 items=opModes
407 k_thresh: FloatProperty(
408 name="k",
409 min=0, soft_min=0,
410 default=0, precision=5,
411 description="Threshold"
413 pointsNr: IntProperty(
414 name="n",
415 min=5, soft_min=5,
416 max=16, soft_max=9,
417 default=5,
418 description="Degree of curve to get averaged curvatures"
420 error: FloatProperty(
421 name="Error",
422 description="Maximum allowed distance error",
423 min=0.0, soft_min=0.0,
424 default=0, precision=5,
425 step = 0.1
427 degreeOut: IntProperty(
428 name="Degree",
429 min=3, soft_min=3,
430 max=7, soft_max=7,
431 default=5,
432 description="Degree of new curve"
434 dis_error: FloatProperty(
435 name="Distance error",
436 description="Maximum allowed distance error in Blender Units",
437 min=0, soft_min=0,
438 default=0.0, precision=5
440 fcurves = []
442 def draw(self, context):
443 layout = self.layout
444 col = layout.column()
446 col.label(text="Distance Error:")
447 col.prop(self, "error", expand=True)
449 @classmethod
450 def poll(cls, context):
451 # Check for animdata
452 obj = context.active_object
453 fcurves = False
454 if obj:
455 animdata = obj.animation_data
456 if animdata:
457 act = animdata.action
458 if act:
459 fcurves = act.fcurves
460 return (obj and fcurves)
462 def execute(self, context):
463 options = [
464 self.mode, # 0
465 self.mode, # 1
466 self.k_thresh, # 2
467 self.pointsNr, # 3
468 self.error, # 4
469 self.degreeOut, # 6
470 self.dis_error # 7
473 obj = context.active_object
475 if not self.fcurves:
476 self.fcurves = getFcurveData(obj)
478 fcurves_simplify(context, obj, options, self.fcurves)
480 return {'FINISHED'}
483 # ### Curves OPERATOR ###
484 class CURVE_OT_simplify(Operator):
485 bl_idname = "curve.simplify"
486 bl_label = "Simplify Curves"
487 bl_description = ("Simplify the existing Curve based upon the chosen settings\n"
488 "Notes: Needs an existing Curve object,\n"
489 "Outputs a new Curve with the Simple prefix in the name")
490 bl_options = {'REGISTER', 'UNDO'}
492 # Properties
493 opModes = [
494 ('DISTANCE', 'Distance', 'Distance-based simplification (Poly)'),
495 ('CURVATURE', 'Curvature', 'Curvature-based simplification (RDP)')
497 mode: EnumProperty(
498 name="Mode",
499 description="Choose algorithm to use",
500 items=opModes
502 SplineTypes = [
503 ('INPUT', 'Input', 'Same type as input spline'),
504 ('NURBS', 'Nurbs', 'NURBS'),
505 ('BEZIER', 'Bezier', 'BEZIER'),
506 ('POLY', 'Poly', 'POLY')
508 output: EnumProperty(
509 name="Output splines",
510 description="Type of splines to output",
511 items=SplineTypes
513 k_thresh: FloatProperty(
514 name="k",
515 min=0, soft_min=0,
516 default=0, precision=5,
517 description="Threshold"
519 pointsNr: IntProperty(
520 name="n",
521 min=5, soft_min=5,
522 max=9, soft_max=9,
523 default=5,
524 description="Degree of curve to get averaged curvatures"
526 error: FloatProperty(
527 name="Error",
528 description="Maximum allowed distance error in Blender Units",
529 min=0, soft_min=0,
530 default=0.0, precision=5,
531 step = 0.1
533 degreeOut: IntProperty(
534 name="Degree",
535 min=3, soft_min=3,
536 max=7, soft_max=7,
537 default=5,
538 description="Degree of new curve"
540 dis_error: FloatProperty(
541 name="Distance error",
542 description="Maximum allowed distance error in Blender Units",
543 min=0, soft_min=0,
544 default=0.0
546 keepShort: BoolProperty(
547 name="Keep short splines",
548 description="Keep short splines (less than 3 points)",
549 default=True
552 def draw(self, context):
553 layout = self.layout
554 col = layout.column()
556 col.label(text="Distance Error:")
557 col.prop(self, "error", expand=True)
558 col.prop(self, "output", text="Output", icon="OUTLINER_OB_CURVE")
559 if self.output == "NURBS":
560 col.prop(self, "degreeOut", expand=True)
561 col.separator()
562 col.prop(self, "keepShort", expand=True)
564 @classmethod
565 def poll(cls, context):
566 obj = context.active_object
567 return (obj and obj.type == 'CURVE')
569 def execute(self, context):
570 options = [
571 self.mode, # 0
572 self.output, # 1
573 self.k_thresh, # 2
574 self.pointsNr, # 3
575 self.error, # 4
576 self.degreeOut, # 5
577 self.dis_error, # 6
578 self.keepShort # 7
580 try:
581 bpy.ops.object.mode_set(mode='OBJECT')
582 obj = context.active_object
583 curve_dimension = obj.data.dimensions
585 main(context, obj, options, curve_dimension)
586 except Exception as e:
587 error_handlers(self, "curve.simplify", e, "Simplify Curves")
588 return {'CANCELLED'}
590 return {'FINISHED'}
592 ## Initial use Curve Remove Doubles ##
594 def main_rd(context, distance = 0.01):
596 selected_Curves = context.selected_objects
597 if bpy.ops.object.mode_set.poll():
598 bpy.ops.object.mode_set(mode='EDIT')
600 for curve in selected_Curves:
601 bezier_dellist = []
602 dellist = []
604 for spline in curve.data.splines:
605 if spline.type == 'BEZIER':
606 if len(spline.bezier_points) > 1:
607 for i in range(0, len(spline.bezier_points)):
609 if i == 0:
610 ii = len(spline.bezier_points) - 1
611 else:
612 ii = i - 1
614 dot = spline.bezier_points[i];
615 dot1 = spline.bezier_points[ii];
617 while dot1 in bezier_dellist and i != ii:
618 ii -= 1
619 if ii < 0:
620 ii = len(spline.bezier_points)-1
621 dot1 = spline.bezier_points[ii]
623 if dot.select_control_point and dot1.select_control_point and (i!=0 or spline.use_cyclic_u):
625 if (dot.co-dot1.co).length < distance:
626 # remove points and recreate hangles
627 dot1.handle_right_type = "FREE"
628 dot1.handle_right = dot.handle_right
629 dot1.co = (dot.co + dot1.co) / 2
630 bezier_dellist.append(dot)
632 else:
633 # Handles that are on main point position converts to vector,
634 # if next handle are also vector
635 if dot.handle_left_type == 'VECTOR' and (dot1.handle_right - dot1.co).length < distance:
636 dot1.handle_right_type = "VECTOR"
637 if dot1.handle_right_type == 'VECTOR' and (dot.handle_left - dot.co).length < distance:
638 dot.handle_left_type = "VECTOR"
639 else:
640 if len(spline.points) > 1:
641 for i in range(0, len(spline.points)):
643 if i == 0:
644 ii = len(spline.points) - 1
645 else:
646 ii = i - 1
648 dot = spline.points[i];
649 dot1 = spline.points[ii];
651 while dot1 in dellist and i != ii:
652 ii -= 1
653 if ii < 0:
654 ii = len(spline.points)-1
655 dot1 = spline.points[ii]
657 if dot.select and dot1.select and (i!=0 or spline.use_cyclic_u):
659 if (dot.co-dot1.co).length < distance:
660 dot1.co = (dot.co + dot1.co) / 2
661 dellist.append(dot)
663 bpy.ops.curve.select_all(action = 'DESELECT')
665 for dot in bezier_dellist:
666 dot.select_control_point = True
668 for dot in dellist:
669 dot.select = True
671 bezier_count = len(bezier_dellist)
672 count = len(dellist)
674 bpy.ops.curve.delete(type = 'VERT')
676 bpy.ops.curve.select_all(action = 'DESELECT')
678 return bezier_count + count
682 class Curve_OT_CurveRemvDbs(bpy.types.Operator):
683 """Merge consecutive points that are near to each other"""
684 bl_idname = 'curve.remove_double'
685 bl_label = 'Merge By Distance'
686 bl_options = {'REGISTER', 'UNDO'}
688 distance: bpy.props.FloatProperty(name = 'Distance', default = 0.01, soft_min = 0.001, step = 0.1)
690 @classmethod
691 def poll(cls, context):
692 obj = context.active_object
693 return (obj and obj.type == 'CURVE')
695 def execute(self, context):
696 removed=main_rd(context, self.distance)
697 self.report({'INFO'}, "Removed %d bezier points" % removed)
698 return {'FINISHED'}
700 def menu_func_rd(self, context):
701 self.layout.operator(Curve_OT_CurveRemvDbs.bl_idname, text='Merge By Distance')
703 # Register
704 classes = [
705 GRAPH_OT_simplify,
706 CURVE_OT_simplify,
707 Curve_OT_CurveRemvDbs,
711 def register():
712 from bpy.utils import register_class
713 for cls in classes:
714 register_class(cls)
716 #bpy.types.GRAPH_MT_channel.append(menu_func)
717 #bpy.types.DOPESHEET_MT_channel.append(menu_func)
718 bpy.types.VIEW3D_MT_curve_add.append(menu)
719 bpy.types.VIEW3D_MT_edit_curve_context_menu.prepend(menu)
720 bpy.types.VIEW3D_MT_edit_curve_context_menu.prepend(menu_func_rd)
723 def unregister():
724 from bpy.utils import unregister_class
725 for cls in reversed(classes):
726 unregister_class(cls)
728 #bpy.types.GRAPH_MT_channel.remove(menu_func)
729 #bpy.types.DOPESHEET_MT_channel.remove(menu_func)
730 bpy.types.VIEW3D_MT_curve_add.remove(menu)
731 bpy.types.VIEW3D_MT_edit_curve_context_menu.remove(menu)
732 bpy.types.VIEW3D_MT_edit_curve_context_menu.remove(menu_func_rd)
734 if __name__ == "__main__":
735 register()