Fix T52833: OBJ triangulate doesn't match viewport
[blender-addons.git] / add_curve_ivygen.py
blobdd52310207cabc2840a7085eab22bb35fd927005
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 #####
19 # <pep8-80 compliant>
21 bl_info = {
22 "name": "IvyGen",
23 "author": "testscreenings, PKHG, TrumanBlending",
24 "version": (0, 1, 2),
25 "blender": (2, 59, 0),
26 "location": "View3D > Add > Curve",
27 "description": "Adds generated ivy to a mesh object starting "
28 "at the 3D cursor",
29 "warning": "",
30 "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
31 "Scripts/Curve/Ivy_Gen",
32 "category": "Add Curve",
36 import bpy
37 from bpy.props import (
38 FloatProperty,
39 IntProperty,
40 BoolProperty,
42 from mathutils import (
43 Vector,
44 Matrix,
46 from collections import deque
47 from math import (
48 pow, cos,
49 pi, atan2,
51 from random import (
52 random as rand_val,
53 seed as rand_seed,
55 import time
58 def createIvyGeometry(IVY, growLeaves):
59 """Create the curve geometry for IVY"""
60 # Compute the local size and the gauss weight filter
61 # local_ivyBranchSize = IVY.ivyBranchSize # * radius * IVY.ivySize
62 gaussWeight = (1.0, 2.0, 4.0, 7.0, 9.0, 10.0, 9.0, 7.0, 4.0, 2.0, 1.0)
64 # Create a new curve and intialise it
65 curve = bpy.data.curves.new("IVY", type='CURVE')
66 curve.dimensions = '3D'
67 curve.bevel_depth = 1
68 curve.fill_mode = 'FULL'
69 curve.resolution_u = 4
71 if growLeaves:
72 # Create the ivy leaves
73 # Order location of the vertices
74 signList = ((-1.0, +1.0),
75 (+1.0, +1.0),
76 (+1.0, -1.0),
77 (-1.0, -1.0),
80 # Get the local size
81 # local_ivyLeafSize = IVY.ivyLeafSize # * radius * IVY.ivySize
83 # Initialise the vertex and face lists
84 vertList = deque()
86 # Store the methods for faster calling
87 addV = vertList.extend
88 rotMat = Matrix.Rotation
90 # Loop over all roots to generate its nodes
91 for root in IVY.ivyRoots:
92 # Only grow if more than one node
93 numNodes = len(root.ivyNodes)
94 if numNodes > 1:
95 # Calculate the local radius
96 local_ivyBranchRadius = 1.0 / (root.parents + 1) + 1.0
97 prevIvyLength = 1.0 / root.ivyNodes[-1].length
98 splineVerts = [ax for n in root.ivyNodes for ax in n.pos.to_4d()]
100 radiusConstant = local_ivyBranchRadius * IVY.ivyBranchSize
101 splineRadii = [radiusConstant * (1.3 - n.length * prevIvyLength)
102 for n in root.ivyNodes]
104 # Add the poly curve and set coords and radii
105 newSpline = curve.splines.new(type='POLY')
106 newSpline.points.add(len(splineVerts) // 4 - 1)
107 newSpline.points.foreach_set('co', splineVerts)
108 newSpline.points.foreach_set('radius', splineRadii)
110 # Loop over all nodes in the root
111 for i, n in enumerate(root.ivyNodes):
112 for k in range(len(gaussWeight)):
113 idx = max(0, min(i + k - 5, numNodes - 1))
114 n.smoothAdhesionVector += (gaussWeight[k] *
115 root.ivyNodes[idx].adhesionVector)
116 n.smoothAdhesionVector /= 56.0
117 n.adhesionLength = n.smoothAdhesionVector.length
118 n.smoothAdhesionVector.normalize()
120 if growLeaves and (i < numNodes - 1):
121 node = root.ivyNodes[i]
122 nodeNext = root.ivyNodes[i + 1]
124 # Find the weight and normalize the smooth adhesion vector
125 weight = pow(node.length * prevIvyLength, 0.7)
127 # Calculate the ground ivy and the new weight
128 groundIvy = max(0.0, -node.smoothAdhesionVector.z)
129 weight += groundIvy * pow(1 - node.length *
130 prevIvyLength, 2)
132 # Find the alignment weight
133 alignmentWeight = node.adhesionLength
135 # Calculate the needed angles
136 phi = atan2(node.smoothAdhesionVector.y,
137 node.smoothAdhesionVector.x) - pi / 2.0
139 theta = (0.5 *
140 node.smoothAdhesionVector.angle(Vector((0, 0, -1)), 0))
142 # Find the size weight
143 sizeWeight = 1.5 - (cos(2 * pi * weight) * 0.5 + 0.5)
145 # Randomise the angles
146 phi += (rand_val() - 0.5) * (1.3 - alignmentWeight)
147 theta += (rand_val() - 0.5) * (1.1 - alignmentWeight)
149 # Calculate the leaf size an append the face to the list
150 leafSize = IVY.ivyLeafSize * sizeWeight
152 for j in range(10):
153 # Generate the probability
154 probability = rand_val()
156 # If we need to grow a leaf, do so
157 if (probability * weight) > IVY.leafProbability:
159 # Generate the random vector
160 randomVector = Vector((rand_val() - 0.5,
161 rand_val() - 0.5,
162 rand_val() - 0.5,
165 # Find the leaf center
166 center = (node.pos.lerp(nodeNext.pos, j / 10.0) +
167 IVY.ivyLeafSize * randomVector)
169 # For each of the verts, rotate/scale and append
170 basisVecX = Vector((1, 0, 0))
171 basisVecY = Vector((0, 1, 0))
173 horiRot = rotMat(theta, 3, 'X')
174 vertRot = rotMat(phi, 3, 'Z')
176 basisVecX.rotate(horiRot)
177 basisVecY.rotate(horiRot)
179 basisVecX.rotate(vertRot)
180 basisVecY.rotate(vertRot)
182 basisVecX *= leafSize
183 basisVecY *= leafSize
185 addV([k1 * basisVecX + k2 * basisVecY + center for
186 k1, k2 in signList])
188 # Add the object and link to scene
189 newCurve = bpy.data.objects.new("IVY_Curve", curve)
190 bpy.context.scene.objects.link(newCurve)
192 if growLeaves:
193 faceList = [[4 * i + l for l in range(4)] for i in
194 range(len(vertList) // 4)]
196 # Generate the new leaf mesh and link
197 me = bpy.data.meshes.new('IvyLeaf')
198 me.from_pydata(vertList, [], faceList)
199 me.update(calc_edges=True)
200 ob = bpy.data.objects.new('IvyLeaf', me)
201 bpy.context.scene.objects.link(ob)
203 me.uv_textures.new("Leaves")
205 # Set the uv texture coords
206 # TODO, this is non-functional, default uvs are ok?
208 for d in tex.data:
209 uv1, uv2, uv3, uv4 = signList
212 ob.parent = newCurve
216 def computeBoundingSphere(ob):
217 # Get the mesh data
218 me = ob.data
219 # Intialise the center
220 center = Vector((0.0, 0.0, 0.0))
221 # Add all vertex coords
222 for v in me.vertices:
223 center += v.co
224 # Average over all verts
225 center /= len(me.vertices)
226 # Create the iterator and find its max
227 length_iter = ((center - v.co).length for v in me.vertices)
228 radius = max(length_iter)
229 return radius
233 class IvyNode:
234 """ The basic class used for each point on the ivy which is grown."""
235 __slots__ = ('pos', 'primaryDir', 'adhesionVector', 'adhesionLength',
236 'smoothAdhesionVector', 'length', 'floatingLength', 'climb')
238 def __init__(self):
239 self.pos = Vector((0, 0, 0))
240 self.primaryDir = Vector((0, 0, 1))
241 self.adhesionVector = Vector((0, 0, 0))
242 self.smoothAdhesionVector = Vector((0, 0, 0))
243 self.length = 0.0001
244 self.floatingLength = 0.0
245 self.climb = True
248 class IvyRoot:
249 """ The class used to hold all ivy nodes growing from this root point."""
250 __slots__ = ('ivyNodes', 'alive', 'parents')
252 def __init__(self):
253 self.ivyNodes = deque()
254 self.alive = True
255 self.parents = 0
258 class Ivy:
259 """ The class holding all parameters and ivy roots."""
260 __slots__ = ('ivyRoots', 'primaryWeight', 'randomWeight',
261 'gravityWeight', 'adhesionWeight', 'branchingProbability',
262 'leafProbability', 'ivySize', 'ivyLeafSize', 'ivyBranchSize',
263 'maxFloatLength', 'maxAdhesionDistance', 'maxLength')
265 def __init__(self,
266 primaryWeight=0.5,
267 randomWeight=0.2,
268 gravityWeight=1.0,
269 adhesionWeight=0.1,
270 branchingProbability=0.05,
271 leafProbability=0.35,
272 ivySize=0.02,
273 ivyLeafSize=0.02,
274 ivyBranchSize=0.001,
275 maxFloatLength=0.5,
276 maxAdhesionDistance=1.0):
278 self.ivyRoots = deque()
279 self.primaryWeight = primaryWeight
280 self.randomWeight = randomWeight
281 self.gravityWeight = gravityWeight
282 self.adhesionWeight = adhesionWeight
283 self.branchingProbability = 1 - branchingProbability
284 self.leafProbability = 1 - leafProbability
285 self.ivySize = ivySize
286 self.ivyLeafSize = ivyLeafSize
287 self.ivyBranchSize = ivyBranchSize
288 self.maxFloatLength = maxFloatLength
289 self.maxAdhesionDistance = maxAdhesionDistance
290 self.maxLength = 0.0
292 # Normalize all the weights only on intialisation
293 sums = self.primaryWeight + self.randomWeight + self.adhesionWeight
294 self.primaryWeight /= sums
295 self.randomWeight /= sums
296 self.adhesionWeight /= sums
298 def seed(self, seedPos):
299 # Seed the Ivy by making a new root and first node
300 tmpRoot = IvyRoot()
301 tmpIvy = IvyNode()
302 tmpIvy.pos = seedPos
304 tmpRoot.ivyNodes.append(tmpIvy)
305 self.ivyRoots.append(tmpRoot)
307 def grow(self, ob):
308 # Determine the local sizes
309 # local_ivySize = self.ivySize # * radius
310 # local_maxFloatLength = self.maxFloatLength # * radius
311 # local_maxAdhesionDistance = self.maxAdhesionDistance # * radius
313 for root in self.ivyRoots:
314 # Make sure the root is alive, if not, skip
315 if not root.alive:
316 continue
318 # Get the last node in the current root
319 prevIvy = root.ivyNodes[-1]
321 # If the node is floating for too long, kill the root
322 if prevIvy.floatingLength > self.maxFloatLength:
323 root.alive = False
325 # Set the primary direction from the last node
326 primaryVector = prevIvy.primaryDir
328 # Make the random vector and normalize
329 randomVector = Vector((rand_val() - 0.5, rand_val() - 0.5,
330 rand_val() - 0.5)) + Vector((0, 0, 0.2))
331 randomVector.normalize()
333 # Calculate the adhesion vector
334 adhesionVector = adhesion(prevIvy.pos, ob,
335 self.maxAdhesionDistance)
337 # Calculate the growing vector
338 growVector = self.ivySize * (primaryVector * self.primaryWeight +
339 randomVector * self.randomWeight +
340 adhesionVector * self.adhesionWeight)
342 # Find the gravity vector
343 gravityVector = (self.ivySize * self.gravityWeight *
344 Vector((0, 0, -1)))
345 gravityVector *= pow(prevIvy.floatingLength / self.maxFloatLength,
346 0.7)
348 # Determine the new position vector
349 newPos = prevIvy.pos + growVector + gravityVector
351 # Check for collisions with the object
352 climbing = collision(ob, prevIvy.pos, newPos)
354 # Update the growing vector for any collisions
355 growVector = newPos - prevIvy.pos - gravityVector
356 growVector.normalize()
358 # Create a new IvyNode and set its properties
359 tmpNode = IvyNode()
360 tmpNode.climb = climbing
361 tmpNode.pos = newPos
362 tmpNode.primaryDir = prevIvy.primaryDir.lerp(growVector, 0.5)
363 tmpNode.primaryDir.normalize()
364 tmpNode.adhesionVector = adhesionVector
365 tmpNode.length = prevIvy.length + (newPos - prevIvy.pos).length
367 if tmpNode.length > self.maxLength:
368 self.maxLength = tmpNode.length
370 # If the node isn't climbing, update it's floating length
371 # Otherwise set it to 0
372 if not climbing:
373 tmpNode.floatingLength = prevIvy.floatingLength + (newPos -
374 prevIvy.pos).length
375 else:
376 tmpNode.floatingLength = 0.0
378 root.ivyNodes.append(tmpNode)
380 # Loop through all roots to check if a new root is generated
381 for root in self.ivyRoots:
382 # Check the root is alive and isn't at high level of recursion
383 if (root.parents > 3) or (not root.alive):
384 continue
386 # Check to make sure there's more than 1 node
387 if len(root.ivyNodes) > 1:
388 # Loop through all nodes in root to check if new root is grown
389 for node in root.ivyNodes:
390 # Set the last node of the root and find the weighting
391 prevIvy = root.ivyNodes[-1]
392 weight = 1.0 - (cos(2.0 * pi * node.length /
393 prevIvy.length) * 0.5 + 0.5)
395 probability = rand_val()
397 # Check if a new root is grown and if so, set its values
398 if (probability * weight > self.branchingProbability):
399 tmpNode = IvyNode()
400 tmpNode.pos = node.pos
401 tmpNode.floatingLength = node.floatingLength
403 tmpRoot = IvyRoot()
404 tmpRoot.parents = root.parents + 1
406 tmpRoot.ivyNodes.append(tmpNode)
407 self.ivyRoots.append(tmpRoot)
408 return
411 def adhesion(loc, ob, max_l):
412 # Get transfor vector and transformed loc
413 tran_mat = ob.matrix_world.inverted()
414 tran_loc = tran_mat * loc
416 # Compute the adhesion vector by finding the nearest point
417 nearest_result = ob.closest_point_on_mesh(tran_loc, max_l)
418 adhesion_vector = Vector((0.0, 0.0, 0.0))
419 if nearest_result[0]:
420 # Compute the distance to the nearest point
421 adhesion_vector = ob.matrix_world * nearest_result[1] - loc
422 distance = adhesion_vector.length
423 # If it's less than the maximum allowed and not 0, continue
424 if distance:
425 # Compute the direction vector between the closest point and loc
426 adhesion_vector.normalize()
427 adhesion_vector *= 1.0 - distance / max_l
428 # adhesion_vector *= getFaceWeight(ob.data, nearest_result[3])
429 return adhesion_vector
432 def collision(ob, pos, new_pos):
433 # Check for collision with the object
434 climbing = False
436 # Transform vecs
437 tran_mat = ob.matrix_world.inverted()
438 tran_pos = tran_mat * pos
439 tran_new_pos = tran_mat * new_pos
440 tran_dir = tran_new_pos - tran_pos
442 ray_result = ob.ray_cast(tran_pos, tran_dir, tran_dir.length)
443 # If there's a collision we need to check it
444 if ray_result[0]:
445 # Check whether the collision is going into the object
446 if tran_dir.dot(ray_result[2]) < 0.0:
447 # Find projection of the piont onto the plane
448 p0 = tran_new_pos - (tran_new_pos -
449 ray_result[1]).project(ray_result[2])
450 # Reflect in the plane
451 tran_new_pos += 2 * (p0 - tran_new_pos)
452 new_pos *= 0
453 new_pos += ob.matrix_world * tran_new_pos
454 climbing = True
455 return climbing
458 def check_mesh_faces(ob):
459 me = ob.data
460 if len(me.polygons) > 0:
461 return True
463 return False
466 class IvyGen(bpy.types.Operator):
467 bl_idname = "curve.ivy_gen"
468 bl_label = "IvyGen"
469 bl_description = "Generate Ivy on an Mesh Object"
470 bl_options = {'REGISTER', 'UNDO'}
472 maxIvyLength = FloatProperty(
473 name="Max Ivy Length",
474 description="Maximum ivy length in Blender Units",
475 default=1.0,
476 min=0.0,
477 soft_max=3.0,
478 subtype='DISTANCE',
479 unit='LENGTH'
481 primaryWeight = FloatProperty(
482 name="Primary Weight",
483 description="Weighting given to the current direction",
484 default=0.5,
485 min=0.0,
486 soft_max=1.0
488 randomWeight = FloatProperty(
489 name="Random Weight",
490 description="Weighting given to the random direction",
491 default=0.2,
492 min=0.0,
493 soft_max=1.0
495 gravityWeight = FloatProperty(
496 name="Gravity Weight",
497 description="Weighting given to the gravity direction",
498 default=1.0,
499 min=0.0,
500 soft_max=1.0
502 adhesionWeight = FloatProperty(
503 name="Adhesion Weight",
504 description="Weighting given to the adhesion direction",
505 default=0.1,
506 min=0.0,
507 soft_max=1.0
509 branchingProbability = FloatProperty(
510 name="Branching Probability",
511 description="Probability of a new branch forming",
512 default=0.05,
513 min=0.0,
514 soft_max=1.0
516 leafProbability = FloatProperty(
517 name="Leaf Probability",
518 description="Probability of a leaf forming",
519 default=0.35,
520 min=0.0,
521 soft_max=1.0
523 ivySize = FloatProperty(
524 name="Ivy Size",
525 description="The length of an ivy segment in Blender"
526 " Units",
527 default=0.02,
528 min=0.0,
529 soft_max=1.0,
530 precision=3
532 ivyLeafSize = FloatProperty(
533 name="Ivy Leaf Size",
534 description="The size of the ivy leaves",
535 default=0.02,
536 min=0.0,
537 soft_max=0.5,
538 precision=3
540 ivyBranchSize = FloatProperty(
541 name="Ivy Branch Size",
542 description="The size of the ivy branches",
543 default=0.001,
544 min=0.0,
545 soft_max=0.1,
546 precision=4
548 maxFloatLength = FloatProperty(
549 name="Max Float Length",
550 description="The maximum distance that a branch "
551 "can live while floating",
552 default=0.5,
553 min=0.0,
554 soft_max=1.0)
555 maxAdhesionDistance = FloatProperty(
556 name="Max Adhesion Length",
557 description="The maximum distance that a branch "
558 "will feel the effects of adhesion",
559 default=1.0,
560 min=0.0,
561 soft_max=2.0,
562 precision=2
564 randomSeed = IntProperty(
565 name="Random Seed",
566 description="The seed governing random generation",
567 default=0,
568 min=0
570 maxTime = FloatProperty(
571 name="Maximum Time",
572 description="The maximum time to run the generation for "
573 "in seconds generation (0.0 = Disabled)",
574 default=0.0,
575 min=0.0,
576 soft_max=10
578 growLeaves = BoolProperty(
579 name="Grow Leaves",
580 description="Grow leaves or not",
581 default=True
583 updateIvy = BoolProperty(
584 name="Update Ivy",
585 default=False
588 @classmethod
589 def poll(self, context):
590 # Check if there's an object and whether it's a mesh
591 ob = context.active_object
592 return ((ob is not None) and
593 (ob.type == 'MESH') and
594 (context.mode == 'OBJECT'))
596 def invoke(self, context, event):
597 self.updateIvy = True
598 return self.execute(context)
600 def execute(self, context):
601 if not self.updateIvy:
602 return {'PASS_THROUGH'}
604 bpy.ops.object.mode_set(mode='EDIT', toggle=False)
605 bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
607 # Get the selected object
608 ob = context.active_object
610 # Check if the mesh has at least one polygon since some functions
611 # are expecting them in the object's data (see T51753)
612 check_face = check_mesh_faces(ob)
613 if check_face is False:
614 self.report({'WARNING'},
615 "Mesh Object doesn't have at least one Face. "
616 "Operation Cancelled")
617 return {"CANCELLED"}
619 # Compute bounding sphere radius
620 # radius = computeBoundingSphere(ob) # Not needed anymore
622 # Get the seeding point
623 seedPoint = context.scene.cursor_location
625 # Fix the random seed
626 rand_seed(self.randomSeed)
628 # Make the new ivy
629 IVY = Ivy(**self.as_keywords(ignore=('randomSeed', 'growLeaves',
630 'maxIvyLength', 'maxTime', 'updateIvy')))
632 # Generate first root and node
633 IVY.seed(seedPoint)
635 checkTime = False
636 maxLength = self.maxIvyLength # * radius
638 # If we need to check time set the flag
639 if self.maxTime != 0.0:
640 checkTime = True
642 t = time.time()
643 startPercent = 0.0
644 checkAliveIter = [True, ]
646 # Grow until 200 roots is reached or backup counter exceeds limit
647 while (any(checkAliveIter) and
648 (IVY.maxLength < maxLength) and
649 (not checkTime or (time.time() - t < self.maxTime))):
650 # Grow the ivy for this iteration
651 IVY.grow(ob)
653 # Print the proportion of ivy growth to console
654 if (IVY.maxLength / maxLength * 100) > 10 * startPercent // 10:
655 print('%0.2f%% of Ivy nodes have grown' %
656 (IVY.maxLength / maxLength * 100))
657 startPercent += 10
658 if IVY.maxLength / maxLength > 1:
659 print("Halting Growth")
661 # Make an iterator to check if all are alive
662 checkAliveIter = (r.alive for r in IVY.ivyRoots)
664 # Create the curve and leaf geometry
665 createIvyGeometry(IVY, self.growLeaves)
666 print("Geometry Generation Complete")
668 print("Ivy generated in %0.2f s" % (time.time() - t))
670 self.updateIvy = False
672 return {'FINISHED'}
674 def draw(self, context):
675 layout = self.layout
677 layout.prop(self, 'updateIvy', icon='CURVE_DATA')
679 properties = layout.operator('curve.ivy_gen', text="Add New Ivy")
680 properties.randomSeed = self.randomSeed
681 properties.maxTime = self.maxTime
682 properties.maxIvyLength = self.maxIvyLength
683 properties.ivySize = self.ivySize
684 properties.maxFloatLength = self.maxFloatLength
685 properties.maxAdhesionDistance = self.maxAdhesionDistance
686 properties.primaryWeight = self.primaryWeight
687 properties.randomWeight = self.randomWeight
688 properties.gravityWeight = self.gravityWeight
689 properties.adhesionWeight = self.adhesionWeight
690 properties.branchingProbability = self.branchingProbability
691 properties.leafProbability = self.leafProbability
692 properties.ivyBranchSize = self.ivyBranchSize
693 properties.ivyLeafSize = self.ivyLeafSize
694 properties.updateIvy = True
696 prop_def = layout.operator('curve.ivy_gen', text="Add New Default Ivy")
697 prop_def.updateIvy = True
699 layout.prop(self, 'growLeaves')
701 box = layout.box()
702 box.label("Generation Settings:")
703 box.prop(self, 'randomSeed')
704 box.prop(self, 'maxTime')
706 box = layout.box()
707 box.label("Size Settings:")
708 box.prop(self, 'maxIvyLength')
709 box.prop(self, 'ivySize')
710 box.prop(self, 'maxFloatLength')
711 box.prop(self, 'maxAdhesionDistance')
713 box = layout.box()
714 box.label("Weight Settings:")
715 box.prop(self, 'primaryWeight')
716 box.prop(self, 'randomWeight')
717 box.prop(self, 'gravityWeight')
718 box.prop(self, 'adhesionWeight')
720 box = layout.box()
721 box.label("Branch Settings:")
722 box.prop(self, 'branchingProbability')
723 box.prop(self, 'ivyBranchSize')
725 if self.growLeaves:
726 box = layout.box()
727 box.label("Leaf Settings:")
728 box.prop(self, 'ivyLeafSize')
729 box.prop(self, 'leafProbability')
732 def menu_func(self, context):
733 self.layout.operator(IvyGen.bl_idname, text="Add Ivy to Mesh",
734 icon='OUTLINER_DATA_CURVE').updateIvy = True
737 def register():
738 bpy.utils.register_module(__name__)
739 bpy.types.INFO_MT_curve_add.append(menu_func)
742 def unregister():
743 bpy.types.INFO_MT_curve_add.remove(menu_func)
744 bpy.utils.unregister_module(__name__)
747 if __name__ == "__main__":
748 register()