1 # SPDX-License-Identifier: GPL-2.0-or-later
6 "description": "Make spirals",
7 "author": "Alejandro Omar Chocano Vasquez",
10 "location": "View3D > Add > Curve",
12 "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/extra_objects.html",
13 "category": "Add Curve",
19 from bpy
.props
import (
26 from mathutils
import (
33 from bpy_extras
.object_utils
import object_data_add
34 from bpy
.types
import (
38 from bl_operators
.presets
import AddPresetBase
42 # ----------------------------------------------------------------------------
44 def make_spiral(props
, context
):
45 # archemedian and logarithmic can be plotted in cylindrical coordinates
47 # INPUT: turns->degree->max_phi, steps, direction
48 # Initialise Polar Coordinate Environment
49 props
.degree
= 360 * props
.turns
# If you want to make the slider for degree
50 steps
= props
.steps
* props
.turns
# props.steps[per turn] -> steps[for the whole spiral]
51 props
.z_scale
= props
.dif_z
* props
.turns
53 max_phi
= pi
* props
.degree
/ 180 # max angle in radian
54 step_phi
= max_phi
/ steps
# angle in radians between two vertices
56 if props
.spiral_direction
== 'CLOCKWISE':
57 step_phi
*= -1 # flip direction
60 step_z
= props
.z_scale
/ (steps
- 1) # z increase in one step
63 verts
.append([props
.radius
, 0, 0])
68 # Archemedean: dif_radius, radius
69 cur_rad
= props
.radius
70 step_rad
= props
.dif_radius
/ (steps
* 360 / props
.degree
)
71 # radius increase per angle for archemedean spiral|
72 # (steps * 360/props.degree)...Steps needed for 360 deg
73 # Logarithmic: radius, B_force, ang_div, dif_z
75 while abs(cur_phi
) <= abs(max_phi
):
79 if props
.spiral_type
== 'ARCH':
81 if props
.spiral_type
== 'LOG':
82 # r = a*e^{|theta| * b}
83 cur_rad
= props
.radius
* pow(props
.B_force
, abs(cur_phi
))
85 px
= cur_rad
* cos(cur_phi
)
86 py
= cur_rad
* sin(cur_phi
)
87 verts
.append([px
, py
, cur_z
])
93 # ----------------------------------------------------------------------------
95 def make_spiral_spheric(props
, context
):
96 # INPUT: turns, steps[per turn], radius
97 # use spherical Coordinates
98 step_phi
= (2 * pi
) / props
.steps
# Step of angle in radians for one turn
99 steps
= props
.steps
* props
.turns
# props.steps[per turn] -> steps[for the whole spiral]
101 max_phi
= 2 * pi
* props
.turns
# max angle in radian
102 step_phi
= max_phi
/ steps
# angle in radians between two vertices
103 if props
.spiral_direction
== 'CLOCKWISE': # flip direction
106 step_theta
= pi
/ (steps
- 1) # theta increase in one step (pi == 180 deg)
109 verts
.append([0, 0, -props
.radius
]) # First vertex at south pole
112 cur_theta
= -pi
/ 2 # Beginning at south pole
114 while abs(cur_phi
) <= abs(max_phi
):
115 # Coordinate Transformation sphere->rect
116 px
= props
.radius
* cos(cur_theta
) * cos(cur_phi
)
117 py
= props
.radius
* cos(cur_theta
) * sin(cur_phi
)
118 pz
= props
.radius
* sin(cur_theta
)
120 verts
.append([px
, py
, pz
])
121 cur_theta
+= step_theta
128 # ----------------------------------------------------------------------------
130 def make_spiral_torus(props
, context
):
131 # INPUT: turns, steps, inner_radius, curves_number,
132 # mul_height, dif_inner_radius, cycles
133 max_phi
= 2 * pi
* props
.turns
* props
.cycles
# max angle in radian
134 step_phi
= 2 * pi
/ props
.steps
# Step of angle in radians between two vertices
136 if props
.spiral_direction
== 'CLOCKWISE': # flip direction
140 step_theta
= (2 * pi
/ props
.turns
) / props
.steps
141 step_rad
= props
.dif_radius
/ (props
.steps
* props
.turns
)
142 step_inner_rad
= props
.dif_inner_radius
/ props
.steps
143 step_z
= props
.dif_z
/ (props
.steps
* props
.turns
)
147 cur_phi
= 0 # Inner Ring Radius Angle
148 cur_theta
= 0 # Ring Radius Angle
149 cur_rad
= props
.radius
150 cur_inner_rad
= props
.inner_radius
154 while abs(cur_phi
) <= abs(max_phi
):
155 # Torus Coordinates -> Rect
156 px
= (cur_rad
+ cur_inner_rad
* cos(cur_phi
)) * \
157 cos(props
.curves_number
* cur_theta
)
158 py
= (cur_rad
+ cur_inner_rad
* cos(cur_phi
)) * \
159 sin(props
.curves_number
* cur_theta
)
160 pz
= cur_inner_rad
* sin(cur_phi
) + cur_z
162 verts
.append([px
, py
, pz
])
164 if props
.touch
and cur_phi
>= n_cycle
* 2 * pi
:
165 step_z
= ((n_cycle
+ 1) * props
.dif_inner_radius
+
166 props
.inner_radius
) * 2 / (props
.steps
* props
.turns
)
169 cur_theta
+= step_theta
172 cur_inner_rad
+= step_inner_rad
177 # ------------------------------------------------------------
178 # calculates the matrix for the new object
179 # depending on user pref
181 def align_matrix(context
, location
):
182 loc
= Matrix
.Translation(location
)
183 obj_align
= context
.preferences
.edit
.object_align
184 if (context
.space_data
.type == 'VIEW_3D' and
185 obj_align
== 'VIEW'):
186 rot
= context
.space_data
.region_3d
.view_matrix
.to_3x3().inverted().to_4x4()
189 align_matrix
= loc
@ rot
193 # ------------------------------------------------------------
194 # get array of vertcoordinates according to splinetype
195 def vertsToPoints(Verts
, splineType
):
200 # array for BEZIER spline output (V3)
201 if splineType
== 'BEZIER':
205 # array for nonBEZIER output (V4)
209 if splineType
== 'NURBS':
217 def draw_curve(props
, context
, align_matrix
):
218 # output splineType 'POLY' 'NURBS' 'BEZIER'
219 splineType
= props
.curve_type
221 if props
.spiral_type
== 'ARCH':
222 verts
= make_spiral(props
, context
)
223 if props
.spiral_type
== 'LOG':
224 verts
= make_spiral(props
, context
)
225 if props
.spiral_type
== 'SPHERE':
226 verts
= make_spiral_spheric(props
, context
)
227 if props
.spiral_type
== 'TORUS':
228 verts
= make_spiral_torus(props
, context
)
231 if bpy
.context
.mode
== 'EDIT_CURVE':
232 Curve
= context
.active_object
233 newSpline
= Curve
.data
.splines
.new(type=splineType
) # spline
236 dataCurve
= bpy
.data
.curves
.new(name
='Spiral', type='CURVE') # curvedatablock
237 newSpline
= dataCurve
.splines
.new(type=splineType
) # spline
239 # create object with newCurve
240 Curve
= object_data_add(context
, dataCurve
) # place in active scene
241 Curve
.matrix_world
= align_matrix
# apply matrix
242 Curve
.rotation_euler
= props
.rotation_euler
243 Curve
.select_set(True)
245 # turn verts into array
246 vertArray
= vertsToPoints(verts
, splineType
)
248 for spline
in Curve
.data
.splines
:
249 if spline
.type == 'BEZIER':
250 for point
in spline
.bezier_points
:
251 point
.select_control_point
= False
252 point
.select_left_handle
= False
253 point
.select_right_handle
= False
255 for point
in spline
.points
:
258 # create newSpline from vertarray
259 if splineType
== 'BEZIER':
260 newSpline
.bezier_points
.add(int(len(vertArray
) * 0.33))
261 newSpline
.bezier_points
.foreach_set('co', vertArray
)
262 for point
in newSpline
.bezier_points
:
263 point
.handle_right_type
= props
.handleType
264 point
.handle_left_type
= props
.handleType
265 point
.select_control_point
= True
266 point
.select_left_handle
= True
267 point
.select_right_handle
= True
269 newSpline
.points
.add(int(len(vertArray
) * 0.25 - 1))
270 newSpline
.points
.foreach_set('co', vertArray
)
271 newSpline
.use_endpoint_u
= False
272 for point
in newSpline
.points
:
276 newSpline
.use_cyclic_u
= props
.use_cyclic_u
277 newSpline
.use_endpoint_u
= props
.endp_u
278 newSpline
.order_u
= props
.order_u
281 Curve
.data
.dimensions
= props
.shape
282 Curve
.data
.use_path
= True
283 if props
.shape
== '3D':
284 Curve
.data
.fill_mode
= 'FULL'
286 Curve
.data
.fill_mode
= 'BOTH'
288 # move and rotate spline in edit mode
289 if bpy
.context
.mode
== 'EDIT_CURVE':
290 bpy
.ops
.transform
.translate(value
= props
.startlocation
)
291 bpy
.ops
.transform
.rotate(value
= props
.rotation_euler
[0], orient_axis
= 'X')
292 bpy
.ops
.transform
.rotate(value
= props
.rotation_euler
[1], orient_axis
= 'Y')
293 bpy
.ops
.transform
.rotate(value
= props
.rotation_euler
[2], orient_axis
= 'Z')
295 class CURVE_OT_spirals(Operator
):
296 bl_idname
= "curve.spirals"
297 bl_label
= "Curve Spirals"
298 bl_description
= "Create different types of spirals"
299 bl_options
= {'REGISTER', 'UNDO', 'PRESET'}
301 # align_matrix for the invoke
302 align_matrix
: Matrix()
304 spiral_type
: EnumProperty(
305 items
=[('ARCH', "Archemedian", "Archemedian"),
306 ("LOG", "Logarithmic", "Logarithmic"),
307 ("SPHERE", "Spheric", "Spheric"),
308 ("TORUS", "Torus", "Torus")],
311 description
="Type of spiral to add"
313 spiral_direction
: EnumProperty(
314 items
=[('COUNTER_CLOCKWISE', "Counter Clockwise",
315 "Wind in a counter clockwise direction"),
316 ("CLOCKWISE", "Clockwise",
317 "Wind in a clockwise direction")],
318 default
='COUNTER_CLOCKWISE',
319 name
="Spiral Direction",
320 description
="Direction of winding"
325 description
="Length of Spiral in 360 deg"
330 description
="Number of Vertices per turn"
332 radius
: FloatProperty(
334 min=0.00, max=100.00,
335 description
="Radius for first turn"
337 dif_z
: FloatProperty(
339 min=-10.00, max=100.00,
340 description
="Increase in Z axis per turn"
342 # needed for 1 and 2 spiral_type
343 # Archemedian variables
344 dif_radius
: FloatProperty(
346 min=-50.00, max=50.00,
347 description
="Radius increment in each turn"
349 # step between turns(one turn equals 360 deg)
351 B_force
: FloatProperty(
354 description
="Factor of exponent"
357 inner_radius
: FloatProperty(
360 description
="Inner Radius of Torus"
362 dif_inner_radius
: FloatProperty(
365 description
="Increase of inner Radius per Cycle"
367 dif_radius
: FloatProperty(
370 description
="Increase of Torus Radius per Cycle"
372 cycles
: FloatProperty(
375 description
="Number of Cycles"
377 curves_number
: IntProperty(
380 description
="Number of curves of spiral"
384 description
="No empty spaces between cycles"
388 ('2D', "2D", "2D shape Curve"),
389 ('3D', "3D", "3D shape Curve")]
390 shape
: EnumProperty(
393 description
="2D or 3D Curve",
396 curve_type
: EnumProperty(
397 name
="Output splines",
398 description
="Type of splines to output",
400 ('POLY', "Poly", "Poly Spline type"),
401 ('NURBS', "Nurbs", "Nurbs Spline type"),
402 ('BEZIER', "Bezier", "Bezier Spline type")],
405 use_cyclic_u
: BoolProperty(
408 description
="make curve closed"
410 endp_u
: BoolProperty(
411 name
="Use endpoint u",
413 description
="stretch to endpoints"
415 order_u
: IntProperty(
420 description
="Order of nurbs spline"
422 handleType
: EnumProperty(
425 description
="Bezier handles type",
427 ('VECTOR', "Vector", "Vector type Bezier handles"),
428 ('AUTO', "Auto", "Automatic type Bezier handles")]
430 edit_mode
: BoolProperty(
431 name
="Show in edit mode",
433 description
="Show in edit mode"
435 startlocation
: FloatVectorProperty(
437 description
="Start location",
438 default
=(0.0, 0.0, 0.0),
439 subtype
='TRANSLATION'
441 rotation_euler
: FloatVectorProperty(
443 description
="Rotation",
444 default
=(0.0, 0.0, 0.0),
448 def draw(self
, context
):
450 col
= layout
.column_flow(align
=True)
452 col
.label(text
="Presets:")
454 row
= col
.row(align
=True)
455 row
.menu("OBJECT_MT_spiral_curve_presets",
456 text
=bpy
.types
.OBJECT_MT_spiral_curve_presets
.bl_label
)
457 row
.operator("curve_extras.spiral_presets", text
=" + ")
458 op
= row
.operator("curve_extras.spiral_presets", text
=" - ")
459 op
.remove_active
= True
461 layout
.prop(self
, "spiral_type")
462 layout
.prop(self
, "spiral_direction")
464 col
= layout
.column(align
=True)
465 col
.label(text
="Spiral Parameters:")
466 col
.prop(self
, "turns", text
="Turns")
467 col
.prop(self
, "steps", text
="Steps")
470 if self
.spiral_type
== 'ARCH':
471 box
.label(text
="Archemedian Settings:")
472 col
= box
.column(align
=True)
473 col
.prop(self
, "dif_radius", text
="Radius Growth")
474 col
.prop(self
, "radius", text
="Radius")
475 col
.prop(self
, "dif_z", text
="Height")
477 if self
.spiral_type
== 'LOG':
478 box
.label(text
="Logarithmic Settings:")
479 col
= box
.column(align
=True)
480 col
.prop(self
, "radius", text
="Radius")
481 col
.prop(self
, "B_force", text
="Expansion Force")
482 col
.prop(self
, "dif_z", text
="Height")
484 if self
.spiral_type
== 'SPHERE':
485 box
.label(text
="Spheric Settings:")
486 box
.prop(self
, "radius", text
="Radius")
488 if self
.spiral_type
== 'TORUS':
489 box
.label(text
="Torus Settings:")
490 col
= box
.column(align
=True)
491 col
.prop(self
, "cycles", text
="Number of Cycles")
493 if self
.dif_inner_radius
== 0 and self
.dif_z
== 0:
495 col
.prop(self
, "radius", text
="Radius")
498 col
.prop(self
, "dif_z", text
="Height per Cycle")
501 col2
= box2
.column(align
=True)
502 col2
.prop(self
, "dif_z", text
="Height per Cycle")
503 col2
.prop(self
, "touch", text
="Make Snail")
505 col
= box
.column(align
=True)
506 col
.prop(self
, "curves_number", text
="Curves Number")
507 col
.prop(self
, "inner_radius", text
="Inner Radius")
508 col
.prop(self
, "dif_radius", text
="Increase of Torus Radius")
509 col
.prop(self
, "dif_inner_radius", text
="Increase of Inner Radius")
512 row
.prop(self
, "shape", expand
=True)
515 col
= layout
.column()
516 col
.label(text
="Output Curve Type:")
517 col
.row().prop(self
, "curve_type", expand
=True)
519 if self
.curve_type
== 'NURBS':
520 col
.prop(self
, "order_u")
521 elif self
.curve_type
== 'BEZIER':
522 col
.row().prop(self
, 'handleType', expand
=True)
524 col
= layout
.column()
525 col
.row().prop(self
, "use_cyclic_u", expand
=True)
527 col
= layout
.column()
528 col
.row().prop(self
, "edit_mode", expand
=True)
531 box
.label(text
="Location:")
532 box
.prop(self
, "startlocation")
534 box
.label(text
="Rotation:")
535 box
.prop(self
, "rotation_euler")
538 def poll(cls
, context
):
539 return context
.scene
is not None
541 def execute(self
, context
):
542 # turn off 'Enter Edit Mode'
543 use_enter_edit_mode
= bpy
.context
.preferences
.edit
.use_enter_edit_mode
544 bpy
.context
.preferences
.edit
.use_enter_edit_mode
= False
546 time_start
= time
.time()
547 self
.align_matrix
= align_matrix(context
, self
.startlocation
)
548 draw_curve(self
, context
, self
.align_matrix
)
550 if use_enter_edit_mode
:
551 bpy
.ops
.object.mode_set(mode
= 'EDIT')
553 # restore pre operator state
554 bpy
.context
.preferences
.edit
.use_enter_edit_mode
= use_enter_edit_mode
557 bpy
.ops
.object.mode_set(mode
= 'EDIT')
559 bpy
.ops
.object.mode_set(mode
= 'OBJECT')
561 #self.report({'INFO'},
562 #"Drawing Spiral Finished: %.4f sec" % (time.time() - time_start))
567 class CURVE_EXTRAS_OT_spirals_presets(AddPresetBase
, Operator
):
568 bl_idname
= "curve_extras.spiral_presets"
570 bl_description
= "Spirals Presets"
571 preset_menu
= "OBJECT_MT_spiral_curve_presets"
572 preset_subdir
= "curve_extras/curve.spirals"
575 "op = bpy.context.active_operator",
580 "op.spiral_direction",
588 "op.dif_inner_radius",
595 class OBJECT_MT_spiral_curve_presets(Menu
):
596 '''Presets for curve.spiral'''
597 bl_label
= "Spiral Curve Presets"
598 bl_idname
= "OBJECT_MT_spiral_curve_presets"
599 preset_subdir
= "curve_extras/curve.spirals"
600 preset_operator
= "script.execute_preset"
602 draw
= bpy
.types
.Menu
.draw_preset
608 CURVE_EXTRAS_OT_spirals_presets
,
609 OBJECT_MT_spiral_curve_presets
613 from bpy
.utils
import register_class
618 from bpy
.utils
import unregister_class
619 for cls
in reversed(classes
):
620 unregister_class(cls
)
622 if __name__
== "__main__":