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
import object_utils
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
178 # ------------------------------------------------------------
179 # get array of vertcoordinates according to splinetype
180 def vertsToPoints(Verts
, splineType
):
185 # array for BEZIER spline output (V3)
186 if splineType
== 'BEZIER':
190 # array for nonBEZIER output (V4)
194 if splineType
== 'NURBS':
203 # ------------------------------------------------------------
204 # create curve object according to the values of the add object editor
205 def draw_curve(props
, context
):
206 # output splineType 'POLY' 'NURBS' 'BEZIER'
207 splineType
= props
.curve_type
209 if props
.spiral_type
== 'ARCH':
210 verts
= make_spiral(props
, context
)
211 if props
.spiral_type
== 'LOG':
212 verts
= make_spiral(props
, context
)
213 if props
.spiral_type
== 'SPHERE':
214 verts
= make_spiral_spheric(props
, context
)
215 if props
.spiral_type
== 'TORUS':
216 verts
= make_spiral_torus(props
, context
)
219 if bpy
.context
.mode
== 'EDIT_CURVE':
220 Curve
= context
.active_object
221 newSpline
= Curve
.data
.splines
.new(type=splineType
) # spline
224 dataCurve
= bpy
.data
.curves
.new(name
='Spiral', type='CURVE') # curvedatablock
225 newSpline
= dataCurve
.splines
.new(type=splineType
) # spline
227 # create object with newCurve
228 Curve
= object_utils
.object_data_add(context
, dataCurve
, operator
=props
) # place in active scene
230 Curve
.select_set(True)
232 # turn verts into array
233 vertArray
= vertsToPoints(verts
, splineType
)
235 for spline
in Curve
.data
.splines
:
236 if spline
.type == 'BEZIER':
237 for point
in spline
.bezier_points
:
238 point
.select_control_point
= False
239 point
.select_left_handle
= False
240 point
.select_right_handle
= False
242 for point
in spline
.points
:
245 # create newSpline from vertarray
246 if splineType
== 'BEZIER':
247 newSpline
.bezier_points
.add(int(len(vertArray
) * 0.33))
248 newSpline
.bezier_points
.foreach_set('co', vertArray
)
249 for point
in newSpline
.bezier_points
:
250 point
.handle_right_type
= props
.handleType
251 point
.handle_left_type
= props
.handleType
252 point
.select_control_point
= True
253 point
.select_left_handle
= True
254 point
.select_right_handle
= True
256 newSpline
.points
.add(int(len(vertArray
) * 0.25 - 1))
257 newSpline
.points
.foreach_set('co', vertArray
)
258 newSpline
.use_endpoint_u
= False
259 for point
in newSpline
.points
:
263 newSpline
.use_cyclic_u
= props
.use_cyclic_u
264 newSpline
.use_endpoint_u
= props
.endp_u
265 newSpline
.order_u
= props
.order_u
268 Curve
.data
.dimensions
= props
.shape
269 Curve
.data
.use_path
= True
270 if props
.shape
== '3D':
271 Curve
.data
.fill_mode
= 'FULL'
273 Curve
.data
.fill_mode
= 'BOTH'
275 # move and rotate spline in edit mode
276 if bpy
.context
.mode
== 'EDIT_CURVE':
277 if props
.align
== 'WORLD':
278 location
= props
.location
- context
.active_object
.location
279 bpy
.ops
.transform
.translate(value
= location
, orient_type
='GLOBAL')
280 bpy
.ops
.transform
.rotate(value
= props
.rotation
[0], orient_axis
= 'X')
281 bpy
.ops
.transform
.rotate(value
= props
.rotation
[1], orient_axis
= 'Y')
282 bpy
.ops
.transform
.rotate(value
= props
.rotation
[2], orient_axis
= 'Z')
283 elif props
.align
== "VIEW":
284 bpy
.ops
.transform
.translate(value
= props
.location
)
285 bpy
.ops
.transform
.rotate(value
= props
.rotation
[0], orient_axis
= 'X')
286 bpy
.ops
.transform
.rotate(value
= props
.rotation
[1], orient_axis
= 'Y')
287 bpy
.ops
.transform
.rotate(value
= props
.rotation
[2], orient_axis
= 'Z')
289 elif props
.align
== "CURSOR":
290 location
= context
.active_object
.location
291 props
.location
= bpy
.context
.scene
.cursor
.location
- location
292 props
.rotation
= bpy
.context
.scene
.cursor
.rotation_euler
294 bpy
.ops
.transform
.translate(value
= props
.location
)
295 bpy
.ops
.transform
.rotate(value
= props
.rotation
[0], orient_axis
= 'X')
296 bpy
.ops
.transform
.rotate(value
= props
.rotation
[1], orient_axis
= 'Y')
297 bpy
.ops
.transform
.rotate(value
= props
.rotation
[2], orient_axis
= 'Z')
300 class CURVE_OT_spirals(Operator
, object_utils
.AddObjectHelper
):
301 bl_idname
= "curve.spirals"
302 bl_label
= "Curve Spirals"
303 bl_description
= "Create different types of spirals"
304 bl_options
= {'REGISTER', 'UNDO', 'PRESET'}
306 spiral_type
: EnumProperty(
307 items
=[('ARCH', "Archemedian", "Archemedian"),
308 ("LOG", "Logarithmic", "Logarithmic"),
309 ("SPHERE", "Spheric", "Spheric"),
310 ("TORUS", "Torus", "Torus")],
313 description
="Type of spiral to add"
315 spiral_direction
: EnumProperty(
316 items
=[('COUNTER_CLOCKWISE', "Counter Clockwise",
317 "Wind in a counter clockwise direction"),
318 ("CLOCKWISE", "Clockwise",
319 "Wind in a clockwise direction")],
320 default
='COUNTER_CLOCKWISE',
321 name
="Spiral Direction",
322 description
="Direction of winding"
327 description
="Length of Spiral in 360 deg"
332 description
="Number of Vertices per turn"
334 radius
: FloatProperty(
336 min=0.00, max=100.00,
337 description
="Radius for first turn"
339 dif_z
: FloatProperty(
341 min=-10.00, max=100.00,
342 description
="Increase in Z axis per turn"
344 # needed for 1 and 2 spiral_type
345 # Archemedian variables
346 dif_radius
: FloatProperty(
348 min=-50.00, max=50.00,
349 description
="Radius increment in each turn"
351 # step between turns(one turn equals 360 deg)
353 B_force
: FloatProperty(
356 description
="Factor of exponent"
359 inner_radius
: FloatProperty(
362 description
="Inner Radius of Torus"
364 dif_inner_radius
: FloatProperty(
367 description
="Increase of inner Radius per Cycle"
369 dif_radius
: FloatProperty(
372 description
="Increase of Torus Radius per Cycle"
374 cycles
: FloatProperty(
377 description
="Number of Cycles"
379 curves_number
: IntProperty(
382 description
="Number of curves of spiral"
386 description
="No empty spaces between cycles"
390 ('2D', "2D", "2D shape Curve"),
391 ('3D', "3D", "3D shape Curve")]
392 shape
: EnumProperty(
395 description
="2D or 3D Curve",
398 curve_type
: EnumProperty(
399 name
="Output splines",
400 description
="Type of splines to output",
402 ('POLY', "Poly", "Poly Spline type"),
403 ('NURBS', "Nurbs", "Nurbs Spline type"),
404 ('BEZIER', "Bezier", "Bezier Spline type")],
407 use_cyclic_u
: BoolProperty(
410 description
="make curve closed"
412 endp_u
: BoolProperty(
413 name
="Use endpoint u",
415 description
="stretch to endpoints"
417 order_u
: IntProperty(
422 description
="Order of nurbs spline"
424 handleType
: EnumProperty(
427 description
="Bezier handles type",
429 ('VECTOR', "Vector", "Vector type Bezier handles"),
430 ('AUTO', "Auto", "Automatic type Bezier handles")]
432 edit_mode
: BoolProperty(
433 name
="Show in edit mode",
435 description
="Show in edit mode"
438 def draw(self
, context
):
440 col
= layout
.column_flow(align
=True)
442 layout
.prop(self
, "spiral_type")
443 layout
.prop(self
, "spiral_direction")
445 col
= layout
.column(align
=True)
446 col
.label(text
="Spiral Parameters:")
447 col
.prop(self
, "turns", text
="Turns")
448 col
.prop(self
, "steps", text
="Steps")
451 if self
.spiral_type
== 'ARCH':
452 box
.label(text
="Archemedian Settings:")
453 col
= box
.column(align
=True)
454 col
.prop(self
, "dif_radius", text
="Radius Growth")
455 col
.prop(self
, "radius", text
="Radius")
456 col
.prop(self
, "dif_z", text
="Height")
458 if self
.spiral_type
== 'LOG':
459 box
.label(text
="Logarithmic Settings:")
460 col
= box
.column(align
=True)
461 col
.prop(self
, "radius", text
="Radius")
462 col
.prop(self
, "B_force", text
="Expansion Force")
463 col
.prop(self
, "dif_z", text
="Height")
465 if self
.spiral_type
== 'SPHERE':
466 box
.label(text
="Spheric Settings:")
467 box
.prop(self
, "radius", text
="Radius")
469 if self
.spiral_type
== 'TORUS':
470 box
.label(text
="Torus Settings:")
471 col
= box
.column(align
=True)
472 col
.prop(self
, "cycles", text
="Number of Cycles")
474 if self
.dif_inner_radius
== 0 and self
.dif_z
== 0:
476 col
.prop(self
, "radius", text
="Radius")
479 col
.prop(self
, "dif_z", text
="Height per Cycle")
482 col2
= box2
.column(align
=True)
483 col2
.prop(self
, "dif_z", text
="Height per Cycle")
484 col2
.prop(self
, "touch", text
="Make Snail")
486 col
= box
.column(align
=True)
487 col
.prop(self
, "curves_number", text
="Curves Number")
488 col
.prop(self
, "inner_radius", text
="Inner Radius")
489 col
.prop(self
, "dif_radius", text
="Increase of Torus Radius")
490 col
.prop(self
, "dif_inner_radius", text
="Increase of Inner Radius")
493 row
.prop(self
, "shape", expand
=True)
496 col
= layout
.column()
497 col
.label(text
="Output Curve Type:")
498 col
.row().prop(self
, "curve_type", expand
=True)
500 if self
.curve_type
== 'NURBS':
501 col
.prop(self
, "order_u")
502 elif self
.curve_type
== 'BEZIER':
503 col
.row().prop(self
, 'handleType', expand
=True)
505 col
= layout
.column()
506 col
.row().prop(self
, "use_cyclic_u", expand
=True)
508 col
= layout
.column()
509 col
.row().prop(self
, "edit_mode", expand
=True)
511 col
= layout
.column()
512 # AddObjectHelper props
513 col
.prop(self
, "align")
514 col
.prop(self
, "location")
515 col
.prop(self
, "rotation")
518 def poll(cls
, context
):
519 return context
.scene
is not None
521 def execute(self
, context
):
522 # turn off 'Enter Edit Mode'
523 use_enter_edit_mode
= bpy
.context
.preferences
.edit
.use_enter_edit_mode
524 bpy
.context
.preferences
.edit
.use_enter_edit_mode
= False
526 time_start
= time
.time()
527 draw_curve(self
, context
)
529 if use_enter_edit_mode
:
530 bpy
.ops
.object.mode_set(mode
= 'EDIT')
532 # restore pre operator state
533 bpy
.context
.preferences
.edit
.use_enter_edit_mode
= use_enter_edit_mode
536 bpy
.ops
.object.mode_set(mode
= 'EDIT')
538 bpy
.ops
.object.mode_set(mode
= 'OBJECT')
540 #self.report({'INFO'},
541 #"Drawing Spiral Finished: %.4f sec" % (time.time() - time_start))
546 class CURVE_EXTRAS_OT_spirals_presets(AddPresetBase
, Operator
):
547 bl_idname
= "curve_extras.spiral_presets"
549 bl_description
= "Spirals Presets"
550 preset_menu
= "OBJECT_MT_spiral_curve_presets"
551 preset_subdir
= "curve_extras/curve.spirals"
554 "op = bpy.context.active_operator",
559 "op.spiral_direction",
567 "op.dif_inner_radius",
574 class OBJECT_MT_spiral_curve_presets(Menu
):
575 '''Presets for curve.spiral'''
576 bl_label
= "Spiral Curve Presets"
577 bl_idname
= "OBJECT_MT_spiral_curve_presets"
578 preset_subdir
= "curve_extras/curve.spirals"
579 preset_operator
= "script.execute_preset"
581 draw
= bpy
.types
.Menu
.draw_preset
587 CURVE_EXTRAS_OT_spirals_presets
,
588 OBJECT_MT_spiral_curve_presets
592 from bpy
.utils
import register_class
597 from bpy
.utils
import unregister_class
598 for cls
in reversed(classes
):
599 unregister_class(cls
)
601 if __name__
== "__main__":