1 # SPDX-FileCopyrightText: 2017-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
7 "name": "SpiroFit, Bounce Spline, and Catenary",
8 "author": "Antonio Osprite, Liero, Atom, Jimmy Hazevoet",
10 "blender": (2, 80, 0),
11 "location": "Add > Curve > Knots",
12 "description": "SpiroFit, BounceSpline and Catenary adds "
13 "splines to selected mesh or objects",
15 "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/extra_objects.html",
20 from bpy
.types
import (
24 from bpy
.props
import (
31 from mathutils
import (
43 # ------------------------------------------------------------
44 # "Build a spiral that fit the active object"
45 # Spirofit, original blender 2.45 script by: Antonio Osprite
46 # http://www.kino3d.com/forum/viewtopic.php?t=5374
47 # ------------------------------------------------------------
49 d
= (Vector(v1
) - Vector(v2
)).length
53 def spiral_point(step
, radius
, z_coord
, spires
, waves
, wave_iscale
, rndm
):
54 x
= radius
* cos(spires
* step
) + (r
.random() - 0.5) * rndm
55 y
= radius
* sin(spires
* step
) + (r
.random() - 0.5) * rndm
56 z
= z_coord
+ (cos(waves
* step
* pi
) * wave_iscale
) + (r
.random() - 0.5) * rndm
60 def spirofit_spline(obj
,
73 bb_xmin
= min([v
[0] for v
in bb
])
74 bb_ymin
= min([v
[1] for v
in bb
])
75 bb_zmin
= min([v
[2] for v
in bb
])
76 bb_xmax
= max([v
[0] for v
in bb
])
77 bb_ymax
= max([v
[1] for v
in bb
])
78 bb_zmax
= max([v
[2] for v
in bb
])
80 radius
= distance([bb_xmax
, bb_ymax
, bb_zmin
], [bb_xmin
, bb_ymin
, bb_zmin
]) / 2.0
81 height
= bb_zmax
- bb_zmin
82 cx
= (bb_xmax
+ bb_xmin
) / 2.0
83 cy
= (bb_ymax
+ bb_ymin
) / 2.0
84 steps
= spires
* spire_resolution
86 for i
in range(steps
+ 1):
87 t
= bb_zmin
+ (2 * pi
/ steps
) * i
88 z
= bb_zmin
+ (float(height
) / steps
) * i
91 cp
= spiral_point(t
, radius
, z
, spires
, waves
, wave_iscale
, rndm_spire
)
93 if map_method
== 'RAYCAST':
94 success
, hit
, nor
, index
= obj
.ray_cast(Vector(cp
), (Vector([cx
, cy
, z
]) - Vector(cp
)))
96 points
.append((hit
+ offset
* nor
))
98 elif map_method
== 'CLOSESTPOINT':
99 success
, hit
, nor
, index
= obj
.closest_point_on_mesh(cp
)
101 points
.append((hit
+ offset
* nor
))
106 class SpiroFitSpline(Operator
):
107 bl_idname
= "object.add_spirofit_spline"
108 bl_label
= "SpiroFit"
109 bl_description
= "Wrap selected mesh in a spiral"
110 bl_options
= {'REGISTER', 'UNDO', 'PRESET'}
112 map_method
: EnumProperty(
115 description
="Mapping method",
116 items
=[('RAYCAST', 'Ray cast', 'Ray casting'),
117 ('CLOSESTPOINT', 'Closest point', 'Closest point on mesh')]
119 direction
: BoolProperty(
121 description
="Spire direction",
124 spire_resolution
: IntProperty(
125 name
="Spire Resolution",
130 description
="Number of steps for one turn"
132 spires
: IntProperty(
138 description
="Number of turns"
140 offset
: FloatProperty(
144 description
="Use normal direction to offset spline"
150 description
="Wave amount"
152 wave_iscale
: FloatProperty(
153 name
="Wave Intensity",
157 description
="Wave intensity scale"
159 rndm_spire
: FloatProperty(
164 description
="Randomise spire"
166 spline_name
: StringProperty(
170 spline_type
: EnumProperty(
173 description
="Spline type",
174 items
=[('POLY', 'Poly', 'Poly spline'),
175 ('BEZIER', 'Bezier', 'Bezier spline')]
177 resolution_u
: IntProperty(
182 description
="Curve resolution u"
184 bevel
: FloatProperty(
189 description
="Bevel depth"
191 bevel_res
: IntProperty(
192 name
="Bevel Resolution",
196 description
="Bevel resolution"
198 extrude
: FloatProperty(
203 description
="Extrude amount"
205 twist_mode
: EnumProperty(
208 description
="Twist method, type of tilt calculation",
209 items
=[('Z_UP', "Z-Up", 'Z Up'),
210 ('MINIMUM', "Minimum", 'Minimum'),
211 ('TANGENT', "Tangent", 'Tangent')]
213 twist_smooth
: FloatProperty(
218 description
="Twist smoothing amount for tangents"
220 tilt
: FloatProperty(
224 description
="Spline handle tilt"
226 random_radius
: FloatProperty(
231 description
="Randomise radius of spline controlpoints"
233 random_seed
: IntProperty(
237 description
="Random seed number"
239 origin_to_start
: BoolProperty(
240 name
="Origin at Start",
241 description
="Set origin at first point of spline",
244 refresh
: BoolProperty(
246 description
="Refresh spline",
249 auto_refresh
: BoolProperty(
251 description
="Auto refresh spline",
255 def draw(self
, context
):
257 col
= layout
.column(align
=True)
258 row
= col
.row(align
=True)
260 if self
.auto_refresh
is False:
262 elif self
.auto_refresh
is True:
265 row
.prop(self
, "auto_refresh", toggle
=True, icon
="AUTO", icon_only
=True)
266 row
.prop(self
, "refresh", toggle
=True, icon
="FILE_REFRESH", icon_only
=True)
267 row
.operator("object.add_spirofit_spline", text
="Add")
268 row
.prop(self
, "origin_to_start", toggle
=True, icon
="CURVE_DATA", icon_only
=True)
270 col
= layout
.column(align
=True)
271 col
.prop(self
, "spline_name")
273 col
.prop(self
, "map_method")
275 col
.prop(self
, "spire_resolution")
276 row
= col
.row(align
=True).split(factor
=0.9, align
=True)
277 row
.prop(self
, "spires")
278 row
.prop(self
, "direction", toggle
=True, text
="", icon
='ARROW_LEFTRIGHT')
279 col
.prop(self
, "offset")
280 col
.prop(self
, "waves")
281 col
.prop(self
, "wave_iscale")
282 col
.prop(self
, "rndm_spire")
283 col
.prop(self
, "random_seed")
284 draw_spline_settings(self
)
287 def poll(self
, context
):
288 ob
= context
.active_object
289 return ((ob
is not None) and
290 (context
.mode
== 'OBJECT'))
292 def invoke(self
, context
, event
):
294 return self
.execute(context
)
296 def execute(self
, context
):
298 return {'PASS_THROUGH'}
300 obj
= context
.active_object
301 if obj
.type != 'MESH':
302 self
.report({'WARNING'},
303 "Active Object is not a Mesh. Operation Cancelled")
306 bpy
.ops
.object.select_all(action
='DESELECT')
308 r
.seed(self
.random_seed
)
310 points
= spirofit_spline(
312 self
.spire_resolution
,
337 if self
.origin_to_start
is True:
338 move_origin_to_start()
340 if self
.auto_refresh
is False:
346 # ------------------------------------------------------------
347 # Bounce spline / Fiber mesh
348 # Original script by Liero and Atom
349 # https://blenderartists.org/forum/showthread.php?331750-Fiber-Mesh-Emulation
350 # ------------------------------------------------------------
352 rand
= Vector((r
.gauss(0, 1), r
.gauss(0, 1), r
.gauss(0, 1)))
353 vec
= rand
.normalized() * var
357 def bounce_spline(obj
,
365 dist
, points
= 1000, []
366 poly
= obj
.data
.polygons
372 print("No active face selected")
375 n
= r
.randint(0, len(poly
) - 1)
377 end
= poly
[n
].normal
.copy() * -1
378 start
= poly
[n
].center
379 points
.append(start
+ offset
* end
)
381 for i
in range(number
):
382 for ray
in range(extra
+ 1):
383 end
+= noise(ang_noise
)
385 hit
, nor
, index
= obj
.ray_cast(start
, end
* dist
)[-3:]
389 start
= hit
- nor
/ 10000
390 end
= end
.reflect(nor
).normalized()
391 points
.append(hit
+ offset
* nor
)
398 class BounceSpline(Operator
):
399 bl_idname
= "object.add_bounce_spline"
400 bl_label
= "Bounce Spline"
401 bl_description
= "Fill selected mesh with a spline"
402 bl_options
= {'REGISTER', 'UNDO', 'PRESET'}
404 bounce_number
: IntProperty(
410 description
="Number of bounces"
412 ang_noise
: FloatProperty(
413 name
="Angular Noise",
417 description
="Add some noise to ray direction"
419 offset
: FloatProperty(
423 description
="Use normal direction to offset spline"
430 description
="Number of extra tries if it fails to hit mesh"
432 active_face
: BoolProperty(
435 description
="Starts from active face or a random one"
437 spline_name
: StringProperty(
439 default
="BounceSpline"
441 spline_type
: EnumProperty(
444 description
="Spline type",
445 items
=[('POLY', "Poly", "Poly spline"),
446 ('BEZIER', "Bezier", "Bezier spline")]
448 resolution_u
: IntProperty(
453 description
="Curve resolution u"
455 bevel
: FloatProperty(
460 description
="Bevel depth"
462 bevel_res
: IntProperty(
463 name
="Bevel Resolution",
467 description
="Bevel resolution"
469 extrude
: FloatProperty(
474 description
="Extrude amount"
476 twist_mode
: EnumProperty(
479 description
="Twist method, type of tilt calculation",
480 items
=[('Z_UP', "Z-Up", 'Z Up'),
481 ('MINIMUM', "Minimum", 'Minimum'),
482 ('TANGENT', "Tangent", 'Tangent')]
484 twist_smooth
: FloatProperty(
489 description
="Twist smoothing amount for tangents"
491 tilt
: FloatProperty(
495 description
="Spline handle tilt"
497 random_radius
: FloatProperty(
502 description
="Randomise radius of spline controlpoints"
504 random_seed
: IntProperty(
508 description
="Random seed number"
510 origin_to_start
: BoolProperty(
511 name
="Origin at Start",
512 description
="Set origin at first point of spline",
515 refresh
: BoolProperty(
517 description
="Refresh spline",
520 auto_refresh
: BoolProperty(
522 description
="Auto refresh spline",
526 def draw(self
, context
):
528 col
= layout
.column(align
=True)
529 row
= col
.row(align
=True)
530 if self
.auto_refresh
is False:
532 elif self
.auto_refresh
is True:
535 row
.prop(self
, "auto_refresh", toggle
=True, icon
="AUTO", icon_only
=True)
536 row
.prop(self
, "refresh", toggle
=True, icon
="FILE_REFRESH", icon_only
=True)
537 row
.operator("object.add_bounce_spline", text
="Add")
538 row
.prop(self
, "origin_to_start", toggle
=True, icon
="CURVE_DATA", icon_only
=True)
540 col
= layout
.column(align
=True)
541 col
.prop(self
, "spline_name")
543 col
.prop(self
, "bounce_number")
544 row
= col
.row(align
=True).split(factor
=0.9, align
=True)
545 row
.prop(self
, "ang_noise")
546 row
.prop(self
, "active_face", toggle
=True, text
="", icon
="SNAP_FACE")
547 col
.prop(self
, "offset")
548 col
.prop(self
, "extra")
549 col
.prop(self
, "random_seed")
550 draw_spline_settings(self
)
553 def poll(self
, context
):
554 ob
= context
.active_object
555 return ((ob
is not None) and
556 (context
.mode
== 'OBJECT'))
558 def invoke(self
, context
, event
):
560 return self
.execute(context
)
562 def execute(self
, context
):
564 return {'PASS_THROUGH'}
566 obj
= context
.active_object
567 if obj
.type != 'MESH':
570 bpy
.ops
.object.select_all(action
='DESELECT')
572 r
.seed(self
.random_seed
)
574 points
= bounce_spline(
598 if self
.origin_to_start
is True:
599 move_origin_to_start()
601 if self
.auto_refresh
is False:
607 # ------------------------------------------------------------
608 # Hang Catenary curve between two selected objects
609 # ------------------------------------------------------------
618 lx
= end
[0] - start
[0]
619 ly
= end
[1] - start
[1]
620 lr
= sqrt(pow(lx
, 2) + pow(ly
, 2))
621 lv
= lr
/ 2 - (end
[2] - start
[2]) * a
/ lr
622 zv
= start
[2] - pow(lv
, 2) / (2 * a
)
628 x
= start
[0] + i
* slx
629 y
= start
[1] + i
* sly
630 z
= zv
+ pow((i
* slr
) - lv
, 2) / (2 * a
)
631 points
.append([x
, y
, z
])
636 class CatenaryCurve(Operator
):
637 bl_idname
= "object.add_catenary_curve"
638 bl_label
= "Catenary"
639 bl_description
= "Hang a curve between two selected objects"
640 bl_options
= {'REGISTER', 'UNDO', 'PRESET'}
644 description
="Resolution of the curve",
649 var_a
: FloatProperty(
651 description
="Catenary variable a",
657 spline_name
: StringProperty(
661 spline_type
: EnumProperty(
664 description
="Spline type",
665 items
=[('POLY', "Poly", "Poly spline"),
666 ('BEZIER', "Bezier", "Bezier spline")]
668 resolution_u
: IntProperty(
673 description
="Curve resolution u"
675 bevel
: FloatProperty(
680 description
="Bevel depth"
682 bevel_res
: IntProperty(
683 name
="Bevel Resolution",
687 description
="Bevel resolution"
689 extrude
: FloatProperty(
694 description
="Extrude amount"
696 twist_mode
: EnumProperty(
699 description
="Twist method, type of tilt calculation",
700 items
=[('Z_UP', "Z-Up", 'Z Up'),
701 ('MINIMUM', "Minimum", "Minimum"),
702 ('TANGENT', "Tangent", "Tangent")]
704 twist_smooth
: FloatProperty(
709 description
="Twist smoothing amount for tangents"
711 tilt
: FloatProperty(
715 description
="Spline handle tilt"
717 random_radius
: FloatProperty(
722 description
="Randomise radius of spline controlpoints"
724 random_seed
: IntProperty(
728 description
="Random seed number"
730 origin_to_start
: BoolProperty(
731 name
="Origin at Start",
732 description
="Set origin at first point of spline",
735 refresh
: BoolProperty(
737 description
="Refresh spline",
740 auto_refresh
: BoolProperty(
742 description
="Auto refresh spline",
746 def draw(self
, context
):
748 col
= layout
.column(align
=True)
749 row
= col
.row(align
=True)
751 if self
.auto_refresh
is False:
753 elif self
.auto_refresh
is True:
756 row
.prop(self
, "auto_refresh", toggle
=True, icon
="AUTO", icon_only
=True)
757 row
.prop(self
, "refresh", toggle
=True, icon
="FILE_REFRESH", icon_only
=True)
758 row
.operator("object.add_catenary_curve", text
="Add")
759 row
.prop(self
, "origin_to_start", toggle
=True, icon
="CURVE_DATA", icon_only
=True)
761 col
= layout
.column(align
=True)
762 col
.prop(self
, "spline_name")
764 col
.prop(self
, "steps")
765 col
.prop(self
, "var_a")
767 draw_spline_settings(self
)
768 col
= layout
.column(align
=True)
769 col
.prop(self
, "random_seed")
772 def poll(self
, context
):
773 ob
= context
.active_object
774 return ob
is not None
776 def invoke(self
, context
, event
):
778 return self
.execute(context
)
780 def execute(self
, context
):
782 return {'PASS_THROUGH'}
785 #ob1 = bpy.context.active_object
787 ob1
= bpy
.context
.selected_objects
[0]
788 ob2
= bpy
.context
.selected_objects
[1]
792 if (start
[0] == end
[0]) and (start
[1] == end
[1]):
793 self
.report({"WARNING"},
794 "Objects have the same X, Y location. Operation Cancelled")
798 self
.report({"WARNING"},
799 "Catenary could not be completed. Operation Cancelled")
802 bpy
.ops
.object.select_all(action
='DESELECT')
804 r
.seed(self
.random_seed
)
806 points
= catenary_curve(
827 if self
.origin_to_start
is True:
828 move_origin_to_start()
830 bpy
.ops
.object.origin_set(type='ORIGIN_GEOMETRY')
832 if self
.auto_refresh
is False:
838 # ------------------------------------------------------------
839 # Generate curve object from given points
840 # ------------------------------------------------------------
841 def add_curve_object(
844 spline_name
="Spline",
845 spline_type
='BEZIER',
851 twist_mode
='MINIMUM',
856 scene
= bpy
.context
.scene
857 vl
= bpy
.context
.view_layer
858 curve
= bpy
.data
.curves
.new(spline_name
, 'CURVE')
859 curve
.dimensions
= '3D'
860 spline
= curve
.splines
.new(spline_type
)
861 cur
= bpy
.data
.objects
.new(spline_name
, curve
)
862 spline
.radius_interpolation
= 'BSPLINE'
863 spline
.tilt_interpolation
= 'BSPLINE'
865 if spline_type
== 'BEZIER':
866 spline
.bezier_points
.add(int(len(verts
) - 1))
867 for i
in range(len(verts
)):
868 spline
.bezier_points
[i
].co
= verts
[i
]
869 spline
.bezier_points
[i
].handle_right_type
= 'AUTO'
870 spline
.bezier_points
[i
].handle_left_type
= 'AUTO'
871 spline
.bezier_points
[i
].radius
+= spline_radius
* r
.random()
872 spline
.bezier_points
[i
].tilt
= radians(tilt
)
874 spline
.points
.add(int(len(verts
) - 1))
875 for i
in range(len(verts
)):
876 spline
.points
[i
].co
= verts
[i
][0], verts
[i
][1], verts
[i
][2], 1
878 scene
.collection
.objects
.link(cur
)
879 cur
.data
.resolution_u
= resolution_u
880 cur
.data
.fill_mode
= 'FULL'
881 cur
.data
.bevel_depth
= bevel
882 cur
.data
.bevel_resolution
= bevel_resolution
883 cur
.data
.extrude
= extrude
884 cur
.data
.twist_mode
= twist_mode
885 cur
.data
.twist_smooth
= twist_smooth
886 cur
.matrix_world
= matrix
888 vl
.objects
.active
= cur
892 def move_origin_to_start():
893 active
= bpy
.context
.active_object
894 spline
= active
.data
.splines
[0]
896 if spline
.type == 'BEZIER':
897 start
= active
.matrix_world
@ spline
.bezier_points
[0].co
899 start
= active
.matrix_world
@ spline
.points
[0].co
902 cursor
= bpy
.context
.scene
.cursor
.location
.copy()
903 bpy
.context
.scene
.cursor
.location
= start
904 bpy
.ops
.object.origin_set(type='ORIGIN_CURSOR')
905 bpy
.context
.scene
.cursor
.location
= cursor
908 def draw_spline_settings(self
):
910 col
= layout
.column(align
=True)
912 col
.prop(self
, "spline_type")
914 col
.prop(self
, "resolution_u")
915 col
.prop(self
, "bevel")
916 col
.prop(self
, "bevel_res")
917 col
.prop(self
, "extrude")
919 if self
.spline_type
== 'BEZIER':
920 col
.prop(self
, "random_radius")
922 col
.prop(self
, "twist_mode")
925 if self
.twist_mode
== 'TANGENT':
926 col
.prop(self
, "twist_smooth")
928 if self
.spline_type
== 'BEZIER':
929 col
.prop(self
, "tilt")
932 # ------------------------------------------------------------
934 # ------------------------------------------------------------
936 bpy
.utils
.register_class(SpiroFitSpline
)
937 bpy
.utils
.register_class(BounceSpline
)
938 bpy
.utils
.register_class(CatenaryCurve
)
942 bpy
.utils
.unregister_class(SpiroFitSpline
)
943 bpy
.utils
.unregister_class(BounceSpline
)
944 bpy
.utils
.unregister_class(CatenaryCurve
)
947 if __name__
== "__main__":