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 "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/ivy_gen.html",
31 "category": "Add Curve",
36 from bpy
.types
import (
41 from bpy
.props
import (
47 from mathutils
.bvhtree
import BVHTree
48 from mathutils
import (
52 from collections
import deque
64 def createIvyGeometry(IVY
, growLeaves
):
65 """Create the curve geometry for IVY"""
66 # Compute the local size and the gauss weight filter
67 # local_ivyBranchSize = IVY.ivyBranchSize # * radius * IVY.ivySize
68 gaussWeight
= (1.0, 2.0, 4.0, 7.0, 9.0, 10.0, 9.0, 7.0, 4.0, 2.0, 1.0)
70 # Create a new curve and initialise it
71 curve
= bpy
.data
.curves
.new("IVY", type='CURVE')
72 curve
.dimensions
= '3D'
74 curve
.fill_mode
= 'FULL'
75 curve
.resolution_u
= 4
78 # Create the ivy leaves
79 # Order location of the vertices
80 signList
= ((-1.0, +1.0),
87 # local_ivyLeafSize = IVY.ivyLeafSize # * radius * IVY.ivySize
89 # Initialise the vertex and face lists
92 # Store the methods for faster calling
93 addV
= vertList
.extend
94 rotMat
= Matrix
.Rotation
96 # Loop over all roots to generate its nodes
97 for root
in IVY
.ivyRoots
:
98 # Only grow if more than one node
99 numNodes
= len(root
.ivyNodes
)
101 # Calculate the local radius
102 local_ivyBranchRadius
= 1.0 / (root
.parents
+ 1) + 1.0
103 prevIvyLength
= 1.0 / root
.ivyNodes
[-1].length
104 splineVerts
= [ax
for n
in root
.ivyNodes
for ax
in n
.pos
.to_4d()]
106 radiusConstant
= local_ivyBranchRadius
* IVY
.ivyBranchSize
107 splineRadii
= [radiusConstant
* (1.3 - n
.length
* prevIvyLength
)
108 for n
in root
.ivyNodes
]
110 # Add the poly curve and set coords and radii
111 newSpline
= curve
.splines
.new(type='POLY')
112 newSpline
.points
.add(len(splineVerts
) // 4 - 1)
113 newSpline
.points
.foreach_set('co', splineVerts
)
114 newSpline
.points
.foreach_set('radius', splineRadii
)
116 # Loop over all nodes in the root
117 for i
, n
in enumerate(root
.ivyNodes
):
118 for k
in range(len(gaussWeight
)):
119 idx
= max(0, min(i
+ k
- 5, numNodes
- 1))
120 n
.smoothAdhesionVector
+= (gaussWeight
[k
] *
121 root
.ivyNodes
[idx
].adhesionVector
)
122 n
.smoothAdhesionVector
/= 56.0
123 n
.adhesionLength
= n
.smoothAdhesionVector
.length
124 n
.smoothAdhesionVector
.normalize()
126 if growLeaves
and (i
< numNodes
- 1):
127 node
= root
.ivyNodes
[i
]
128 nodeNext
= root
.ivyNodes
[i
+ 1]
130 # Find the weight and normalize the smooth adhesion vector
131 weight
= pow(node
.length
* prevIvyLength
, 0.7)
133 # Calculate the ground ivy and the new weight
134 groundIvy
= max(0.0, -node
.smoothAdhesionVector
.z
)
135 weight
+= groundIvy
* pow(1 - node
.length
*
138 # Find the alignment weight
139 alignmentWeight
= node
.adhesionLength
141 # Calculate the needed angles
142 phi
= atan2(node
.smoothAdhesionVector
.y
,
143 node
.smoothAdhesionVector
.x
) - pi
/ 2.0
146 node
.smoothAdhesionVector
.angle(Vector((0, 0, -1)), 0))
148 # Find the size weight
149 sizeWeight
= 1.5 - (cos(2 * pi
* weight
) * 0.5 + 0.5)
151 # Randomise the angles
152 phi
+= (rand_val() - 0.5) * (1.3 - alignmentWeight
)
153 theta
+= (rand_val() - 0.5) * (1.1 - alignmentWeight
)
155 # Calculate the leaf size an append the face to the list
156 leafSize
= IVY
.ivyLeafSize
* sizeWeight
159 # Generate the probability
160 probability
= rand_val()
162 # If we need to grow a leaf, do so
163 if (probability
* weight
) > IVY
.leafProbability
:
165 # Generate the random vector
166 randomVector
= Vector((rand_val() - 0.5,
171 # Find the leaf center
172 center
= (node
.pos
.lerp(nodeNext
.pos
, j
/ 10.0) +
173 IVY
.ivyLeafSize
* randomVector
)
175 # For each of the verts, rotate/scale and append
176 basisVecX
= Vector((1, 0, 0))
177 basisVecY
= Vector((0, 1, 0))
179 horiRot
= rotMat(theta
, 3, 'X')
180 vertRot
= rotMat(phi
, 3, 'Z')
182 basisVecX
.rotate(horiRot
)
183 basisVecY
.rotate(horiRot
)
185 basisVecX
.rotate(vertRot
)
186 basisVecY
.rotate(vertRot
)
188 basisVecX
*= leafSize
189 basisVecY
*= leafSize
191 addV([k1
* basisVecX
+ k2
* basisVecY
+ center
for
194 # Add the object and link to scene
195 newCurve
= bpy
.data
.objects
.new("IVY_Curve", curve
)
196 bpy
.context
.collection
.objects
.link(newCurve
)
199 faceList
= [[4 * i
+ l
for l
in range(4)] for i
in
200 range(len(vertList
) // 4)]
202 # Generate the new leaf mesh and link
203 me
= bpy
.data
.meshes
.new('IvyLeaf')
204 me
.from_pydata(vertList
, [], faceList
)
205 me
.update(calc_edges
=True)
206 ob
= bpy
.data
.objects
.new('IvyLeaf', me
)
207 bpy
.context
.collection
.objects
.link(ob
)
209 me
.uv_layers
.new(name
="Leaves")
211 # Set the uv texture coords
212 # TODO, this is non-functional, default uvs are ok?
215 uv1, uv2, uv3, uv4 = signList
222 """ The basic class used for each point on the ivy which is grown."""
223 __slots__
= ('pos', 'primaryDir', 'adhesionVector', 'adhesionLength',
224 'smoothAdhesionVector', 'length', 'floatingLength', 'climb')
227 self
.pos
= Vector((0, 0, 0))
228 self
.primaryDir
= Vector((0, 0, 1))
229 self
.adhesionVector
= Vector((0, 0, 0))
230 self
.smoothAdhesionVector
= Vector((0, 0, 0))
232 self
.floatingLength
= 0.0
237 """ The class used to hold all ivy nodes growing from this root point."""
238 __slots__
= ('ivyNodes', 'alive', 'parents')
241 self
.ivyNodes
= deque()
247 """ The class holding all parameters and ivy roots."""
248 __slots__
= ('ivyRoots', 'primaryWeight', 'randomWeight',
249 'gravityWeight', 'adhesionWeight', 'branchingProbability',
250 'leafProbability', 'ivySize', 'ivyLeafSize', 'ivyBranchSize',
251 'maxFloatLength', 'maxAdhesionDistance', 'maxLength')
258 branchingProbability
=0.05,
259 leafProbability
=0.35,
264 maxAdhesionDistance
=1.0):
266 self
.ivyRoots
= deque()
267 self
.primaryWeight
= primaryWeight
268 self
.randomWeight
= randomWeight
269 self
.gravityWeight
= gravityWeight
270 self
.adhesionWeight
= adhesionWeight
271 self
.branchingProbability
= 1 - branchingProbability
272 self
.leafProbability
= 1 - leafProbability
273 self
.ivySize
= ivySize
274 self
.ivyLeafSize
= ivyLeafSize
275 self
.ivyBranchSize
= ivyBranchSize
276 self
.maxFloatLength
= maxFloatLength
277 self
.maxAdhesionDistance
= maxAdhesionDistance
280 # Normalize all the weights only on initialisation
281 sums
= self
.primaryWeight
+ self
.randomWeight
+ self
.adhesionWeight
282 self
.primaryWeight
/= sums
283 self
.randomWeight
/= sums
284 self
.adhesionWeight
/= sums
286 def seed(self
, seedPos
):
287 # Seed the Ivy by making a new root and first node
292 tmpRoot
.ivyNodes
.append(tmpIvy
)
293 self
.ivyRoots
.append(tmpRoot
)
295 def grow(self
, ob
, bvhtree
):
296 # Determine the local sizes
297 # local_ivySize = self.ivySize # * radius
298 # local_maxFloatLength = self.maxFloatLength # * radius
299 # local_maxAdhesionDistance = self.maxAdhesionDistance # * radius
301 for root
in self
.ivyRoots
:
302 # Make sure the root is alive, if not, skip
306 # Get the last node in the current root
307 prevIvy
= root
.ivyNodes
[-1]
309 # If the node is floating for too long, kill the root
310 if prevIvy
.floatingLength
> self
.maxFloatLength
:
313 # Set the primary direction from the last node
314 primaryVector
= prevIvy
.primaryDir
316 # Make the random vector and normalize
317 randomVector
= Vector((rand_val() - 0.5, rand_val() - 0.5,
318 rand_val() - 0.5)) + Vector((0, 0, 0.2))
319 randomVector
.normalize()
321 # Calculate the adhesion vector
322 adhesionVector
= adhesion(
323 prevIvy
.pos
, bvhtree
, self
.maxAdhesionDistance
)
325 # Calculate the growing vector
326 growVector
= self
.ivySize
* (primaryVector
* self
.primaryWeight
+
327 randomVector
* self
.randomWeight
+
328 adhesionVector
* self
.adhesionWeight
)
330 # Find the gravity vector
331 gravityVector
= (self
.ivySize
* self
.gravityWeight
*
333 gravityVector
*= pow(prevIvy
.floatingLength
/ self
.maxFloatLength
,
336 # Determine the new position vector
337 newPos
= prevIvy
.pos
+ growVector
+ gravityVector
339 # Check for collisions with the object
340 climbing
, newPos
= collision(bvhtree
, prevIvy
.pos
, newPos
)
342 # Update the growing vector for any collisions
343 growVector
= newPos
- prevIvy
.pos
- gravityVector
344 growVector
.normalize()
346 # Create a new IvyNode and set its properties
348 tmpNode
.climb
= climbing
350 tmpNode
.primaryDir
= prevIvy
.primaryDir
.lerp(growVector
, 0.5)
351 tmpNode
.primaryDir
.normalize()
352 tmpNode
.adhesionVector
= adhesionVector
353 tmpNode
.length
= prevIvy
.length
+ (newPos
- prevIvy
.pos
).length
355 if tmpNode
.length
> self
.maxLength
:
356 self
.maxLength
= tmpNode
.length
358 # If the node isn't climbing, update it's floating length
359 # Otherwise set it to 0
361 tmpNode
.floatingLength
= prevIvy
.floatingLength
+ (newPos
-
364 tmpNode
.floatingLength
= 0.0
366 root
.ivyNodes
.append(tmpNode
)
368 # Loop through all roots to check if a new root is generated
369 for root
in self
.ivyRoots
:
370 # Check the root is alive and isn't at high level of recursion
371 if (root
.parents
> 3) or (not root
.alive
):
374 # Check to make sure there's more than 1 node
375 if len(root
.ivyNodes
) > 1:
376 # Loop through all nodes in root to check if new root is grown
377 for node
in root
.ivyNodes
:
378 # Set the last node of the root and find the weighting
379 prevIvy
= root
.ivyNodes
[-1]
380 weight
= 1.0 - (cos(2.0 * pi
* node
.length
/
381 prevIvy
.length
) * 0.5 + 0.5)
383 probability
= rand_val()
385 # Check if a new root is grown and if so, set its values
386 if (probability
* weight
> self
.branchingProbability
):
388 tmpNode
.pos
= node
.pos
389 tmpNode
.floatingLength
= node
.floatingLength
392 tmpRoot
.parents
= root
.parents
+ 1
394 tmpRoot
.ivyNodes
.append(tmpNode
)
395 self
.ivyRoots
.append(tmpRoot
)
399 def adhesion(loc
, bvhtree
, max_l
):
400 # Compute the adhesion vector by finding the nearest point
401 nearest_location
, *_
= bvhtree
.find_nearest(loc
, max_l
)
402 adhesion_vector
= Vector((0.0, 0.0, 0.0))
403 if nearest_location
is not None:
404 # Compute the distance to the nearest point
405 adhesion_vector
= nearest_location
- loc
406 distance
= adhesion_vector
.length
407 # If it's less than the maximum allowed and not 0, continue
409 # Compute the direction vector between the closest point and loc
410 adhesion_vector
.normalize()
411 adhesion_vector
*= 1.0 - distance
/ max_l
412 # adhesion_vector *= getFaceWeight(ob.data, nearest_result[3])
413 return adhesion_vector
416 def collision(bvhtree
, pos
, new_pos
):
417 # Check for collision with the object
420 corrected_new_pos
= new_pos
421 direction
= new_pos
- pos
423 hit_location
, hit_normal
, *_
= bvhtree
.ray_cast(pos
, direction
, direction
.length
)
424 # If there's a collision we need to check it
425 if hit_location
is not None:
426 # Check whether the collision is going into the object
427 if direction
.dot(hit_normal
) < 0.0:
428 reflected_direction
= (new_pos
- hit_location
).reflect(hit_normal
)
430 corrected_new_pos
= hit_location
+ reflected_direction
433 return climbing
, corrected_new_pos
436 def bvhtree_from_object(ob
):
440 depsgraph
= bpy
.context
.evaluated_depsgraph_get()
441 ob_eval
= ob
.evaluated_get(depsgraph
)
442 mesh
= ob_eval
.to_mesh()
444 bm
.transform(ob
.matrix_world
)
446 bvhtree
= BVHTree
.FromBMesh(bm
)
447 ob_eval
.to_mesh_clear()
450 def check_mesh_faces(ob
):
452 if len(me
.polygons
) > 0:
458 class IvyGen(Operator
):
459 bl_idname
= "curve.ivy_gen"
461 bl_description
= "Generate Ivy on an Mesh Object"
462 bl_options
= {'REGISTER', 'UNDO'}
464 updateIvy
: BoolProperty(
466 description
="Update the Ivy location based on the cursor and Panel settings",
469 defaultIvy
: BoolProperty(
471 options
={"HIDDEN", "SKIP_SAVE"},
476 def poll(self
, context
):
477 # Check if there's an object and whether it's a mesh
478 ob
= context
.active_object
479 return ((ob
is not None) and
480 (ob
.type == 'MESH') and
481 (context
.mode
== 'OBJECT'))
483 def invoke(self
, context
, event
):
484 self
.updateIvy
= True
485 return self
.execute(context
)
487 def execute(self
, context
):
488 # scene = context.scene
489 ivyProps
= context
.window_manager
.ivy_gen_props
491 if not self
.updateIvy
:
492 return {'PASS_THROUGH'}
494 # assign the variables, check if it is default
495 # Note: update the values if window_manager props defaults are changed
496 randomSeed
= ivyProps
.randomSeed
if not self
.defaultIvy
else 0
497 maxTime
= ivyProps
.maxTime
if not self
.defaultIvy
else 0
498 maxIvyLength
= ivyProps
.maxIvyLength
if not self
.defaultIvy
else 1.0
499 ivySize
= ivyProps
.ivySize
if not self
.defaultIvy
else 0.02
500 maxFloatLength
= ivyProps
.maxFloatLength
if not self
.defaultIvy
else 0.5
501 maxAdhesionDistance
= ivyProps
.maxAdhesionDistance
if not self
.defaultIvy
else 1.0
502 primaryWeight
= ivyProps
.primaryWeight
if not self
.defaultIvy
else 0.5
503 randomWeight
= ivyProps
.randomWeight
if not self
.defaultIvy
else 0.2
504 gravityWeight
= ivyProps
.gravityWeight
if not self
.defaultIvy
else 1.0
505 adhesionWeight
= ivyProps
.adhesionWeight
if not self
.defaultIvy
else 0.1
506 branchingProbability
= ivyProps
.branchingProbability
if not self
.defaultIvy
else 0.05
507 leafProbability
= ivyProps
.leafProbability
if not self
.defaultIvy
else 0.35
508 ivyBranchSize
= ivyProps
.ivyBranchSize
if not self
.defaultIvy
else 0.001
509 ivyLeafSize
= ivyProps
.ivyLeafSize
if not self
.defaultIvy
else 0.02
510 growLeaves
= ivyProps
.growLeaves
if not self
.defaultIvy
else True
512 bpy
.ops
.object.mode_set(mode
='EDIT', toggle
=False)
513 bpy
.ops
.object.mode_set(mode
='OBJECT', toggle
=False)
515 # Get the selected object
516 ob
= context
.active_object
517 bvhtree
= bvhtree_from_object(ob
)
519 # Check if the mesh has at least one polygon since some functions
520 # are expecting them in the object's data (see T51753)
521 check_face
= check_mesh_faces(ob
)
522 if check_face
is False:
523 self
.report({'WARNING'},
524 "Mesh Object doesn't have at least one Face. "
525 "Operation Cancelled")
528 # Compute bounding sphere radius
529 # radius = computeBoundingSphere(ob) # Not needed anymore
531 # Get the seeding point
532 seedPoint
= context
.scene
.cursor
.location
534 # Fix the random seed
535 rand_seed(randomSeed
)
539 primaryWeight
=primaryWeight
,
540 randomWeight
=randomWeight
,
541 gravityWeight
=gravityWeight
,
542 adhesionWeight
=adhesionWeight
,
543 branchingProbability
=branchingProbability
,
544 leafProbability
=leafProbability
,
546 ivyLeafSize
=ivyLeafSize
,
547 ivyBranchSize
=ivyBranchSize
,
548 maxFloatLength
=maxFloatLength
,
549 maxAdhesionDistance
=maxAdhesionDistance
551 # Generate first root and node
555 maxLength
= maxIvyLength
# * radius
557 # If we need to check time set the flag
563 checkAliveIter
= [True, ]
565 # Grow until 200 roots is reached or backup counter exceeds limit
566 while (any(checkAliveIter
) and
567 (IVY
.maxLength
< maxLength
) and
568 (not checkTime
or (time
.time() - t
< maxTime
))):
569 # Grow the ivy for this iteration
570 IVY
.grow(ob
, bvhtree
)
572 # Print the proportion of ivy growth to console
573 if (IVY
.maxLength
/ maxLength
* 100) > 10 * startPercent
// 10:
574 print('%0.2f%% of Ivy nodes have grown' %
575 (IVY
.maxLength
/ maxLength
* 100))
577 if IVY
.maxLength
/ maxLength
> 1:
578 print("Halting Growth")
580 # Make an iterator to check if all are alive
581 checkAliveIter
= (r
.alive
for r
in IVY
.ivyRoots
)
583 # Create the curve and leaf geometry
584 createIvyGeometry(IVY
, growLeaves
)
585 print("Geometry Generation Complete")
587 print("Ivy generated in %0.2f s" % (time
.time() - t
))
589 self
.updateIvy
= False
590 self
.defaultIvy
= False
594 def draw(self
, context
):
597 layout
.prop(self
, "updateIvy", icon
="FILE_REFRESH")
600 class CURVE_PT_IvyGenPanel(Panel
):
601 bl_label
= "Ivy Generator"
602 bl_idname
= "CURVE_PT_IvyGenPanel"
603 bl_space_type
= "VIEW_3D"
604 bl_region_type
= "UI"
605 bl_category
= "Create"
606 bl_context
= "objectmode"
607 bl_options
= {"DEFAULT_CLOSED"}
609 def draw(self
, context
):
611 wm
= context
.window_manager
612 col
= layout
.column(align
=True)
614 prop_new
= col
.operator("curve.ivy_gen", text
="Add New Ivy", icon
="OUTLINER_OB_CURVE")
615 prop_new
.defaultIvy
= False
616 prop_new
.updateIvy
= True
618 prop_def
= col
.operator("curve.ivy_gen", text
="Add New Default Ivy", icon
="CURVE_DATA")
619 prop_def
.defaultIvy
= True
620 prop_def
.updateIvy
= True
622 col
= layout
.column(align
=True)
623 col
.label(text
="Generation Settings:")
624 col
.prop(wm
.ivy_gen_props
, "randomSeed")
625 col
.prop(wm
.ivy_gen_props
, "maxTime")
627 col
= layout
.column(align
=True)
628 col
.label(text
="Size Settings:")
629 col
.prop(wm
.ivy_gen_props
, "maxIvyLength")
630 col
.prop(wm
.ivy_gen_props
, "ivySize")
631 col
.prop(wm
.ivy_gen_props
, "maxFloatLength")
632 col
.prop(wm
.ivy_gen_props
, "maxAdhesionDistance")
634 col
= layout
.column(align
=True)
635 col
.label(text
="Weight Settings:")
636 col
.prop(wm
.ivy_gen_props
, "primaryWeight")
637 col
.prop(wm
.ivy_gen_props
, "randomWeight")
638 col
.prop(wm
.ivy_gen_props
, "gravityWeight")
639 col
.prop(wm
.ivy_gen_props
, "adhesionWeight")
641 col
= layout
.column(align
=True)
642 col
.label(text
="Branch Settings:")
643 col
.prop(wm
.ivy_gen_props
, "branchingProbability")
644 col
.prop(wm
.ivy_gen_props
, "ivyBranchSize")
646 col
= layout
.column(align
=True)
647 col
.prop(wm
.ivy_gen_props
, "growLeaves")
649 if wm
.ivy_gen_props
.growLeaves
:
650 col
= layout
.column(align
=True)
651 col
.label(text
="Leaf Settings:")
652 col
.prop(wm
.ivy_gen_props
, "ivyLeafSize")
653 col
.prop(wm
.ivy_gen_props
, "leafProbability")
656 class IvyGenProperties(PropertyGroup
):
657 maxIvyLength
: FloatProperty(
658 name
="Max Ivy Length",
659 description
="Maximum ivy length in Blender Units",
666 primaryWeight
: FloatProperty(
667 name
="Primary Weight",
668 description
="Weighting given to the current direction",
673 randomWeight
: FloatProperty(
674 name
="Random Weight",
675 description
="Weighting given to the random direction",
680 gravityWeight
: FloatProperty(
681 name
="Gravity Weight",
682 description
="Weighting given to the gravity direction",
687 adhesionWeight
: FloatProperty(
688 name
="Adhesion Weight",
689 description
="Weighting given to the adhesion direction",
694 branchingProbability
: FloatProperty(
695 name
="Branching Probability",
696 description
="Probability of a new branch forming",
701 leafProbability
: FloatProperty(
702 name
="Leaf Probability",
703 description
="Probability of a leaf forming",
708 ivySize
: FloatProperty(
710 description
="The length of an ivy segment in Blender"
717 ivyLeafSize
: FloatProperty(
718 name
="Ivy Leaf Size",
719 description
="The size of the ivy leaves",
725 ivyBranchSize
: FloatProperty(
726 name
="Ivy Branch Size",
727 description
="The size of the ivy branches",
733 maxFloatLength
: FloatProperty(
734 name
="Max Float Length",
735 description
="The maximum distance that a branch "
736 "can live while floating",
741 maxAdhesionDistance
: FloatProperty(
742 name
="Max Adhesion Length",
743 description
="The maximum distance that a branch "
744 "will feel the effects of adhesion",
750 randomSeed
: IntProperty(
752 description
="The seed governing random generation",
756 maxTime
: FloatProperty(
758 description
="The maximum time to run the generation for "
759 "in seconds generation (0.0 = Disabled)",
764 growLeaves
: BoolProperty(
766 description
="Grow leaves or not",
780 bpy
.utils
.register_class(cls
)
782 bpy
.types
.WindowManager
.ivy_gen_props
= PointerProperty(
783 type=IvyGenProperties
788 del bpy
.types
.WindowManager
.ivy_gen_props
790 for cls
in reversed(classes
):
791 bpy
.utils
.unregister_class(cls
)
794 if __name__
== "__main__":