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 #####
21 "name": "SpiroFit, BounceSpline and Catenary",
22 "author": "Antonio Osprite, Liero, Atom, Jimmy Hazevoet",
24 "blender": (2, 80, 0),
25 "location": "Sidebar > Create Tab",
26 "description": "SpiroFit, BounceSpline and Catenary adds "
27 "splines to selected mesh or objects",
34 from bpy
.types
import (
38 from bpy
.props
import (
45 from mathutils
import (
57 # ------------------------------------------------------------
58 # "Build a spiral that fit the active object"
59 # Spirofit, original blender 2.45 script by: Antonio Osprite
60 # http://www.kino3d.com/forum/viewtopic.php?t=5374
61 # ------------------------------------------------------------
63 d
= (Vector(v1
) - Vector(v2
)).length
67 def spiral_point(step
, radius
, z_coord
, spires
, waves
, wave_iscale
, rndm
):
68 x
= radius
* cos(spires
* step
) + (r
.random() - 0.5) * rndm
69 y
= radius
* sin(spires
* step
) + (r
.random() - 0.5) * rndm
70 z
= z_coord
+ (cos(waves
* step
* pi
) * wave_iscale
) + (r
.random() - 0.5) * rndm
74 def spirofit_spline(obj
,
87 bb_xmin
= min([v
[0] for v
in bb
])
88 bb_ymin
= min([v
[1] for v
in bb
])
89 bb_zmin
= min([v
[2] for v
in bb
])
90 bb_xmax
= max([v
[0] for v
in bb
])
91 bb_ymax
= max([v
[1] for v
in bb
])
92 bb_zmax
= max([v
[2] for v
in bb
])
94 radius
= distance([bb_xmax
, bb_ymax
, bb_zmin
], [bb_xmin
, bb_ymin
, bb_zmin
]) / 2.0
95 height
= bb_zmax
- bb_zmin
96 cx
= (bb_xmax
+ bb_xmin
) / 2.0
97 cy
= (bb_ymax
+ bb_ymin
) / 2.0
98 steps
= spires
* spire_resolution
100 for i
in range(steps
+ 1):
101 t
= bb_zmin
+ (2 * pi
/ steps
) * i
102 z
= bb_zmin
+ (float(height
) / steps
) * i
105 cp
= spiral_point(t
, radius
, z
, spires
, waves
, wave_iscale
, rndm_spire
)
107 if map_method
== 'RAYCAST':
108 success
, hit
, nor
, index
= obj
.ray_cast(Vector(cp
), (Vector([cx
, cy
, z
]) - Vector(cp
)))
110 points
.append((hit
+ offset
* nor
))
112 elif map_method
== 'CLOSESTPOINT':
113 success
, hit
, nor
, index
= obj
.closest_point_on_mesh(cp
)
115 points
.append((hit
+ offset
* nor
))
120 class SpiroFitSpline(Operator
):
121 bl_idname
= "object.add_spirofit_spline"
122 bl_label
= "SpiroFit"
123 bl_description
= "Wrap selected mesh in a spiral"
124 bl_options
= {'REGISTER', 'UNDO', 'PRESET'}
126 map_method
: EnumProperty(
129 description
="Mapping method",
130 items
=[('RAYCAST', 'Ray cast', 'Ray casting'),
131 ('CLOSESTPOINT', 'Closest point', 'Closest point on mesh')]
133 direction
: BoolProperty(
135 description
="Spire direction",
138 spire_resolution
: IntProperty(
139 name
="Spire Resolution",
144 description
="Number of steps for one turn"
146 spires
: IntProperty(
152 description
="Number of turns"
154 offset
: FloatProperty(
158 description
="Use normal direction to offset spline"
164 description
="Wave amount"
166 wave_iscale
: FloatProperty(
167 name
="Wave Intensity",
171 description
="Wave intensity scale"
173 rndm_spire
: FloatProperty(
178 description
="Randomise spire"
180 spline_name
: StringProperty(
184 spline_type
: EnumProperty(
187 description
="Spline type",
188 items
=[('POLY', 'Poly', 'Poly spline'),
189 ('BEZIER', 'Bezier', 'Bezier spline')]
191 resolution_u
: IntProperty(
196 description
="Curve resolution u"
198 bevel
: FloatProperty(
203 description
="Bevel depth"
205 bevel_res
: IntProperty(
206 name
="Bevel Resolution",
210 description
="Bevel resolution"
212 extrude
: FloatProperty(
217 description
="Extrude amount"
219 twist_mode
: EnumProperty(
222 description
="Twist method, type of tilt calculation",
223 items
=[('Z_UP', "Z-Up", 'Z Up'),
224 ('MINIMUM', "Minimum", 'Minimum'),
225 ('TANGENT', "Tangent", 'Tangent')]
227 twist_smooth
: FloatProperty(
232 description
="Twist smoothing amount for tangents"
234 tilt
: FloatProperty(
238 description
="Spline handle tilt"
240 random_radius
: FloatProperty(
245 description
="Randomise radius of spline controlpoints"
247 random_seed
: IntProperty(
251 description
="Random seed number"
253 origin_to_start
: BoolProperty(
254 name
="Origin at Start",
255 description
="Set origin at first point of spline",
258 refresh
: BoolProperty(
260 description
="Refresh spline",
263 auto_refresh
: BoolProperty(
265 description
="Auto refresh spline",
269 def draw(self
, context
):
271 col
= layout
.column(align
=True)
272 row
= col
.row(align
=True)
274 if self
.auto_refresh
is False:
276 elif self
.auto_refresh
is True:
279 row
.prop(self
, "auto_refresh", toggle
=True, icon
="AUTO", icon_only
=True)
280 row
.prop(self
, "refresh", toggle
=True, icon
="FILE_REFRESH", icon_only
=True)
281 row
.operator("object.add_spirofit_spline", text
="Add")
282 row
.prop(self
, "origin_to_start", toggle
=True, icon
="CURVE_DATA", icon_only
=True)
284 col
= layout
.column(align
=True)
285 col
.prop(self
, "spline_name")
287 col
.prop(self
, "map_method")
289 col
.prop(self
, "spire_resolution")
290 row
= col
.row(align
=True).split(factor
=0.9, align
=True)
291 row
.prop(self
, "spires")
292 row
.prop(self
, "direction", toggle
=True, text
="", icon
='ARROW_LEFTRIGHT')
293 col
.prop(self
, "offset")
294 col
.prop(self
, "waves")
295 col
.prop(self
, "wave_iscale")
296 col
.prop(self
, "rndm_spire")
297 col
.prop(self
, "random_seed")
298 draw_spline_settings(self
)
301 def poll(self
, context
):
302 ob
= context
.active_object
303 return ((ob
is not None) and
304 (context
.mode
== 'OBJECT'))
306 def invoke(self
, context
, event
):
308 return self
.execute(context
)
310 def execute(self
, context
):
312 return {'PASS_THROUGH'}
314 obj
= context
.active_object
315 if obj
.type != 'MESH':
316 self
.report({'WARNING'},
317 "Active Object is not a Mesh. Operation Cancelled")
320 bpy
.ops
.object.select_all(action
='DESELECT')
322 r
.seed(self
.random_seed
)
324 points
= spirofit_spline(
326 self
.spire_resolution
,
351 if self
.origin_to_start
is True:
352 move_origin_to_start()
354 if self
.auto_refresh
is False:
360 # ------------------------------------------------------------
361 # Bounce spline / Fiber mesh
362 # Original script by Liero and Atom
363 # https://blenderartists.org/forum/showthread.php?331750-Fiber-Mesh-Emulation
364 # ------------------------------------------------------------
366 rand
= Vector((r
.gauss(0, 1), r
.gauss(0, 1), r
.gauss(0, 1)))
367 vec
= rand
.normalized() * var
371 def bounce_spline(obj
,
379 dist
, points
= 1000, []
380 poly
= obj
.data
.polygons
386 print("No active face selected")
389 n
= r
.randint(0, len(poly
) - 1)
391 end
= poly
[n
].normal
.copy() * -1
392 start
= poly
[n
].center
393 points
.append(start
+ offset
* end
)
395 for i
in range(number
):
396 for ray
in range(extra
+ 1):
397 end
+= noise(ang_noise
)
399 hit
, nor
, index
= obj
.ray_cast(start
, end
* dist
)[-3:]
403 start
= hit
- nor
/ 10000
404 end
= end
.reflect(nor
).normalized()
405 points
.append(hit
+ offset
* nor
)
412 class BounceSpline(Operator
):
413 bl_idname
= "object.add_bounce_spline"
414 bl_label
= "Bounce Spline"
415 bl_description
= "Fill selected mesh with a spline"
416 bl_options
= {'REGISTER', 'UNDO', 'PRESET'}
418 bounce_number
: IntProperty(
424 description
="Number of bounces"
426 ang_noise
: FloatProperty(
427 name
="Angular Noise",
431 description
="Add some noise to ray direction"
433 offset
: FloatProperty(
437 description
="Use normal direction to offset spline"
444 description
="Number of extra tries if it fails to hit mesh"
446 active_face
: BoolProperty(
449 description
="Starts from active face or a random one"
451 spline_name
: StringProperty(
453 default
="BounceSpline"
455 spline_type
: EnumProperty(
458 description
="Spline type",
459 items
=[('POLY', "Poly", "Poly spline"),
460 ('BEZIER', "Bezier", "Bezier spline")]
462 resolution_u
: IntProperty(
467 description
="Curve resolution u"
469 bevel
: FloatProperty(
474 description
="Bevel depth"
476 bevel_res
: IntProperty(
477 name
="Bevel Resolution",
481 description
="Bevel resolution"
483 extrude
: FloatProperty(
488 description
="Extrude amount"
490 twist_mode
: EnumProperty(
493 description
="Twist method, type of tilt calculation",
494 items
=[('Z_UP', "Z-Up", 'Z Up'),
495 ('MINIMUM', "Minimum", 'Minimum'),
496 ('TANGENT', "Tangent", 'Tangent')]
498 twist_smooth
: FloatProperty(
503 description
="Twist smoothing amount for tangents"
505 tilt
: FloatProperty(
509 description
="Spline handle tilt"
511 random_radius
: FloatProperty(
516 description
="Randomise radius of spline controlpoints"
518 random_seed
: IntProperty(
522 description
="Random seed number"
524 origin_to_start
: BoolProperty(
525 name
="Origin at Start",
526 description
="Set origin at first point of spline",
529 refresh
: BoolProperty(
531 description
="Refresh spline",
534 auto_refresh
: BoolProperty(
536 description
="Auto refresh spline",
540 def draw(self
, context
):
542 col
= layout
.column(align
=True)
543 row
= col
.row(align
=True)
544 if self
.auto_refresh
is False:
546 elif self
.auto_refresh
is True:
549 row
.prop(self
, "auto_refresh", toggle
=True, icon
="AUTO", icon_only
=True)
550 row
.prop(self
, "refresh", toggle
=True, icon
="FILE_REFRESH", icon_only
=True)
551 row
.operator("object.add_bounce_spline", text
="Add")
552 row
.prop(self
, "origin_to_start", toggle
=True, icon
="CURVE_DATA", icon_only
=True)
554 col
= layout
.column(align
=True)
555 col
.prop(self
, "spline_name")
557 col
.prop(self
, "bounce_number")
558 row
= col
.row(align
=True).split(factor
=0.9, align
=True)
559 row
.prop(self
, "ang_noise")
560 row
.prop(self
, "active_face", toggle
=True, text
="", icon
="SNAP_FACE")
561 col
.prop(self
, "offset")
562 col
.prop(self
, "extra")
563 col
.prop(self
, "random_seed")
564 draw_spline_settings(self
)
567 def poll(self
, context
):
568 ob
= context
.active_object
569 return ((ob
is not None) and
570 (context
.mode
== 'OBJECT'))
572 def invoke(self
, context
, event
):
574 return self
.execute(context
)
576 def execute(self
, context
):
578 return {'PASS_THROUGH'}
580 obj
= context
.active_object
581 if obj
.type != 'MESH':
584 bpy
.ops
.object.select_all(action
='DESELECT')
586 r
.seed(self
.random_seed
)
588 points
= bounce_spline(
612 if self
.origin_to_start
is True:
613 move_origin_to_start()
615 if self
.auto_refresh
is False:
621 # ------------------------------------------------------------
622 # Hang Catenary curve between two selected objects
623 # ------------------------------------------------------------
632 lx
= end
[0] - start
[0]
633 ly
= end
[1] - start
[1]
634 lr
= sqrt(pow(lx
, 2) + pow(ly
, 2))
635 lv
= lr
/ 2 - (end
[2] - start
[2]) * a
/ lr
636 zv
= start
[2] - pow(lv
, 2) / (2 * a
)
642 x
= start
[0] + i
* slx
643 y
= start
[1] + i
* sly
644 z
= zv
+ pow((i
* slr
) - lv
, 2) / (2 * a
)
645 points
.append([x
, y
, z
])
650 class CatenaryCurve(Operator
):
651 bl_idname
= "object.add_catenary_curve"
652 bl_label
= "Catenary"
653 bl_description
= "Hang a curve between two selected objects"
654 bl_options
= {'REGISTER', 'UNDO', 'PRESET'}
658 description
="Resolution of the curve",
663 var_a
: FloatProperty(
665 description
="Catenary variable a",
671 spline_name
: StringProperty(
675 spline_type
: EnumProperty(
678 description
="Spline type",
679 items
=[('POLY', "Poly", "Poly spline"),
680 ('BEZIER', "Bezier", "Bezier spline")]
682 resolution_u
: IntProperty(
687 description
="Curve resolution u"
689 bevel
: FloatProperty(
694 description
="Bevel depth"
696 bevel_res
: IntProperty(
697 name
="Bevel Resolution",
701 description
="Bevel resolution"
703 extrude
: FloatProperty(
708 description
="Extrude amount"
710 twist_mode
: EnumProperty(
713 description
="Twist method, type of tilt calculation",
714 items
=[('Z_UP', "Z-Up", 'Z Up'),
715 ('MINIMUM', "Minimum", "Minimum"),
716 ('TANGENT', "Tangent", "Tangent")]
718 twist_smooth
: FloatProperty(
723 description
="Twist smoothing amount for tangents"
725 tilt
: FloatProperty(
729 description
="Spline handle tilt"
731 random_radius
: FloatProperty(
736 description
="Randomise radius of spline controlpoints"
738 random_seed
: IntProperty(
742 description
="Random seed number"
744 origin_to_start
: BoolProperty(
745 name
="Origin at Start",
746 description
="Set origin at first point of spline",
749 refresh
: BoolProperty(
751 description
="Refresh spline",
754 auto_refresh
: BoolProperty(
756 description
="Auto refresh spline",
760 def draw(self
, context
):
762 col
= layout
.column(align
=True)
763 row
= col
.row(align
=True)
765 if self
.auto_refresh
is False:
767 elif self
.auto_refresh
is True:
770 row
.prop(self
, "auto_refresh", toggle
=True, icon
="AUTO", icon_only
=True)
771 row
.prop(self
, "refresh", toggle
=True, icon
="FILE_REFRESH", icon_only
=True)
772 row
.operator("object.add_catenary_curve", text
="Add")
773 row
.prop(self
, "origin_to_start", toggle
=True, icon
="CURVE_DATA", icon_only
=True)
775 col
= layout
.column(align
=True)
776 col
.prop(self
, "spline_name")
778 col
.prop(self
, "steps")
779 col
.prop(self
, "var_a")
781 draw_spline_settings(self
)
782 col
= layout
.column(align
=True)
783 col
.prop(self
, "random_seed")
786 def poll(self
, context
):
787 ob
= context
.active_object
788 return ob
is not None
790 def invoke(self
, context
, event
):
792 return self
.execute(context
)
794 def execute(self
, context
):
796 return {'PASS_THROUGH'}
799 #ob1 = bpy.context.active_object
801 ob1
= bpy
.context
.selected_objects
[0]
802 ob2
= bpy
.context
.selected_objects
[1]
806 if (start
[0] == end
[0]) and (start
[1] == end
[1]):
807 self
.report({"WARNING"},
808 "Objects have the same X, Y location. Operation Cancelled")
812 self
.report({"WARNING"},
813 "Catenary could not be completed. Operation Cancelled")
816 bpy
.ops
.object.select_all(action
='DESELECT')
818 r
.seed(self
.random_seed
)
820 points
= catenary_curve(
841 if self
.origin_to_start
is True:
842 move_origin_to_start()
844 bpy
.ops
.object.origin_set(type='ORIGIN_GEOMETRY')
846 if self
.auto_refresh
is False:
852 # ------------------------------------------------------------
853 # Generate curve object from given points
854 # ------------------------------------------------------------
855 def add_curve_object(
858 spline_name
="Spline",
859 spline_type
='BEZIER',
865 twist_mode
='MINIMUM',
870 scene
= bpy
.context
.scene
871 vl
= bpy
.context
.view_layer
872 curve
= bpy
.data
.curves
.new(spline_name
, 'CURVE')
873 curve
.dimensions
= '3D'
874 spline
= curve
.splines
.new(spline_type
)
875 cur
= bpy
.data
.objects
.new(spline_name
, curve
)
876 spline
.radius_interpolation
= 'BSPLINE'
877 spline
.tilt_interpolation
= 'BSPLINE'
879 if spline_type
== 'BEZIER':
880 spline
.bezier_points
.add(int(len(verts
) - 1))
881 for i
in range(len(verts
)):
882 spline
.bezier_points
[i
].co
= verts
[i
]
883 spline
.bezier_points
[i
].handle_right_type
= 'AUTO'
884 spline
.bezier_points
[i
].handle_left_type
= 'AUTO'
885 spline
.bezier_points
[i
].radius
+= spline_radius
* r
.random()
886 spline
.bezier_points
[i
].tilt
= radians(tilt
)
888 spline
.points
.add(int(len(verts
) - 1))
889 for i
in range(len(verts
)):
890 spline
.points
[i
].co
= verts
[i
][0], verts
[i
][1], verts
[i
][2], 1
892 scene
.collection
.objects
.link(cur
)
893 cur
.data
.resolution_u
= resolution_u
894 cur
.data
.fill_mode
= 'FULL'
895 cur
.data
.bevel_depth
= bevel
896 cur
.data
.bevel_resolution
= bevel_resolution
897 cur
.data
.extrude
= extrude
898 cur
.data
.twist_mode
= twist_mode
899 cur
.data
.twist_smooth
= twist_smooth
900 cur
.matrix_world
= matrix
902 vl
.objects
.active
= cur
906 def move_origin_to_start():
907 active
= bpy
.context
.active_object
908 spline
= active
.data
.splines
[0]
910 if spline
.type == 'BEZIER':
911 start
= active
.matrix_world
@ spline
.bezier_points
[0].co
913 start
= active
.matrix_world
@ spline
.points
[0].co
916 cursor
= bpy
.context
.scene
.cursor
.location
.copy()
917 bpy
.context
.scene
.cursor
.location
= start
918 bpy
.ops
.object.origin_set(type='ORIGIN_CURSOR')
919 bpy
.context
.scene
.cursor
.location
= cursor
922 def draw_spline_settings(self
):
924 col
= layout
.column(align
=True)
926 col
.prop(self
, "spline_type")
928 col
.prop(self
, "resolution_u")
929 col
.prop(self
, "bevel")
930 col
.prop(self
, "bevel_res")
931 col
.prop(self
, "extrude")
933 if self
.spline_type
== 'BEZIER':
934 col
.prop(self
, "random_radius")
936 col
.prop(self
, "twist_mode")
939 if self
.twist_mode
== 'TANGENT':
940 col
.prop(self
, "twist_smooth")
942 if self
.spline_type
== 'BEZIER':
943 col
.prop(self
, "tilt")
946 # ------------------------------------------------------------
948 # ------------------------------------------------------------
950 bpy
.utils
.register_class(SpiroFitSpline
)
951 bpy
.utils
.register_class(BounceSpline
)
952 bpy
.utils
.register_class(CatenaryCurve
)
956 bpy
.utils
.unregister_class(SpiroFitSpline
)
957 bpy
.utils
.unregister_class(BounceSpline
)
958 bpy
.utils
.unregister_class(CatenaryCurve
)
961 if __name__
== "__main__":