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 #####
23 "author": "testscreenings, PKHG, TrumanBlending",
25 "blender": (2, 80, 0),
26 "location": "View3D > Sidebar > Ivy Generator (Create Tab)",
27 "description": "Adds generated ivy to a mesh object starting "
30 "wiki_url": "https://docs.blender.org/manual/en/dev/addons/"
31 "add_curve/ivy_gen.html",
32 "category": "Add Curve",
37 from bpy
.types
import (
42 from bpy
.props
import (
48 from mathutils
.bvhtree
import BVHTree
49 from mathutils
import (
53 from collections
import deque
65 def createIvyGeometry(IVY
, growLeaves
):
66 """Create the curve geometry for IVY"""
67 # Compute the local size and the gauss weight filter
68 # local_ivyBranchSize = IVY.ivyBranchSize # * radius * IVY.ivySize
69 gaussWeight
= (1.0, 2.0, 4.0, 7.0, 9.0, 10.0, 9.0, 7.0, 4.0, 2.0, 1.0)
71 # Create a new curve and initialise it
72 curve
= bpy
.data
.curves
.new("IVY", type='CURVE')
73 curve
.dimensions
= '3D'
75 curve
.fill_mode
= 'FULL'
76 curve
.resolution_u
= 4
79 # Create the ivy leaves
80 # Order location of the vertices
81 signList
= ((-1.0, +1.0),
88 # local_ivyLeafSize = IVY.ivyLeafSize # * radius * IVY.ivySize
90 # Initialise the vertex and face lists
93 # Store the methods for faster calling
94 addV
= vertList
.extend
95 rotMat
= Matrix
.Rotation
97 # Loop over all roots to generate its nodes
98 for root
in IVY
.ivyRoots
:
99 # Only grow if more than one node
100 numNodes
= len(root
.ivyNodes
)
102 # Calculate the local radius
103 local_ivyBranchRadius
= 1.0 / (root
.parents
+ 1) + 1.0
104 prevIvyLength
= 1.0 / root
.ivyNodes
[-1].length
105 splineVerts
= [ax
for n
in root
.ivyNodes
for ax
in n
.pos
.to_4d()]
107 radiusConstant
= local_ivyBranchRadius
* IVY
.ivyBranchSize
108 splineRadii
= [radiusConstant
* (1.3 - n
.length
* prevIvyLength
)
109 for n
in root
.ivyNodes
]
111 # Add the poly curve and set coords and radii
112 newSpline
= curve
.splines
.new(type='POLY')
113 newSpline
.points
.add(len(splineVerts
) // 4 - 1)
114 newSpline
.points
.foreach_set('co', splineVerts
)
115 newSpline
.points
.foreach_set('radius', splineRadii
)
117 # Loop over all nodes in the root
118 for i
, n
in enumerate(root
.ivyNodes
):
119 for k
in range(len(gaussWeight
)):
120 idx
= max(0, min(i
+ k
- 5, numNodes
- 1))
121 n
.smoothAdhesionVector
+= (gaussWeight
[k
] *
122 root
.ivyNodes
[idx
].adhesionVector
)
123 n
.smoothAdhesionVector
/= 56.0
124 n
.adhesionLength
= n
.smoothAdhesionVector
.length
125 n
.smoothAdhesionVector
.normalize()
127 if growLeaves
and (i
< numNodes
- 1):
128 node
= root
.ivyNodes
[i
]
129 nodeNext
= root
.ivyNodes
[i
+ 1]
131 # Find the weight and normalize the smooth adhesion vector
132 weight
= pow(node
.length
* prevIvyLength
, 0.7)
134 # Calculate the ground ivy and the new weight
135 groundIvy
= max(0.0, -node
.smoothAdhesionVector
.z
)
136 weight
+= groundIvy
* pow(1 - node
.length
*
139 # Find the alignment weight
140 alignmentWeight
= node
.adhesionLength
142 # Calculate the needed angles
143 phi
= atan2(node
.smoothAdhesionVector
.y
,
144 node
.smoothAdhesionVector
.x
) - pi
/ 2.0
147 node
.smoothAdhesionVector
.angle(Vector((0, 0, -1)), 0))
149 # Find the size weight
150 sizeWeight
= 1.5 - (cos(2 * pi
* weight
) * 0.5 + 0.5)
152 # Randomise the angles
153 phi
+= (rand_val() - 0.5) * (1.3 - alignmentWeight
)
154 theta
+= (rand_val() - 0.5) * (1.1 - alignmentWeight
)
156 # Calculate the leaf size an append the face to the list
157 leafSize
= IVY
.ivyLeafSize
* sizeWeight
160 # Generate the probability
161 probability
= rand_val()
163 # If we need to grow a leaf, do so
164 if (probability
* weight
) > IVY
.leafProbability
:
166 # Generate the random vector
167 randomVector
= Vector((rand_val() - 0.5,
172 # Find the leaf center
173 center
= (node
.pos
.lerp(nodeNext
.pos
, j
/ 10.0) +
174 IVY
.ivyLeafSize
* randomVector
)
176 # For each of the verts, rotate/scale and append
177 basisVecX
= Vector((1, 0, 0))
178 basisVecY
= Vector((0, 1, 0))
180 horiRot
= rotMat(theta
, 3, 'X')
181 vertRot
= rotMat(phi
, 3, 'Z')
183 basisVecX
.rotate(horiRot
)
184 basisVecY
.rotate(horiRot
)
186 basisVecX
.rotate(vertRot
)
187 basisVecY
.rotate(vertRot
)
189 basisVecX
*= leafSize
190 basisVecY
*= leafSize
192 addV([k1
* basisVecX
+ k2
* basisVecY
+ center
for
195 # Add the object and link to scene
196 newCurve
= bpy
.data
.objects
.new("IVY_Curve", curve
)
197 bpy
.context
.collection
.objects
.link(newCurve
)
200 faceList
= [[4 * i
+ l
for l
in range(4)] for i
in
201 range(len(vertList
) // 4)]
203 # Generate the new leaf mesh and link
204 me
= bpy
.data
.meshes
.new('IvyLeaf')
205 me
.from_pydata(vertList
, [], faceList
)
206 me
.update(calc_edges
=True)
207 ob
= bpy
.data
.objects
.new('IvyLeaf', me
)
208 bpy
.context
.collection
.objects
.link(ob
)
210 me
.uv_layers
.new(name
="Leaves")
212 # Set the uv texture coords
213 # TODO, this is non-functional, default uvs are ok?
216 uv1, uv2, uv3, uv4 = signList
223 """ The basic class used for each point on the ivy which is grown."""
224 __slots__
= ('pos', 'primaryDir', 'adhesionVector', 'adhesionLength',
225 'smoothAdhesionVector', 'length', 'floatingLength', 'climb')
228 self
.pos
= Vector((0, 0, 0))
229 self
.primaryDir
= Vector((0, 0, 1))
230 self
.adhesionVector
= Vector((0, 0, 0))
231 self
.smoothAdhesionVector
= Vector((0, 0, 0))
233 self
.floatingLength
= 0.0
238 """ The class used to hold all ivy nodes growing from this root point."""
239 __slots__
= ('ivyNodes', 'alive', 'parents')
242 self
.ivyNodes
= deque()
248 """ The class holding all parameters and ivy roots."""
249 __slots__
= ('ivyRoots', 'primaryWeight', 'randomWeight',
250 'gravityWeight', 'adhesionWeight', 'branchingProbability',
251 'leafProbability', 'ivySize', 'ivyLeafSize', 'ivyBranchSize',
252 'maxFloatLength', 'maxAdhesionDistance', 'maxLength')
259 branchingProbability
=0.05,
260 leafProbability
=0.35,
265 maxAdhesionDistance
=1.0):
267 self
.ivyRoots
= deque()
268 self
.primaryWeight
= primaryWeight
269 self
.randomWeight
= randomWeight
270 self
.gravityWeight
= gravityWeight
271 self
.adhesionWeight
= adhesionWeight
272 self
.branchingProbability
= 1 - branchingProbability
273 self
.leafProbability
= 1 - leafProbability
274 self
.ivySize
= ivySize
275 self
.ivyLeafSize
= ivyLeafSize
276 self
.ivyBranchSize
= ivyBranchSize
277 self
.maxFloatLength
= maxFloatLength
278 self
.maxAdhesionDistance
= maxAdhesionDistance
281 # Normalize all the weights only on initialisation
282 sums
= self
.primaryWeight
+ self
.randomWeight
+ self
.adhesionWeight
283 self
.primaryWeight
/= sums
284 self
.randomWeight
/= sums
285 self
.adhesionWeight
/= sums
287 def seed(self
, seedPos
):
288 # Seed the Ivy by making a new root and first node
293 tmpRoot
.ivyNodes
.append(tmpIvy
)
294 self
.ivyRoots
.append(tmpRoot
)
296 def grow(self
, ob
, bvhtree
):
297 # Determine the local sizes
298 # local_ivySize = self.ivySize # * radius
299 # local_maxFloatLength = self.maxFloatLength # * radius
300 # local_maxAdhesionDistance = self.maxAdhesionDistance # * radius
302 for root
in self
.ivyRoots
:
303 # Make sure the root is alive, if not, skip
307 # Get the last node in the current root
308 prevIvy
= root
.ivyNodes
[-1]
310 # If the node is floating for too long, kill the root
311 if prevIvy
.floatingLength
> self
.maxFloatLength
:
314 # Set the primary direction from the last node
315 primaryVector
= prevIvy
.primaryDir
317 # Make the random vector and normalize
318 randomVector
= Vector((rand_val() - 0.5, rand_val() - 0.5,
319 rand_val() - 0.5)) + Vector((0, 0, 0.2))
320 randomVector
.normalize()
322 # Calculate the adhesion vector
323 adhesionVector
= adhesion(
324 prevIvy
.pos
, bvhtree
, self
.maxAdhesionDistance
)
326 # Calculate the growing vector
327 growVector
= self
.ivySize
* (primaryVector
* self
.primaryWeight
+
328 randomVector
* self
.randomWeight
+
329 adhesionVector
* self
.adhesionWeight
)
331 # Find the gravity vector
332 gravityVector
= (self
.ivySize
* self
.gravityWeight
*
334 gravityVector
*= pow(prevIvy
.floatingLength
/ self
.maxFloatLength
,
337 # Determine the new position vector
338 newPos
= prevIvy
.pos
+ growVector
+ gravityVector
340 # Check for collisions with the object
341 climbing
, newPos
= collision(bvhtree
, prevIvy
.pos
, newPos
)
343 # Update the growing vector for any collisions
344 growVector
= newPos
- prevIvy
.pos
- gravityVector
345 growVector
.normalize()
347 # Create a new IvyNode and set its properties
349 tmpNode
.climb
= climbing
351 tmpNode
.primaryDir
= prevIvy
.primaryDir
.lerp(growVector
, 0.5)
352 tmpNode
.primaryDir
.normalize()
353 tmpNode
.adhesionVector
= adhesionVector
354 tmpNode
.length
= prevIvy
.length
+ (newPos
- prevIvy
.pos
).length
356 if tmpNode
.length
> self
.maxLength
:
357 self
.maxLength
= tmpNode
.length
359 # If the node isn't climbing, update it's floating length
360 # Otherwise set it to 0
362 tmpNode
.floatingLength
= prevIvy
.floatingLength
+ (newPos
-
365 tmpNode
.floatingLength
= 0.0
367 root
.ivyNodes
.append(tmpNode
)
369 # Loop through all roots to check if a new root is generated
370 for root
in self
.ivyRoots
:
371 # Check the root is alive and isn't at high level of recursion
372 if (root
.parents
> 3) or (not root
.alive
):
375 # Check to make sure there's more than 1 node
376 if len(root
.ivyNodes
) > 1:
377 # Loop through all nodes in root to check if new root is grown
378 for node
in root
.ivyNodes
:
379 # Set the last node of the root and find the weighting
380 prevIvy
= root
.ivyNodes
[-1]
381 weight
= 1.0 - (cos(2.0 * pi
* node
.length
/
382 prevIvy
.length
) * 0.5 + 0.5)
384 probability
= rand_val()
386 # Check if a new root is grown and if so, set its values
387 if (probability
* weight
> self
.branchingProbability
):
389 tmpNode
.pos
= node
.pos
390 tmpNode
.floatingLength
= node
.floatingLength
393 tmpRoot
.parents
= root
.parents
+ 1
395 tmpRoot
.ivyNodes
.append(tmpNode
)
396 self
.ivyRoots
.append(tmpRoot
)
400 def adhesion(loc
, bvhtree
, max_l
):
401 # Compute the adhesion vector by finding the nearest point
402 nearest_location
, *_
= bvhtree
.find_nearest(loc
, max_l
)
403 adhesion_vector
= Vector((0.0, 0.0, 0.0))
404 if nearest_location
is not None:
405 # Compute the distance to the nearest point
406 adhesion_vector
= nearest_location
- loc
407 distance
= adhesion_vector
.length
408 # If it's less than the maximum allowed and not 0, continue
410 # Compute the direction vector between the closest point and loc
411 adhesion_vector
.normalize()
412 adhesion_vector
*= 1.0 - distance
/ max_l
413 # adhesion_vector *= getFaceWeight(ob.data, nearest_result[3])
414 return adhesion_vector
417 def collision(bvhtree
, pos
, new_pos
):
418 # Check for collision with the object
421 corrected_new_pos
= new_pos
422 direction
= new_pos
- pos
424 hit_location
, hit_normal
, *_
= bvhtree
.ray_cast(pos
, direction
, direction
.length
)
425 # If there's a collision we need to check it
426 if hit_location
is not None:
427 # Check whether the collision is going into the object
428 if direction
.dot(hit_normal
) < 0.0:
429 reflected_direction
= (new_pos
- hit_location
).reflect(hit_normal
)
431 corrected_new_pos
= hit_location
+ reflected_direction
434 return climbing
, corrected_new_pos
437 def bvhtree_from_object(ob
):
441 depsgraph
= bpy
.context
.evaluated_depsgraph_get()
442 ob_eval
= ob
.evaluated_get(depsgraph
)
443 mesh
= ob_eval
.to_mesh()
445 bm
.transform(ob
.matrix_world
)
447 bvhtree
= BVHTree
.FromBMesh(bm
)
448 ob_eval
.to_mesh_clear()
451 def check_mesh_faces(ob
):
453 if len(me
.polygons
) > 0:
459 class IvyGen(Operator
):
460 bl_idname
= "curve.ivy_gen"
462 bl_description
= "Generate Ivy on an Mesh Object"
463 bl_options
= {'REGISTER', 'UNDO'}
465 updateIvy
: BoolProperty(
467 description
="Update the Ivy location based on the cursor and Panel settings",
470 defaultIvy
: BoolProperty(
472 options
={"HIDDEN", "SKIP_SAVE"},
477 def poll(self
, context
):
478 # Check if there's an object and whether it's a mesh
479 ob
= context
.active_object
480 return ((ob
is not None) and
481 (ob
.type == 'MESH') and
482 (context
.mode
== 'OBJECT'))
484 def invoke(self
, context
, event
):
485 self
.updateIvy
= True
486 return self
.execute(context
)
488 def execute(self
, context
):
489 # scene = context.scene
490 ivyProps
= context
.window_manager
.ivy_gen_props
492 if not self
.updateIvy
:
493 return {'PASS_THROUGH'}
495 # assign the variables, check if it is default
496 # Note: update the values if window_manager props defaults are changed
497 randomSeed
= ivyProps
.randomSeed
if not self
.defaultIvy
else 0
498 maxTime
= ivyProps
.maxTime
if not self
.defaultIvy
else 0
499 maxIvyLength
= ivyProps
.maxIvyLength
if not self
.defaultIvy
else 1.0
500 ivySize
= ivyProps
.ivySize
if not self
.defaultIvy
else 0.02
501 maxFloatLength
= ivyProps
.maxFloatLength
if not self
.defaultIvy
else 0.5
502 maxAdhesionDistance
= ivyProps
.maxAdhesionDistance
if not self
.defaultIvy
else 1.0
503 primaryWeight
= ivyProps
.primaryWeight
if not self
.defaultIvy
else 0.5
504 randomWeight
= ivyProps
.randomWeight
if not self
.defaultIvy
else 0.2
505 gravityWeight
= ivyProps
.gravityWeight
if not self
.defaultIvy
else 1.0
506 adhesionWeight
= ivyProps
.adhesionWeight
if not self
.defaultIvy
else 0.1
507 branchingProbability
= ivyProps
.branchingProbability
if not self
.defaultIvy
else 0.05
508 leafProbability
= ivyProps
.leafProbability
if not self
.defaultIvy
else 0.35
509 ivyBranchSize
= ivyProps
.ivyBranchSize
if not self
.defaultIvy
else 0.001
510 ivyLeafSize
= ivyProps
.ivyLeafSize
if not self
.defaultIvy
else 0.02
511 growLeaves
= ivyProps
.growLeaves
if not self
.defaultIvy
else True
513 bpy
.ops
.object.mode_set(mode
='EDIT', toggle
=False)
514 bpy
.ops
.object.mode_set(mode
='OBJECT', toggle
=False)
516 # Get the selected object
517 ob
= context
.active_object
518 bvhtree
= bvhtree_from_object(ob
)
520 # Check if the mesh has at least one polygon since some functions
521 # are expecting them in the object's data (see T51753)
522 check_face
= check_mesh_faces(ob
)
523 if check_face
is False:
524 self
.report({'WARNING'},
525 "Mesh Object doesn't have at least one Face. "
526 "Operation Cancelled")
529 # Compute bounding sphere radius
530 # radius = computeBoundingSphere(ob) # Not needed anymore
532 # Get the seeding point
533 seedPoint
= context
.scene
.cursor
.location
535 # Fix the random seed
536 rand_seed(randomSeed
)
540 primaryWeight
=primaryWeight
,
541 randomWeight
=randomWeight
,
542 gravityWeight
=gravityWeight
,
543 adhesionWeight
=adhesionWeight
,
544 branchingProbability
=branchingProbability
,
545 leafProbability
=leafProbability
,
547 ivyLeafSize
=ivyLeafSize
,
548 ivyBranchSize
=ivyBranchSize
,
549 maxFloatLength
=maxFloatLength
,
550 maxAdhesionDistance
=maxAdhesionDistance
552 # Generate first root and node
556 maxLength
= maxIvyLength
# * radius
558 # If we need to check time set the flag
564 checkAliveIter
= [True, ]
566 # Grow until 200 roots is reached or backup counter exceeds limit
567 while (any(checkAliveIter
) and
568 (IVY
.maxLength
< maxLength
) and
569 (not checkTime
or (time
.time() - t
< maxTime
))):
570 # Grow the ivy for this iteration
571 IVY
.grow(ob
, bvhtree
)
573 # Print the proportion of ivy growth to console
574 if (IVY
.maxLength
/ maxLength
* 100) > 10 * startPercent
// 10:
575 print('%0.2f%% of Ivy nodes have grown' %
576 (IVY
.maxLength
/ maxLength
* 100))
578 if IVY
.maxLength
/ maxLength
> 1:
579 print("Halting Growth")
581 # Make an iterator to check if all are alive
582 checkAliveIter
= (r
.alive
for r
in IVY
.ivyRoots
)
584 # Create the curve and leaf geometry
585 createIvyGeometry(IVY
, growLeaves
)
586 print("Geometry Generation Complete")
588 print("Ivy generated in %0.2f s" % (time
.time() - t
))
590 self
.updateIvy
= False
591 self
.defaultIvy
= False
595 def draw(self
, context
):
598 layout
.prop(self
, "updateIvy", icon
="FILE_REFRESH")
601 class CURVE_PT_IvyGenPanel(Panel
):
602 bl_label
= "Ivy Generator"
603 bl_idname
= "CURVE_PT_IvyGenPanel"
604 bl_space_type
= "VIEW_3D"
605 bl_region_type
= "UI"
606 bl_category
= "Create"
607 bl_context
= "objectmode"
608 bl_options
= {"DEFAULT_CLOSED"}
610 def draw(self
, context
):
612 wm
= context
.window_manager
613 col
= layout
.column(align
=True)
615 prop_new
= col
.operator("curve.ivy_gen", text
="Add New Ivy", icon
="OUTLINER_OB_CURVE")
616 prop_new
.defaultIvy
= False
617 prop_new
.updateIvy
= True
619 prop_def
= col
.operator("curve.ivy_gen", text
="Add New Default Ivy", icon
="CURVE_DATA")
620 prop_def
.defaultIvy
= True
621 prop_def
.updateIvy
= True
623 col
= layout
.column(align
=True)
624 col
.label(text
="Generation Settings:")
625 col
.prop(wm
.ivy_gen_props
, "randomSeed")
626 col
.prop(wm
.ivy_gen_props
, "maxTime")
628 col
= layout
.column(align
=True)
629 col
.label(text
="Size Settings:")
630 col
.prop(wm
.ivy_gen_props
, "maxIvyLength")
631 col
.prop(wm
.ivy_gen_props
, "ivySize")
632 col
.prop(wm
.ivy_gen_props
, "maxFloatLength")
633 col
.prop(wm
.ivy_gen_props
, "maxAdhesionDistance")
635 col
= layout
.column(align
=True)
636 col
.label(text
="Weight Settings:")
637 col
.prop(wm
.ivy_gen_props
, "primaryWeight")
638 col
.prop(wm
.ivy_gen_props
, "randomWeight")
639 col
.prop(wm
.ivy_gen_props
, "gravityWeight")
640 col
.prop(wm
.ivy_gen_props
, "adhesionWeight")
642 col
= layout
.column(align
=True)
643 col
.label(text
="Branch Settings:")
644 col
.prop(wm
.ivy_gen_props
, "branchingProbability")
645 col
.prop(wm
.ivy_gen_props
, "ivyBranchSize")
647 col
= layout
.column(align
=True)
648 col
.prop(wm
.ivy_gen_props
, "growLeaves")
650 if wm
.ivy_gen_props
.growLeaves
:
651 col
= layout
.column(align
=True)
652 col
.label(text
="Leaf Settings:")
653 col
.prop(wm
.ivy_gen_props
, "ivyLeafSize")
654 col
.prop(wm
.ivy_gen_props
, "leafProbability")
657 class IvyGenProperties(PropertyGroup
):
658 maxIvyLength
: FloatProperty(
659 name
="Max Ivy Length",
660 description
="Maximum ivy length in Blender Units",
667 primaryWeight
: FloatProperty(
668 name
="Primary Weight",
669 description
="Weighting given to the current direction",
674 randomWeight
: FloatProperty(
675 name
="Random Weight",
676 description
="Weighting given to the random direction",
681 gravityWeight
: FloatProperty(
682 name
="Gravity Weight",
683 description
="Weighting given to the gravity direction",
688 adhesionWeight
: FloatProperty(
689 name
="Adhesion Weight",
690 description
="Weighting given to the adhesion direction",
695 branchingProbability
: FloatProperty(
696 name
="Branching Probability",
697 description
="Probability of a new branch forming",
702 leafProbability
: FloatProperty(
703 name
="Leaf Probability",
704 description
="Probability of a leaf forming",
709 ivySize
: FloatProperty(
711 description
="The length of an ivy segment in Blender"
718 ivyLeafSize
: FloatProperty(
719 name
="Ivy Leaf Size",
720 description
="The size of the ivy leaves",
726 ivyBranchSize
: FloatProperty(
727 name
="Ivy Branch Size",
728 description
="The size of the ivy branches",
734 maxFloatLength
: FloatProperty(
735 name
="Max Float Length",
736 description
="The maximum distance that a branch "
737 "can live while floating",
742 maxAdhesionDistance
: FloatProperty(
743 name
="Max Adhesion Length",
744 description
="The maximum distance that a branch "
745 "will feel the effects of adhesion",
751 randomSeed
: IntProperty(
753 description
="The seed governing random generation",
757 maxTime
: FloatProperty(
759 description
="The maximum time to run the generation for "
760 "in seconds generation (0.0 = Disabled)",
765 growLeaves
: BoolProperty(
767 description
="Grow leaves or not",
781 bpy
.utils
.register_class(cls
)
783 bpy
.types
.WindowManager
.ivy_gen_props
= PointerProperty(
784 type=IvyGenProperties
789 del bpy
.types
.WindowManager
.ivy_gen_props
791 for cls
in reversed(classes
):
792 bpy
.utils
.unregister_class(cls
)
795 if __name__
== "__main__":