1 # SPDX-License-Identifier: GPL-2.0-or-later
5 "name": "SpiroFit, Bounce Spline, and Catenary",
6 "author": "Antonio Osprite, Liero, Atom, Jimmy Hazevoet",
9 "location": "Add > Curve > Knots",
10 "description": "SpiroFit, BounceSpline and Catenary adds "
11 "splines to selected mesh or objects",
13 "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/extra_objects.html",
18 from bpy
.types
import (
22 from bpy
.props
import (
29 from mathutils
import (
41 # ------------------------------------------------------------
42 # "Build a spiral that fit the active object"
43 # Spirofit, original blender 2.45 script by: Antonio Osprite
44 # http://www.kino3d.com/forum/viewtopic.php?t=5374
45 # ------------------------------------------------------------
47 d
= (Vector(v1
) - Vector(v2
)).length
51 def spiral_point(step
, radius
, z_coord
, spires
, waves
, wave_iscale
, rndm
):
52 x
= radius
* cos(spires
* step
) + (r
.random() - 0.5) * rndm
53 y
= radius
* sin(spires
* step
) + (r
.random() - 0.5) * rndm
54 z
= z_coord
+ (cos(waves
* step
* pi
) * wave_iscale
) + (r
.random() - 0.5) * rndm
58 def spirofit_spline(obj
,
71 bb_xmin
= min([v
[0] for v
in bb
])
72 bb_ymin
= min([v
[1] for v
in bb
])
73 bb_zmin
= min([v
[2] for v
in bb
])
74 bb_xmax
= max([v
[0] for v
in bb
])
75 bb_ymax
= max([v
[1] for v
in bb
])
76 bb_zmax
= max([v
[2] for v
in bb
])
78 radius
= distance([bb_xmax
, bb_ymax
, bb_zmin
], [bb_xmin
, bb_ymin
, bb_zmin
]) / 2.0
79 height
= bb_zmax
- bb_zmin
80 cx
= (bb_xmax
+ bb_xmin
) / 2.0
81 cy
= (bb_ymax
+ bb_ymin
) / 2.0
82 steps
= spires
* spire_resolution
84 for i
in range(steps
+ 1):
85 t
= bb_zmin
+ (2 * pi
/ steps
) * i
86 z
= bb_zmin
+ (float(height
) / steps
) * i
89 cp
= spiral_point(t
, radius
, z
, spires
, waves
, wave_iscale
, rndm_spire
)
91 if map_method
== 'RAYCAST':
92 success
, hit
, nor
, index
= obj
.ray_cast(Vector(cp
), (Vector([cx
, cy
, z
]) - Vector(cp
)))
94 points
.append((hit
+ offset
* nor
))
96 elif map_method
== 'CLOSESTPOINT':
97 success
, hit
, nor
, index
= obj
.closest_point_on_mesh(cp
)
99 points
.append((hit
+ offset
* nor
))
104 class SpiroFitSpline(Operator
):
105 bl_idname
= "object.add_spirofit_spline"
106 bl_label
= "SpiroFit"
107 bl_description
= "Wrap selected mesh in a spiral"
108 bl_options
= {'REGISTER', 'UNDO', 'PRESET'}
110 map_method
: EnumProperty(
113 description
="Mapping method",
114 items
=[('RAYCAST', 'Ray cast', 'Ray casting'),
115 ('CLOSESTPOINT', 'Closest point', 'Closest point on mesh')]
117 direction
: BoolProperty(
119 description
="Spire direction",
122 spire_resolution
: IntProperty(
123 name
="Spire Resolution",
128 description
="Number of steps for one turn"
130 spires
: IntProperty(
136 description
="Number of turns"
138 offset
: FloatProperty(
142 description
="Use normal direction to offset spline"
148 description
="Wave amount"
150 wave_iscale
: FloatProperty(
151 name
="Wave Intensity",
155 description
="Wave intensity scale"
157 rndm_spire
: FloatProperty(
162 description
="Randomise spire"
164 spline_name
: StringProperty(
168 spline_type
: EnumProperty(
171 description
="Spline type",
172 items
=[('POLY', 'Poly', 'Poly spline'),
173 ('BEZIER', 'Bezier', 'Bezier spline')]
175 resolution_u
: IntProperty(
180 description
="Curve resolution u"
182 bevel
: FloatProperty(
187 description
="Bevel depth"
189 bevel_res
: IntProperty(
190 name
="Bevel Resolution",
194 description
="Bevel resolution"
196 extrude
: FloatProperty(
201 description
="Extrude amount"
203 twist_mode
: EnumProperty(
206 description
="Twist method, type of tilt calculation",
207 items
=[('Z_UP', "Z-Up", 'Z Up'),
208 ('MINIMUM', "Minimum", 'Minimum'),
209 ('TANGENT', "Tangent", 'Tangent')]
211 twist_smooth
: FloatProperty(
216 description
="Twist smoothing amount for tangents"
218 tilt
: FloatProperty(
222 description
="Spline handle tilt"
224 random_radius
: FloatProperty(
229 description
="Randomise radius of spline controlpoints"
231 random_seed
: IntProperty(
235 description
="Random seed number"
237 origin_to_start
: BoolProperty(
238 name
="Origin at Start",
239 description
="Set origin at first point of spline",
242 refresh
: BoolProperty(
244 description
="Refresh spline",
247 auto_refresh
: BoolProperty(
249 description
="Auto refresh spline",
253 def draw(self
, context
):
255 col
= layout
.column(align
=True)
256 row
= col
.row(align
=True)
258 if self
.auto_refresh
is False:
260 elif self
.auto_refresh
is True:
263 row
.prop(self
, "auto_refresh", toggle
=True, icon
="AUTO", icon_only
=True)
264 row
.prop(self
, "refresh", toggle
=True, icon
="FILE_REFRESH", icon_only
=True)
265 row
.operator("object.add_spirofit_spline", text
="Add")
266 row
.prop(self
, "origin_to_start", toggle
=True, icon
="CURVE_DATA", icon_only
=True)
268 col
= layout
.column(align
=True)
269 col
.prop(self
, "spline_name")
271 col
.prop(self
, "map_method")
273 col
.prop(self
, "spire_resolution")
274 row
= col
.row(align
=True).split(factor
=0.9, align
=True)
275 row
.prop(self
, "spires")
276 row
.prop(self
, "direction", toggle
=True, text
="", icon
='ARROW_LEFTRIGHT')
277 col
.prop(self
, "offset")
278 col
.prop(self
, "waves")
279 col
.prop(self
, "wave_iscale")
280 col
.prop(self
, "rndm_spire")
281 col
.prop(self
, "random_seed")
282 draw_spline_settings(self
)
285 def poll(self
, context
):
286 ob
= context
.active_object
287 return ((ob
is not None) and
288 (context
.mode
== 'OBJECT'))
290 def invoke(self
, context
, event
):
292 return self
.execute(context
)
294 def execute(self
, context
):
296 return {'PASS_THROUGH'}
298 obj
= context
.active_object
299 if obj
.type != 'MESH':
300 self
.report({'WARNING'},
301 "Active Object is not a Mesh. Operation Cancelled")
304 bpy
.ops
.object.select_all(action
='DESELECT')
306 r
.seed(self
.random_seed
)
308 points
= spirofit_spline(
310 self
.spire_resolution
,
335 if self
.origin_to_start
is True:
336 move_origin_to_start()
338 if self
.auto_refresh
is False:
344 # ------------------------------------------------------------
345 # Bounce spline / Fiber mesh
346 # Original script by Liero and Atom
347 # https://blenderartists.org/forum/showthread.php?331750-Fiber-Mesh-Emulation
348 # ------------------------------------------------------------
350 rand
= Vector((r
.gauss(0, 1), r
.gauss(0, 1), r
.gauss(0, 1)))
351 vec
= rand
.normalized() * var
355 def bounce_spline(obj
,
363 dist
, points
= 1000, []
364 poly
= obj
.data
.polygons
370 print("No active face selected")
373 n
= r
.randint(0, len(poly
) - 1)
375 end
= poly
[n
].normal
.copy() * -1
376 start
= poly
[n
].center
377 points
.append(start
+ offset
* end
)
379 for i
in range(number
):
380 for ray
in range(extra
+ 1):
381 end
+= noise(ang_noise
)
383 hit
, nor
, index
= obj
.ray_cast(start
, end
* dist
)[-3:]
387 start
= hit
- nor
/ 10000
388 end
= end
.reflect(nor
).normalized()
389 points
.append(hit
+ offset
* nor
)
396 class BounceSpline(Operator
):
397 bl_idname
= "object.add_bounce_spline"
398 bl_label
= "Bounce Spline"
399 bl_description
= "Fill selected mesh with a spline"
400 bl_options
= {'REGISTER', 'UNDO', 'PRESET'}
402 bounce_number
: IntProperty(
408 description
="Number of bounces"
410 ang_noise
: FloatProperty(
411 name
="Angular Noise",
415 description
="Add some noise to ray direction"
417 offset
: FloatProperty(
421 description
="Use normal direction to offset spline"
428 description
="Number of extra tries if it fails to hit mesh"
430 active_face
: BoolProperty(
433 description
="Starts from active face or a random one"
435 spline_name
: StringProperty(
437 default
="BounceSpline"
439 spline_type
: EnumProperty(
442 description
="Spline type",
443 items
=[('POLY', "Poly", "Poly spline"),
444 ('BEZIER', "Bezier", "Bezier spline")]
446 resolution_u
: IntProperty(
451 description
="Curve resolution u"
453 bevel
: FloatProperty(
458 description
="Bevel depth"
460 bevel_res
: IntProperty(
461 name
="Bevel Resolution",
465 description
="Bevel resolution"
467 extrude
: FloatProperty(
472 description
="Extrude amount"
474 twist_mode
: EnumProperty(
477 description
="Twist method, type of tilt calculation",
478 items
=[('Z_UP', "Z-Up", 'Z Up'),
479 ('MINIMUM', "Minimum", 'Minimum'),
480 ('TANGENT', "Tangent", 'Tangent')]
482 twist_smooth
: FloatProperty(
487 description
="Twist smoothing amount for tangents"
489 tilt
: FloatProperty(
493 description
="Spline handle tilt"
495 random_radius
: FloatProperty(
500 description
="Randomise radius of spline controlpoints"
502 random_seed
: IntProperty(
506 description
="Random seed number"
508 origin_to_start
: BoolProperty(
509 name
="Origin at Start",
510 description
="Set origin at first point of spline",
513 refresh
: BoolProperty(
515 description
="Refresh spline",
518 auto_refresh
: BoolProperty(
520 description
="Auto refresh spline",
524 def draw(self
, context
):
526 col
= layout
.column(align
=True)
527 row
= col
.row(align
=True)
528 if self
.auto_refresh
is False:
530 elif self
.auto_refresh
is True:
533 row
.prop(self
, "auto_refresh", toggle
=True, icon
="AUTO", icon_only
=True)
534 row
.prop(self
, "refresh", toggle
=True, icon
="FILE_REFRESH", icon_only
=True)
535 row
.operator("object.add_bounce_spline", text
="Add")
536 row
.prop(self
, "origin_to_start", toggle
=True, icon
="CURVE_DATA", icon_only
=True)
538 col
= layout
.column(align
=True)
539 col
.prop(self
, "spline_name")
541 col
.prop(self
, "bounce_number")
542 row
= col
.row(align
=True).split(factor
=0.9, align
=True)
543 row
.prop(self
, "ang_noise")
544 row
.prop(self
, "active_face", toggle
=True, text
="", icon
="SNAP_FACE")
545 col
.prop(self
, "offset")
546 col
.prop(self
, "extra")
547 col
.prop(self
, "random_seed")
548 draw_spline_settings(self
)
551 def poll(self
, context
):
552 ob
= context
.active_object
553 return ((ob
is not None) and
554 (context
.mode
== 'OBJECT'))
556 def invoke(self
, context
, event
):
558 return self
.execute(context
)
560 def execute(self
, context
):
562 return {'PASS_THROUGH'}
564 obj
= context
.active_object
565 if obj
.type != 'MESH':
568 bpy
.ops
.object.select_all(action
='DESELECT')
570 r
.seed(self
.random_seed
)
572 points
= bounce_spline(
596 if self
.origin_to_start
is True:
597 move_origin_to_start()
599 if self
.auto_refresh
is False:
605 # ------------------------------------------------------------
606 # Hang Catenary curve between two selected objects
607 # ------------------------------------------------------------
616 lx
= end
[0] - start
[0]
617 ly
= end
[1] - start
[1]
618 lr
= sqrt(pow(lx
, 2) + pow(ly
, 2))
619 lv
= lr
/ 2 - (end
[2] - start
[2]) * a
/ lr
620 zv
= start
[2] - pow(lv
, 2) / (2 * a
)
626 x
= start
[0] + i
* slx
627 y
= start
[1] + i
* sly
628 z
= zv
+ pow((i
* slr
) - lv
, 2) / (2 * a
)
629 points
.append([x
, y
, z
])
634 class CatenaryCurve(Operator
):
635 bl_idname
= "object.add_catenary_curve"
636 bl_label
= "Catenary"
637 bl_description
= "Hang a curve between two selected objects"
638 bl_options
= {'REGISTER', 'UNDO', 'PRESET'}
642 description
="Resolution of the curve",
647 var_a
: FloatProperty(
649 description
="Catenary variable a",
655 spline_name
: StringProperty(
659 spline_type
: EnumProperty(
662 description
="Spline type",
663 items
=[('POLY', "Poly", "Poly spline"),
664 ('BEZIER', "Bezier", "Bezier spline")]
666 resolution_u
: IntProperty(
671 description
="Curve resolution u"
673 bevel
: FloatProperty(
678 description
="Bevel depth"
680 bevel_res
: IntProperty(
681 name
="Bevel Resolution",
685 description
="Bevel resolution"
687 extrude
: FloatProperty(
692 description
="Extrude amount"
694 twist_mode
: EnumProperty(
697 description
="Twist method, type of tilt calculation",
698 items
=[('Z_UP', "Z-Up", 'Z Up'),
699 ('MINIMUM', "Minimum", "Minimum"),
700 ('TANGENT', "Tangent", "Tangent")]
702 twist_smooth
: FloatProperty(
707 description
="Twist smoothing amount for tangents"
709 tilt
: FloatProperty(
713 description
="Spline handle tilt"
715 random_radius
: FloatProperty(
720 description
="Randomise radius of spline controlpoints"
722 random_seed
: IntProperty(
726 description
="Random seed number"
728 origin_to_start
: BoolProperty(
729 name
="Origin at Start",
730 description
="Set origin at first point of spline",
733 refresh
: BoolProperty(
735 description
="Refresh spline",
738 auto_refresh
: BoolProperty(
740 description
="Auto refresh spline",
744 def draw(self
, context
):
746 col
= layout
.column(align
=True)
747 row
= col
.row(align
=True)
749 if self
.auto_refresh
is False:
751 elif self
.auto_refresh
is True:
754 row
.prop(self
, "auto_refresh", toggle
=True, icon
="AUTO", icon_only
=True)
755 row
.prop(self
, "refresh", toggle
=True, icon
="FILE_REFRESH", icon_only
=True)
756 row
.operator("object.add_catenary_curve", text
="Add")
757 row
.prop(self
, "origin_to_start", toggle
=True, icon
="CURVE_DATA", icon_only
=True)
759 col
= layout
.column(align
=True)
760 col
.prop(self
, "spline_name")
762 col
.prop(self
, "steps")
763 col
.prop(self
, "var_a")
765 draw_spline_settings(self
)
766 col
= layout
.column(align
=True)
767 col
.prop(self
, "random_seed")
770 def poll(self
, context
):
771 ob
= context
.active_object
772 return ob
is not None
774 def invoke(self
, context
, event
):
776 return self
.execute(context
)
778 def execute(self
, context
):
780 return {'PASS_THROUGH'}
783 #ob1 = bpy.context.active_object
785 ob1
= bpy
.context
.selected_objects
[0]
786 ob2
= bpy
.context
.selected_objects
[1]
790 if (start
[0] == end
[0]) and (start
[1] == end
[1]):
791 self
.report({"WARNING"},
792 "Objects have the same X, Y location. Operation Cancelled")
796 self
.report({"WARNING"},
797 "Catenary could not be completed. Operation Cancelled")
800 bpy
.ops
.object.select_all(action
='DESELECT')
802 r
.seed(self
.random_seed
)
804 points
= catenary_curve(
825 if self
.origin_to_start
is True:
826 move_origin_to_start()
828 bpy
.ops
.object.origin_set(type='ORIGIN_GEOMETRY')
830 if self
.auto_refresh
is False:
836 # ------------------------------------------------------------
837 # Generate curve object from given points
838 # ------------------------------------------------------------
839 def add_curve_object(
842 spline_name
="Spline",
843 spline_type
='BEZIER',
849 twist_mode
='MINIMUM',
854 scene
= bpy
.context
.scene
855 vl
= bpy
.context
.view_layer
856 curve
= bpy
.data
.curves
.new(spline_name
, 'CURVE')
857 curve
.dimensions
= '3D'
858 spline
= curve
.splines
.new(spline_type
)
859 cur
= bpy
.data
.objects
.new(spline_name
, curve
)
860 spline
.radius_interpolation
= 'BSPLINE'
861 spline
.tilt_interpolation
= 'BSPLINE'
863 if spline_type
== 'BEZIER':
864 spline
.bezier_points
.add(int(len(verts
) - 1))
865 for i
in range(len(verts
)):
866 spline
.bezier_points
[i
].co
= verts
[i
]
867 spline
.bezier_points
[i
].handle_right_type
= 'AUTO'
868 spline
.bezier_points
[i
].handle_left_type
= 'AUTO'
869 spline
.bezier_points
[i
].radius
+= spline_radius
* r
.random()
870 spline
.bezier_points
[i
].tilt
= radians(tilt
)
872 spline
.points
.add(int(len(verts
) - 1))
873 for i
in range(len(verts
)):
874 spline
.points
[i
].co
= verts
[i
][0], verts
[i
][1], verts
[i
][2], 1
876 scene
.collection
.objects
.link(cur
)
877 cur
.data
.resolution_u
= resolution_u
878 cur
.data
.fill_mode
= 'FULL'
879 cur
.data
.bevel_depth
= bevel
880 cur
.data
.bevel_resolution
= bevel_resolution
881 cur
.data
.extrude
= extrude
882 cur
.data
.twist_mode
= twist_mode
883 cur
.data
.twist_smooth
= twist_smooth
884 cur
.matrix_world
= matrix
886 vl
.objects
.active
= cur
890 def move_origin_to_start():
891 active
= bpy
.context
.active_object
892 spline
= active
.data
.splines
[0]
894 if spline
.type == 'BEZIER':
895 start
= active
.matrix_world
@ spline
.bezier_points
[0].co
897 start
= active
.matrix_world
@ spline
.points
[0].co
900 cursor
= bpy
.context
.scene
.cursor
.location
.copy()
901 bpy
.context
.scene
.cursor
.location
= start
902 bpy
.ops
.object.origin_set(type='ORIGIN_CURSOR')
903 bpy
.context
.scene
.cursor
.location
= cursor
906 def draw_spline_settings(self
):
908 col
= layout
.column(align
=True)
910 col
.prop(self
, "spline_type")
912 col
.prop(self
, "resolution_u")
913 col
.prop(self
, "bevel")
914 col
.prop(self
, "bevel_res")
915 col
.prop(self
, "extrude")
917 if self
.spline_type
== 'BEZIER':
918 col
.prop(self
, "random_radius")
920 col
.prop(self
, "twist_mode")
923 if self
.twist_mode
== 'TANGENT':
924 col
.prop(self
, "twist_smooth")
926 if self
.spline_type
== 'BEZIER':
927 col
.prop(self
, "tilt")
930 # ------------------------------------------------------------
932 # ------------------------------------------------------------
934 bpy
.utils
.register_class(SpiroFitSpline
)
935 bpy
.utils
.register_class(BounceSpline
)
936 bpy
.utils
.register_class(CatenaryCurve
)
940 bpy
.utils
.unregister_class(SpiroFitSpline
)
941 bpy
.utils
.unregister_class(BounceSpline
)
942 bpy
.utils
.unregister_class(CatenaryCurve
)
945 if __name__
== "__main__":