1 # SPDX-FileCopyrightText: 2012-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
8 "description": "Make spirals",
9 "author": "Alejandro Omar Chocano Vasquez",
11 "blender": (2, 80, 0),
12 "location": "View3D > Add > Curve",
14 "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/extra_objects.html",
15 "category": "Add Curve",
21 from bpy
.props
import (
28 from mathutils
import (
35 from bpy_extras
import object_utils
36 from bpy
.types
import (
40 from bl_operators
.presets
import AddPresetBase
44 # ----------------------------------------------------------------------------
46 def make_spiral(props
, context
):
47 # archemedian and logarithmic can be plotted in cylindrical coordinates
49 # INPUT: turns->degree->max_phi, steps, direction
50 # Initialise Polar Coordinate Environment
51 props
.degree
= 360 * props
.turns
# If you want to make the slider for degree
52 steps
= props
.steps
* props
.turns
# props.steps[per turn] -> steps[for the whole spiral]
53 props
.z_scale
= props
.dif_z
* props
.turns
55 max_phi
= pi
* props
.degree
/ 180 # max angle in radian
56 step_phi
= max_phi
/ steps
# angle in radians between two vertices
58 if props
.spiral_direction
== 'CLOCKWISE':
59 step_phi
*= -1 # flip direction
62 step_z
= props
.z_scale
/ (steps
- 1) # z increase in one step
65 verts
.append([props
.radius
, 0, 0])
70 # Archemedean: dif_radius, radius
71 cur_rad
= props
.radius
72 step_rad
= props
.dif_radius
/ (steps
* 360 / props
.degree
)
73 # radius increase per angle for archemedean spiral|
74 # (steps * 360/props.degree)...Steps needed for 360 deg
75 # Logarithmic: radius, B_force, ang_div, dif_z
77 while abs(cur_phi
) <= abs(max_phi
):
81 if props
.spiral_type
== 'ARCH':
83 if props
.spiral_type
== 'LOG':
84 # r = a*e^{|theta| * b}
85 cur_rad
= props
.radius
* pow(props
.B_force
, abs(cur_phi
))
87 px
= cur_rad
* cos(cur_phi
)
88 py
= cur_rad
* sin(cur_phi
)
89 verts
.append([px
, py
, cur_z
])
95 # ----------------------------------------------------------------------------
97 def make_spiral_spheric(props
, context
):
98 # INPUT: turns, steps[per turn], radius
99 # use spherical Coordinates
100 step_phi
= (2 * pi
) / props
.steps
# Step of angle in radians for one turn
101 steps
= props
.steps
* props
.turns
# props.steps[per turn] -> steps[for the whole spiral]
103 max_phi
= 2 * pi
* props
.turns
# max angle in radian
104 step_phi
= max_phi
/ steps
# angle in radians between two vertices
105 if props
.spiral_direction
== 'CLOCKWISE': # flip direction
108 step_theta
= pi
/ (steps
- 1) # theta increase in one step (pi == 180 deg)
111 verts
.append([0, 0, -props
.radius
]) # First vertex at south pole
114 cur_theta
= -pi
/ 2 # Beginning at south pole
116 while abs(cur_phi
) <= abs(max_phi
):
117 # Coordinate Transformation sphere->rect
118 px
= props
.radius
* cos(cur_theta
) * cos(cur_phi
)
119 py
= props
.radius
* cos(cur_theta
) * sin(cur_phi
)
120 pz
= props
.radius
* sin(cur_theta
)
122 verts
.append([px
, py
, pz
])
123 cur_theta
+= step_theta
130 # ----------------------------------------------------------------------------
132 def make_spiral_torus(props
, context
):
133 # INPUT: turns, steps, inner_radius, curves_number,
134 # mul_height, dif_inner_radius, cycles
135 max_phi
= 2 * pi
* props
.turns
* props
.cycles
# max angle in radian
136 step_phi
= 2 * pi
/ props
.steps
# Step of angle in radians between two vertices
138 if props
.spiral_direction
== 'CLOCKWISE': # flip direction
142 step_theta
= (2 * pi
/ props
.turns
) / props
.steps
143 step_rad
= props
.dif_radius
/ (props
.steps
* props
.turns
)
144 step_inner_rad
= props
.dif_inner_radius
/ props
.steps
145 step_z
= props
.dif_z
/ (props
.steps
* props
.turns
)
149 cur_phi
= 0 # Inner Ring Radius Angle
150 cur_theta
= 0 # Ring Radius Angle
151 cur_rad
= props
.radius
152 cur_inner_rad
= props
.inner_radius
156 while abs(cur_phi
) <= abs(max_phi
):
157 # Torus Coordinates -> Rect
158 px
= (cur_rad
+ cur_inner_rad
* cos(cur_phi
)) * \
159 cos(props
.curves_number
* cur_theta
)
160 py
= (cur_rad
+ cur_inner_rad
* cos(cur_phi
)) * \
161 sin(props
.curves_number
* cur_theta
)
162 pz
= cur_inner_rad
* sin(cur_phi
) + cur_z
164 verts
.append([px
, py
, pz
])
166 if props
.touch
and cur_phi
>= n_cycle
* 2 * pi
:
167 step_z
= ((n_cycle
+ 1) * props
.dif_inner_radius
+
168 props
.inner_radius
) * 2 / (props
.steps
* props
.turns
)
171 cur_theta
+= step_theta
174 cur_inner_rad
+= step_inner_rad
180 # ------------------------------------------------------------
181 # get array of vertcoordinates according to splinetype
182 def vertsToPoints(Verts
, splineType
):
187 # array for BEZIER spline output (V3)
188 if splineType
== 'BEZIER':
192 # array for nonBEZIER output (V4)
196 if splineType
== 'NURBS':
205 # ------------------------------------------------------------
206 # create curve object according to the values of the add object editor
207 def draw_curve(props
, context
):
208 # output splineType 'POLY' 'NURBS' 'BEZIER'
209 splineType
= props
.curve_type
211 if props
.spiral_type
== 'ARCH':
212 verts
= make_spiral(props
, context
)
213 if props
.spiral_type
== 'LOG':
214 verts
= make_spiral(props
, context
)
215 if props
.spiral_type
== 'SPHERE':
216 verts
= make_spiral_spheric(props
, context
)
217 if props
.spiral_type
== 'TORUS':
218 verts
= make_spiral_torus(props
, context
)
221 if bpy
.context
.mode
== 'EDIT_CURVE':
222 Curve
= context
.active_object
223 newSpline
= Curve
.data
.splines
.new(type=splineType
) # spline
226 dataCurve
= bpy
.data
.curves
.new(name
='Spiral', type='CURVE') # curvedatablock
227 newSpline
= dataCurve
.splines
.new(type=splineType
) # spline
229 # create object with newCurve
230 Curve
= object_utils
.object_data_add(context
, dataCurve
, operator
=props
) # place in active scene
232 Curve
.select_set(True)
234 # turn verts into array
235 vertArray
= vertsToPoints(verts
, splineType
)
237 for spline
in Curve
.data
.splines
:
238 if spline
.type == 'BEZIER':
239 for point
in spline
.bezier_points
:
240 point
.select_control_point
= False
241 point
.select_left_handle
= False
242 point
.select_right_handle
= False
244 for point
in spline
.points
:
247 # create newSpline from vertarray
248 if splineType
== 'BEZIER':
249 newSpline
.bezier_points
.add(int(len(vertArray
) * 0.33))
250 newSpline
.bezier_points
.foreach_set('co', vertArray
)
251 for point
in newSpline
.bezier_points
:
252 point
.handle_right_type
= props
.handleType
253 point
.handle_left_type
= props
.handleType
254 point
.select_control_point
= True
255 point
.select_left_handle
= True
256 point
.select_right_handle
= True
258 newSpline
.points
.add(int(len(vertArray
) * 0.25 - 1))
259 newSpline
.points
.foreach_set('co', vertArray
)
260 newSpline
.use_endpoint_u
= False
261 for point
in newSpline
.points
:
265 newSpline
.use_cyclic_u
= props
.use_cyclic_u
266 newSpline
.use_endpoint_u
= props
.endp_u
267 newSpline
.order_u
= props
.order_u
270 Curve
.data
.dimensions
= props
.shape
271 Curve
.data
.use_path
= True
272 if props
.shape
== '3D':
273 Curve
.data
.fill_mode
= 'FULL'
275 Curve
.data
.fill_mode
= 'BOTH'
277 # move and rotate spline in edit mode
278 if bpy
.context
.mode
== 'EDIT_CURVE':
279 if props
.align
== 'WORLD':
280 location
= props
.location
- context
.active_object
.location
281 bpy
.ops
.transform
.translate(value
= location
, orient_type
='GLOBAL')
282 bpy
.ops
.transform
.rotate(value
= props
.rotation
[0], orient_axis
= 'X')
283 bpy
.ops
.transform
.rotate(value
= props
.rotation
[1], orient_axis
= 'Y')
284 bpy
.ops
.transform
.rotate(value
= props
.rotation
[2], orient_axis
= 'Z')
285 elif props
.align
== "VIEW":
286 bpy
.ops
.transform
.translate(value
= props
.location
)
287 bpy
.ops
.transform
.rotate(value
= props
.rotation
[0], orient_axis
= 'X')
288 bpy
.ops
.transform
.rotate(value
= props
.rotation
[1], orient_axis
= 'Y')
289 bpy
.ops
.transform
.rotate(value
= props
.rotation
[2], orient_axis
= 'Z')
291 elif props
.align
== "CURSOR":
292 location
= context
.active_object
.location
293 props
.location
= bpy
.context
.scene
.cursor
.location
- location
294 props
.rotation
= bpy
.context
.scene
.cursor
.rotation_euler
296 bpy
.ops
.transform
.translate(value
= props
.location
)
297 bpy
.ops
.transform
.rotate(value
= props
.rotation
[0], orient_axis
= 'X')
298 bpy
.ops
.transform
.rotate(value
= props
.rotation
[1], orient_axis
= 'Y')
299 bpy
.ops
.transform
.rotate(value
= props
.rotation
[2], orient_axis
= 'Z')
302 class CURVE_OT_spirals(Operator
, object_utils
.AddObjectHelper
):
303 bl_idname
= "curve.spirals"
304 bl_label
= "Curve Spirals"
305 bl_description
= "Create different types of spirals"
306 bl_options
= {'REGISTER', 'UNDO', 'PRESET'}
308 spiral_type
: EnumProperty(
309 items
=[('ARCH', "Archemedian", "Archemedian"),
310 ("LOG", "Logarithmic", "Logarithmic"),
311 ("SPHERE", "Spheric", "Spheric"),
312 ("TORUS", "Torus", "Torus")],
315 description
="Type of spiral to add"
317 spiral_direction
: EnumProperty(
318 items
=[('COUNTER_CLOCKWISE', "Counter Clockwise",
319 "Wind in a counter clockwise direction"),
320 ("CLOCKWISE", "Clockwise",
321 "Wind in a clockwise direction")],
322 default
='COUNTER_CLOCKWISE',
323 name
="Spiral Direction",
324 description
="Direction of winding"
329 description
="Length of Spiral in 360 deg"
334 description
="Number of Vertices per turn"
336 radius
: FloatProperty(
338 min=0.00, max=100.00,
339 description
="Radius for first turn"
341 dif_z
: FloatProperty(
343 min=-10.00, max=100.00,
344 description
="Increase in Z axis per turn"
346 # needed for 1 and 2 spiral_type
347 # Archemedian variables
348 dif_radius
: FloatProperty(
350 min=-50.00, max=50.00,
351 description
="Radius increment in each turn"
353 # step between turns(one turn equals 360 deg)
355 B_force
: FloatProperty(
358 description
="Factor of exponent"
361 inner_radius
: FloatProperty(
364 description
="Inner Radius of Torus"
366 dif_inner_radius
: FloatProperty(
369 description
="Increase of inner Radius per Cycle"
371 dif_radius
: FloatProperty(
374 description
="Increase of Torus Radius per Cycle"
376 cycles
: FloatProperty(
379 description
="Number of Cycles"
381 curves_number
: IntProperty(
384 description
="Number of curves of spiral"
388 description
="No empty spaces between cycles"
392 ('2D', "2D", "2D shape Curve"),
393 ('3D', "3D", "3D shape Curve")]
394 shape
: EnumProperty(
397 description
="2D or 3D Curve",
400 curve_type
: EnumProperty(
401 name
="Output splines",
402 description
="Type of splines to output",
404 ('POLY', "Poly", "Poly Spline type"),
405 ('NURBS', "Nurbs", "Nurbs Spline type"),
406 ('BEZIER', "Bezier", "Bezier Spline type")],
409 use_cyclic_u
: BoolProperty(
412 description
="make curve closed"
414 endp_u
: BoolProperty(
415 name
="Use endpoint u",
417 description
="stretch to endpoints"
419 order_u
: IntProperty(
424 description
="Order of nurbs spline"
426 handleType
: EnumProperty(
429 description
="Bezier handles type",
431 ('VECTOR', "Vector", "Vector type Bezier handles"),
432 ('AUTO', "Auto", "Automatic type Bezier handles")]
434 edit_mode
: BoolProperty(
435 name
="Show in edit mode",
437 description
="Show in edit mode"
440 def draw(self
, context
):
442 col
= layout
.column_flow(align
=True)
444 layout
.prop(self
, "spiral_type")
445 layout
.prop(self
, "spiral_direction")
447 col
= layout
.column(align
=True)
448 col
.label(text
="Spiral Parameters:")
449 col
.prop(self
, "turns", text
="Turns")
450 col
.prop(self
, "steps", text
="Steps")
453 if self
.spiral_type
== 'ARCH':
454 box
.label(text
="Archemedian Settings:")
455 col
= box
.column(align
=True)
456 col
.prop(self
, "dif_radius", text
="Radius Growth")
457 col
.prop(self
, "radius", text
="Radius")
458 col
.prop(self
, "dif_z", text
="Height")
460 if self
.spiral_type
== 'LOG':
461 box
.label(text
="Logarithmic Settings:")
462 col
= box
.column(align
=True)
463 col
.prop(self
, "radius", text
="Radius")
464 col
.prop(self
, "B_force", text
="Expansion Force")
465 col
.prop(self
, "dif_z", text
="Height")
467 if self
.spiral_type
== 'SPHERE':
468 box
.label(text
="Spheric Settings:")
469 box
.prop(self
, "radius", text
="Radius")
471 if self
.spiral_type
== 'TORUS':
472 box
.label(text
="Torus Settings:")
473 col
= box
.column(align
=True)
474 col
.prop(self
, "cycles", text
="Number of Cycles")
476 if self
.dif_inner_radius
== 0 and self
.dif_z
== 0:
478 col
.prop(self
, "radius", text
="Radius")
481 col
.prop(self
, "dif_z", text
="Height per Cycle")
484 col2
= box2
.column(align
=True)
485 col2
.prop(self
, "dif_z", text
="Height per Cycle")
486 col2
.prop(self
, "touch", text
="Make Snail")
488 col
= box
.column(align
=True)
489 col
.prop(self
, "curves_number", text
="Curves Number")
490 col
.prop(self
, "inner_radius", text
="Inner Radius")
491 col
.prop(self
, "dif_radius", text
="Increase of Torus Radius")
492 col
.prop(self
, "dif_inner_radius", text
="Increase of Inner Radius")
495 row
.prop(self
, "shape", expand
=True)
498 col
= layout
.column()
499 col
.label(text
="Output Curve Type:")
500 col
.row().prop(self
, "curve_type", expand
=True)
502 if self
.curve_type
== 'NURBS':
503 col
.prop(self
, "order_u")
504 elif self
.curve_type
== 'BEZIER':
505 col
.row().prop(self
, 'handleType', expand
=True)
507 col
= layout
.column()
508 col
.row().prop(self
, "use_cyclic_u", expand
=True)
510 col
= layout
.column()
511 col
.row().prop(self
, "edit_mode", expand
=True)
513 col
= layout
.column()
514 # AddObjectHelper props
515 col
.prop(self
, "align")
516 col
.prop(self
, "location")
517 col
.prop(self
, "rotation")
520 def poll(cls
, context
):
521 return context
.scene
is not None
523 def execute(self
, context
):
524 # turn off 'Enter Edit Mode'
525 use_enter_edit_mode
= bpy
.context
.preferences
.edit
.use_enter_edit_mode
526 bpy
.context
.preferences
.edit
.use_enter_edit_mode
= False
528 time_start
= time
.time()
529 draw_curve(self
, context
)
531 if use_enter_edit_mode
:
532 bpy
.ops
.object.mode_set(mode
= 'EDIT')
534 # restore pre operator state
535 bpy
.context
.preferences
.edit
.use_enter_edit_mode
= use_enter_edit_mode
538 bpy
.ops
.object.mode_set(mode
= 'EDIT')
540 bpy
.ops
.object.mode_set(mode
= 'OBJECT')
542 #self.report({'INFO'},
543 #"Drawing Spiral Finished: %.4f sec" % (time.time() - time_start))
548 class CURVE_EXTRAS_OT_spirals_presets(AddPresetBase
, Operator
):
549 bl_idname
= "curve_extras.spiral_presets"
551 bl_description
= "Spirals Presets"
552 preset_menu
= "OBJECT_MT_spiral_curve_presets"
553 preset_subdir
= "curve_extras/curve.spirals"
556 "op = bpy.context.active_operator",
561 "op.spiral_direction",
569 "op.dif_inner_radius",
576 class OBJECT_MT_spiral_curve_presets(Menu
):
577 '''Presets for curve.spiral'''
578 bl_label
= "Spiral Curve Presets"
579 bl_idname
= "OBJECT_MT_spiral_curve_presets"
580 preset_subdir
= "curve_extras/curve.spirals"
581 preset_operator
= "script.execute_preset"
583 draw
= bpy
.types
.Menu
.draw_preset
589 CURVE_EXTRAS_OT_spirals_presets
,
590 OBJECT_MT_spiral_curve_presets
594 from bpy
.utils
import register_class
599 from bpy
.utils
import unregister_class
600 for cls
in reversed(classes
):
601 unregister_class(cls
)
603 if __name__
== "__main__":