IVY addon small changes

Hi @JuhaW :slight_smile:

This dictionnary works like a charm !!!
It’s less convenient for dev and debugging purpose but is much more convenient for use purpose by not flooding the custom properties.
I wish there were in python defines like in C or C++ allowing for removing the [“IVY”] from code and revealing all the props.
Of course this can be done in active code but will stuff code with useless things.

I adopt your dictionnary feature :smiley:

So here’s the final feature:
Ivy creation with square leaves and the same one with complex mesh leaves


here’s the code to set up like explained a bit higher:

# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

# <pep8-80 compliant>

bl_info = {
    "name": "IvyGen",
    "author": "testscreenings, PKHG, TrumanBlending, some Blenderartists users",
    "version": (0, 1, 6),
    "blender": (2, 80, 0),
    "location": "View3D > Sidebar > Ivy Generator (Create Tab)",
    "description": "Adds generated ivy to a mesh object starting "
                   "at the 3D cursor",
    "warning": "",
    "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/ivy_gen.html",
    "category": "Add Curve",
}


import bpy
from bpy.types import (
    Operator,
    Panel,
    PropertyGroup,
)
from bpy.props import (
    BoolProperty,
    FloatProperty,
    IntProperty,
    PointerProperty,
    CollectionProperty,
)
from mathutils.bvhtree import BVHTree
from mathutils import (
    Euler,
    Vector,
    Matrix,
)
from collections import deque
from math import (
    pow, cos,
    pi, atan2,
    radians,
)
from random import (
    random as rand_val,
    seed as rand_seed,
)
import time
import mathutils
import random
from rna_prop_ui import rna_idprop_ui_prop_get


#====================================================
#=                                                  =
#= Adds a custom property to an object              =
#=                                                  =
#= IN: - object to add the property to
#=     - property name
#=     - property value
#=                                                  
#= OUT: nothing
#=                                                  =
#====================================================
def AddIntPropertyToObject(object, propertyName:str, propertyVal:int, toolTip:str="DO NOT DELETE THIS INT") -> None:
    object["IVY"][propertyName] = propertyVal
    ui = rna_idprop_ui_prop_get(object,propertyName, create=True)
    ui['description'] = toolTip      
def AddFloatPropertyToObject(object, propertyName:str, propertyVal:float, toolTip:str="DO NOT DELETE THIS FLOAT") -> None:
    object["IVY"][propertyName] = propertyVal
    ui = rna_idprop_ui_prop_get(object,propertyName, create=True)
    ui['description'] = toolTip      
def AddStringPropertyToObject(object, propertyName:str, propertyVal:str, toolTip:str="DO NOT DELETE THIS STRING") -> None:
    object["IVY"][propertyName] = propertyVal
    ui = rna_idprop_ui_prop_get(object,propertyName, create=True)
    ui['description'] = toolTip
def AddBoolPropertyToObject(object, propertyName:str, propertyVal:bool, toolTip:str="DO NOT DELETE THIS BOOL") -> None:
    object["IVY"][propertyName] = propertyVal
    ui = rna_idprop_ui_prop_get(object,propertyName, create=True)
    ui['description'] = toolTip
    # python is such a morons shit that it don't support function overloading ! LOOOOOL !!!!!! let's get back to 70's
def AddVectorPropertyToObject(object, propertyName:str, propertyVal:Vector, toolTip:str="DO NOT DELETE THIS VECTOR") -> None:
    object["IVY"][propertyName] = propertyVal
    ui = rna_idprop_ui_prop_get(object,propertyName, create=True)
    ui['description'] = toolTip
    
def AddCollectionPropertyToObject(object, propertyName:str, propertyVal, toolTip:str="DO NOT DELETE THIS COLLECTION") -> None:
    object["IVY"][propertyName] = propertyVal
    ui = rna_idprop_ui_prop_get(object,propertyName, create=True)
    ui['description'] = toolTip      
    
    
#========================================================================================================    



def createIvyGeometry(IVY, growLeaves:bool, collection4Leaves, growingSupportObject):
    
    
    
    """Create the curve geometry for IVY"""
    # Compute the local size and the gauss weight filter
    # local_ivyBranchSize = IVY.ivyBranchSize  # * radius * IVY.ivySize
    gaussWeight = (1.0, 2.0, 4.0, 7.0, 9.0, 10.0, 9.0, 7.0, 4.0, 2.0, 1.0)

    # Create a new curve and initialise it
    curve = bpy.data.curves.new("IVY", type='CURVE')
    curve.dimensions = '3D'
    curve.bevel_depth = 1
    curve.fill_mode = 'FULL'
    curve.resolution_u = 4
    
    prevSelectedObject = bpy.context.selected_objects
    #print("sel=",prevSelectedObject)
    prevActiveObject = bpy.context.active_object
    #    print("act=",prevActiveObject)
    
    
    # deal with the collections to avoid mess.....
    # 1st get the growingSupportObject collection:
    targetCollection = growingSupportObject.users_collection[0]

        
    # check wether we should use a collection for leaves or not ?
    collection4LeavesSize = 0
    useCollectionForLeaves:bool = False
    if(collection4Leaves is not None):
        useCollectionForLeaves = True
        collection4LeavesSize = len(collection4Leaves.objects) - 1
    
    # process is quite slow and deserves the work of python experts....
    # so limit the amount of leaves at dev time
    max_leaves=300000
    

    if(growLeaves):
        
        # create a leaves mesh and object
        leavesMesh = bpy.data.meshes.new('Msh_IvyLeaf')
        leavesObject = bpy.data.objects.new('IvyLeaf',leavesMesh)
        targetCollection.objects.link(leavesObject)

        
        rotMat = Matrix.Rotation
        
        if(useCollectionForLeaves==False):
            # Create the ivy leaves
            # Order location of the vertices
            signList = ((-1.0, +1.0),
                        (-1.0, -1.0),
                        (+1.0, -1.0),
                        (+1.0, +1.0),)

            # Initialise the vertex and face lists
            LeavesVertList = deque()

            # Store the methods for faster calling
            addV = LeavesVertList.extend
        else:
            # we got to deselect all and make the leaves object active as we'll have to join objects
            for ooo in prevSelectedObject:
                ooo.select_set(False)
            if(prevActiveObject is not None):
                prevActiveObject.select_set(False)
            
            # select and activate the leaves object as we will join all instanced leaves to it...
            leavesObject.select_set(True)
            bpy.context.view_layer.objects.active = leavesObject
            
                    

    # Loop over all roots to generate its nodes
    for root in IVY.ivyRoots:
        # Only grow if more than one node
        numNodes = len(root.ivyNodes)
        if numNodes > 1:
            # Calculate the local radius
            local_ivyBranchRadius = 1.0 / (root.parents + 1) # Cyril  +1.0
            prevIvyLength = 1.0 / root.ivyNodes[-1].length
            splineVerts = [ax for n in root.ivyNodes for ax in n.pos.to_4d()]

            radiusConstant = local_ivyBranchRadius * IVY.ivyBranchSize
            splineRadii = [radiusConstant * (1.3 - n.length * prevIvyLength)
                           for n in root.ivyNodes]

            # Add the poly curve and set coords and radii
            newSpline = curve.splines.new(type='POLY')
            newSpline.points.add(len(splineVerts) // 4 - 1)
            newSpline.points.foreach_set('co', splineVerts)
            newSpline.points.foreach_set('radius', splineRadii)

            # Loop over all nodes in the root
            for i, n in enumerate(root.ivyNodes):
                for k in range(len(gaussWeight)):
                    idx = max(0, min(i + k - 5, numNodes - 1))
                    n.smoothAdhesionVector += (gaussWeight[k] *
                                               root.ivyNodes[idx].adhesionVector)
                n.smoothAdhesionVector /= 56.0
                n.adhesionLength = n.smoothAdhesionVector.length
                n.smoothAdhesionVector.normalize()

                if growLeaves and (i < numNodes - 1):
                    node = root.ivyNodes[i]
                    nodeNext = root.ivyNodes[i + 1]

                    # Find the weight and normalize the smooth adhesion vector
                    weight = pow(node.length * prevIvyLength, 0.7)

                    # Calculate the ground ivy and the new weight
                    groundIvy = max(0.0, -node.smoothAdhesionVector.z)
                    weight += groundIvy * pow(1 - node.length *
                                                              prevIvyLength, 2)

                    # Find the alignment weight
                    alignmentWeight = node.adhesionLength

                    # Calculate the needed angles
                    phi = atan2(node.smoothAdhesionVector.y,
                                node.smoothAdhesionVector.x) - pi / 2.0

                    theta = (0.5 *
                        node.smoothAdhesionVector.angle(Vector((0, 0, -1)), 0))

                    # Find the size weight
                    sizeWeight = 1.5 - (cos(2 * pi * weight) * 0.5 + 0.5)

                    # Randomise the angles
                    phi += (rand_val() - 0.5) * (1.3 - alignmentWeight)
                    theta += (rand_val() - 0.5) * (1.1 - alignmentWeight)

                    # Calculate the leaf size an append the face to the list
                    leafSize = IVY.ivyLeafSize * sizeWeight

                    for j in range(10):
                        # Generate the probability
                        probability = rand_val()

                        # If we need to grow a leaf, do so
                        # cyril if (probability * weight) > IVY.leafProbability:
                        if (probability * (1.0-IVY.leafDensityFromHeight*(1.0-weight))) > IVY.leafProbability:

                            # Generate the random vector
                            #randomVector = Vector((rand_val()-0.5,rand_val()-0.5,rand_val()-0.5,))
                            # no randomness for leaves origin !!!!
                            randomVector = Vector((0,0,0))

                            # Find the leaf center
                            center = (node.pos.lerp(nodeNext.pos, j / 10.0) +
                                               IVY.ivyLeafSize * randomVector)

                            # For each of the verts, rotate/scale and append
                            basisVecX = Vector((1, 0, 0))
                            basisVecY = Vector((0, 1, 0))

                            horiRot = rotMat(theta, 3, 'X')
                            vertRot = rotMat(phi, 3, 'Z')

                            basisVecX.rotate(horiRot)
                            basisVecY.rotate(horiRot)

                            basisVecX.rotate(vertRot)
                            basisVecY.rotate(vertRot)

                            basisVecX *= leafSize / 5.0 # /5.0 for coherent leaf size....
                            basisVecY *= leafSize / 5.0


                            randomValue = random.randint(0,1000)

                            
                            if(useCollectionForLeaves==False):
                                addV([k1 * basisVecX + k2 * basisVecY + center for
                                                           k1, k2 in signList])
                            else:
                                if(max_leaves >0):
                                    max_leaves=max_leaves-1
                                    
                                    # randomly choose a leaf in collection and copy it
                                    leafToClone = collection4Leaves.objects[randomValue % collection4LeavesSize]
                                    newLeaf = leafToClone.copy()
                                    newLeaf.data = leafToClone.data.copy()
                                    
                                    #print("cloned : ",newLeaf)
                                    #print("active = ",bpy.context.active_object) #bpy.context.selected_objects)
                                    
                                    # scale the leaf
                                    newLeaf.scale *= leafSize*10.0 # scale with 10 for coherent size...
                                    # move the leaf to right place
                                    newLeaf.location = center
                                    
                                    # leaf rotation on X and Z global axis
                                    newLeaf.rotation_euler = Euler((theta,0,phi), 'XYZ')
                                    
                                    # I guess setting a 4x4 matrix handling scale pos and rot would be faster^^
                                    
                                    # and join it with the leavesObject
                                    targetCollection.objects.link(newLeaf)
                                    newLeaf.select_set(True)
                                                                

    # here leaves a created if needed and all selected. Join all in one mesh
    if(useCollectionForLeaves==True):
        bpy.ops.object.join()

                                                                    

    # Add the object and link to scene in the growingSupportObject collection
    newCurve = bpy.data.objects.new("IVY_Curve", curve)
    
    # add branches to the proper collection....
    targetCollection.objects.link(newCurve)
    
    if growLeaves:
        if(useCollectionForLeaves==False):

            faceList = [[4 * i + l for l in range(4)] for i in range(len(LeavesVertList) // 4)]

            # Generate the new leaf mesh and link
            leavesMesh.from_pydata(LeavesVertList, [], faceList)
            leavesMesh.update(calc_edges=True)
            
            leavesObject.data = leavesMesh


            # add leaves to the proper collection.....
            #targetCollection.objects.link(leavesObject)

            # Create and set the uv texture coords
            leavesMesh.uv_layers.new(name="UV_diffuse")

            # TODO, this is non-functional, default uvs are ok?
            '''
            for d in tex.data:
                uv1, uv2, uv3, uv4 = signList
            '''
        else:
            #removeMeshFromMemory (leavesMesh)
            print("use of collection here....")

        # parent the leaves to the branches and tag it as 'is_an_ivy'
        leavesObject.parent = newCurve
        leavesObject["IVY"] = {} # create "IVY" dictionnary
        AddFloatPropertyToObject(leavesObject,'IVY_IsSelf', 0.0)

        # unselect the leaves object 
        leavesObject.select_set(False)

        
    # And now, as a final step, lets parent the ivy to the object it was grown on.
    # this will allow to reselect this mesh for edition....
    newCurve.parent = growingSupportObject


    # before quit, we reselect and activate what was at entry
    for ooo in prevSelectedObject:
        ooo.select_set(True)
    if(prevActiveObject is not None):
        prevActiveObject.select_set(True)
    
    bpy.context.view_layer.objects.active =prevActiveObject


    
    # return the branches 3D object
    return(newCurve)



class IvyNode:
    """ The basic class used for each point on the ivy which is grown."""
    __slots__ = ('pos', 'primaryDir', 'adhesionVector', 'adhesionLength',
                 'smoothAdhesionVector', 'length', 'floatingLength', 'climb')

    def __init__(self):
        self.pos = Vector((0, 0, 0))
        self.primaryDir = Vector((0, 0, 1))
        self.adhesionVector = Vector((0, 0, 0))
        self.smoothAdhesionVector = Vector((0, 0, 0))
        self.length = 0.0001
        self.floatingLength = 0.0
        self.climb = True


class IvyRoot:
    """ The class used to hold all ivy nodes growing from this root point."""
    __slots__ = ('ivyNodes', 'alive', 'parents')

    def __init__(self):
        self.ivyNodes = deque()
        self.alive = True
        self.parents = 0


class Ivy:
    """ The class holding all parameters and ivy roots."""
    """
    __slots__ = ('ivyRoots', 'primaryWeight', 'randomWeight',
                 'gravityWeight', 'adhesionWeight', 'branchingProbability',
                 'leafProbability', 'leafDensityFromHeight', 'ivySize', 'ivyLeafSize', 'ivyBranchSize',
                 'maxFloatLength', 'maxAdhesionDistance', 'maxLength')
                 """

    def __init__(self,
                 primaryWeight=0.5,
                 randomWeight=0.2,
                 gravityWeight=1.0,
                 adhesionWeight=0.1,
                 branchingProbability=0.05,
                 leafProbability=0.35,
                 leafDensityFromHeight=0.35,
                 ivySize=0.02,
                 ivyLeafSize=0.02,
                 ivyBranchSize=0.001,
                 maxFloatLength=0.5,
                 maxAdhesionDistance=1.0):



        self.ivyRoots = deque()
        self.primaryWeight = primaryWeight
        self.randomWeight = randomWeight
        self.gravityWeight = gravityWeight
        self.adhesionWeight = adhesionWeight
        self.branchingProbability = 1 - branchingProbability
        self.leafProbability = 1 - leafProbability
        self.leafDensityFromHeight = leafDensityFromHeight
        self.ivySize = ivySize
        self.ivyLeafSize = ivyLeafSize
        self.ivyBranchSize = ivyBranchSize
        self.maxFloatLength = maxFloatLength
        self.maxAdhesionDistance = maxAdhesionDistance
        self.maxLength = 0.0

        # Normalize all the weights only on initialisation
        sums = self.primaryWeight + self.randomWeight + self.adhesionWeight
        self.primaryWeight /= sums
        self.randomWeight /= sums
        self.adhesionWeight /= sums

    def seed(self, seedPos):
        # Seed the Ivy by making a new root and first node
        tmpRoot = IvyRoot()
        tmpIvy = IvyNode()
        tmpIvy.pos = seedPos

        tmpRoot.ivyNodes.append(tmpIvy)
        self.ivyRoots.append(tmpRoot)

    def grow(self, ob, bvhtree):
        # Determine the local sizes
        # local_ivySize = self.ivySize	# * radius
        # local_maxFloatLength = self.maxFloatLength  # * radius
        # local_maxAdhesionDistance = self.maxAdhesionDistance	# * radius

        for root in self.ivyRoots:
            # Make sure the root is alive, if not, skip
            if not root.alive:
                continue

            # Get the last node in the current root
            prevIvy = root.ivyNodes[-1]

            # If the node is floating for too long, kill the root
            if prevIvy.floatingLength > self.maxFloatLength:
                root.alive = False

            # Set the primary direction from the last node
            primaryVector = prevIvy.primaryDir

            # Make the random vector and normalize
            randomVector = Vector((rand_val() - 0.5, rand_val() - 0.5,
                                   rand_val() - 0.5)) + Vector((0, 0, 0.2))
            randomVector.normalize()

            # Calculate the adhesion vector
            adhesionVector = adhesion(
                    prevIvy.pos, bvhtree, self.maxAdhesionDistance)

            # Calculate the growing vector
            growVector = self.ivySize * (primaryVector * self.primaryWeight +
                                          randomVector * self.randomWeight +
                                          adhesionVector * self.adhesionWeight)

            # Find the gravity vector
            gravityVector = (self.ivySize * self.gravityWeight *
                                                            Vector((0, 0, -1)))
            gravityVector *= pow(prevIvy.floatingLength / self.maxFloatLength,
                                 0.7)

            # Determine the new position vector
            newPos = prevIvy.pos + growVector + gravityVector

            # Check for collisions with the object
            climbing, newPos = collision(bvhtree, prevIvy.pos, newPos)

            # Update the growing vector for any collisions
            growVector = newPos - prevIvy.pos - gravityVector
            growVector.normalize()

            # Create a new IvyNode and set its properties
            tmpNode = IvyNode()
            tmpNode.climb = climbing
            tmpNode.pos = newPos
            tmpNode.primaryDir = prevIvy.primaryDir.lerp(growVector, 0.5)
            tmpNode.primaryDir.normalize()
            tmpNode.adhesionVector = adhesionVector
            tmpNode.length = prevIvy.length + (newPos - prevIvy.pos).length

            if tmpNode.length > self.maxLength:
                self.maxLength = tmpNode.length

            # If the node isn't climbing, update it's floating length
            # Otherwise set it to 0
            if not climbing:
                tmpNode.floatingLength = prevIvy.floatingLength + (newPos -
                                                            prevIvy.pos).length
            else:
                tmpNode.floatingLength = 0.0

            root.ivyNodes.append(tmpNode)

        # Loop through all roots to check if a new root is generated
        for root in self.ivyRoots:
            # Check the root is alive and isn't at high level of recursion
            if (root.parents > 3) or (not root.alive):
                continue

            # Check to make sure there's more than 1 node
            if len(root.ivyNodes) > 1:
                # Loop through all nodes in root to check if new root is grown
                for node in root.ivyNodes:
                    # Set the last node of the root and find the weighting
                    prevIvy = root.ivyNodes[-1]
                    weight = 1.0 - (cos(2.0 * pi * node.length /
                                        prevIvy.length) * 0.5 + 0.5)

                    probability = rand_val()

                    # Check if a new root is grown and if so, set its values
                    if (probability * weight > self.branchingProbability):
                        tmpNode = IvyNode()
                        tmpNode.pos = node.pos
                        tmpNode.floatingLength = node.floatingLength

                        tmpRoot = IvyRoot()
                        tmpRoot.parents = root.parents + 1

                        tmpRoot.ivyNodes.append(tmpNode)
                        self.ivyRoots.append(tmpRoot)
                        return


def adhesion(loc, bvhtree, max_l):
    # Compute the adhesion vector by finding the nearest point
    nearest_location, *_ = bvhtree.find_nearest(loc, max_l)
    adhesion_vector = Vector((0.0, 0.0, 0.0))
    if nearest_location is not None:
        # Compute the distance to the nearest point
        adhesion_vector = nearest_location - loc
        distance = adhesion_vector.length
        # If it's less than the maximum allowed and not 0, continue
        if distance:
            # Compute the direction vector between the closest point and loc
            adhesion_vector.normalize()
            adhesion_vector *= 1.0 - distance / max_l
            # adhesion_vector *= getFaceWeight(ob.data, nearest_result[3])
    return adhesion_vector


def collision(bvhtree, pos, new_pos):
    # Check for collision with the object
    climbing = False

    corrected_new_pos = new_pos
    direction = new_pos - pos

    hit_location, hit_normal, *_ = bvhtree.ray_cast(pos, direction, direction.length)
    # If there's a collision we need to check it
    if hit_location is not None:
        # Check whether the collision is going into the object
        if direction.dot(hit_normal) < 0.0:
            reflected_direction = (new_pos - hit_location).reflect(hit_normal)

            corrected_new_pos = hit_location + reflected_direction
            climbing = True

    return climbing, corrected_new_pos


def bvhtree_from_object(ob):
    import bmesh
    bm = bmesh.new()

    depsgraph = bpy.context.evaluated_depsgraph_get()
    ob_eval = ob.evaluated_get(depsgraph)
    mesh = ob_eval.to_mesh()
    bm.from_mesh(mesh)
    bm.transform(ob.matrix_world)

    bvhtree = BVHTree.FromBMesh(bm)
    ob_eval.to_mesh_clear()
    return bvhtree

def check_mesh_faces(ob):
    me = ob.data
    if len(me.polygons) > 0:
        return True

    return False


class IvyGen(Operator):
    
    
    bl_idname = "curve.ivy_gen"
    bl_label = "IvyGen"
    bl_description = "Generate Ivy on an Mesh Object"
    bl_options = {'REGISTER', 'UNDO'}

    resetIvy: BoolProperty(
        name="Reset values",
        description="Reset all values to default",
        default=False
    )
    

    
    '''
    # not sure but it seems this is useless...
    @classmethod
    def poll(self, context):
        # Check if there's an object and whether it's a mesh
        return(True)
    '''

    # called when user clicks on Add new ivy in N-Panel
    def invoke(self, context, event):
        return(self.execute(context))


    def execute(self, context):

        wm = context.window_manager.ivy_gen_props
        
        print("expected ivy len = ",wm.totalIvyLength)
        
        
        if self.resetIvy:
            wm.totalIvyLength = 1.0 # i don't know how to retrieve the default values set in properties :-/
            wm.randomSeed = 0.0
            wm.ivySize = 0.02
            wm.maxFloatLength = 0.5
            wm.maxAdhesionDistance = 1.0
            wm.primaryWeight = 0.5
            wm.randomWeight = 0.2
            wm.gravityWeight = 1.0
            wm.adhesionWeight = 0.1
            wm.branchingProbability = 0.05
            wm.leafProbability = 0.35
            wm.leafDensityFromHeight = 0.35
            wm.ivyBranchSize = 0.001
            wm.ivyLeafSize = 0.02
            
        

        bpy.ops.object.mode_set(mode='EDIT', toggle=False)
        bpy.ops.object.mode_set(mode='OBJECT', toggle=False)

        # Get the active object if we gonna create an ivy.....
        if(context.window_manager.ivy_gen_props.allowIvyCreation):
            # Get the growing support object. this object must be kept selected and active
            supportObject = context.active_object
            
        else: 
            # if we want to edit an ivy instead....
            if(context.window_manager.ivy_gen_props.allowIvyEdition):
                
                # active object is the ivy branches
                if(context.active_object.parent is None):
                    print("Ivy has no growing support. It cannot be edited")
                    
                    return {'PASS_THROUGH'} 
                else:
                    selectedIvyBranches = context.active_object
                    
                    # Retrieve the seed point and bring the 3D cursor on it
                    # the seed point is the 1st curve point
                    context.scene.cursor.location = selectedIvyBranches["IVY"]["IVY_StartPos"]
                    
                    # And also retrieve the growing support object
                    supportObject = context.active_object.parent
                    
                    # now delete branches and leaves as they will be recreated                    
                    selectedIvyBranches.select_set(True)
                    for child in selectedIvyBranches.children:
                        child.select_set(True)
                        
                    bpy.ops.object.delete()
                                        
            else:
                # unknown state: no edition and no creation. We might have selected leaves....
                return {'PASS_THROUGH'} 
                

                        
            
        bvhtree = bvhtree_from_object(supportObject)

        # Check if the mesh has at least one polygon since some functions
        # are expecting them in the object's data (see T51753)
        check_face = check_mesh_faces(supportObject)
        if check_face is False:
            self.report({'WARNING'},
                        "Mesh Object doesn't have at least one Face. "
                        "Operation Cancelled")
            return {"CANCELLED"}

        # Fix the random seed
        rand_seed(wm.randomSeed)

        # Make the new ivy
        New_ivy = Ivy(wm.primaryWeight,
                      wm.randomWeight,
                      wm.gravityWeight,
                      wm.adhesionWeight,
                      wm.branchingProbability,
                      wm.leafProbability,
                      wm.leafDensityFromHeight,
                      wm.ivySize,
                      wm.ivyLeafSize,
                      wm.ivyBranchSize,
                      wm.maxFloatLength,
                      wm.maxAdhesionDistance)
                      
                      
        # Generate first root and node at the 3D cursor location
        New_ivy.seed(context.scene.cursor.location)

        maxLength = wm.totalIvyLength  # * radius

        # If we need to check time set the flag
        checkTime = False
        if(context.window_manager.ivy_gen_props.maxTime != 0.0):
            checkTime = True

        t = time.time()
        startPercent = 0.0
        checkAliveIter = [True, ]

        # Grow until 200 roots is reached or backup counter exceeds limit
        while (any(checkAliveIter) and (New_ivy.maxLength < maxLength) and (not checkTime or (time.time() - t < context.window_manager.ivy_gen_props.maxTime))):
            # Grow the ivy for this iteration
            New_ivy.grow(supportObject, bvhtree)

            # Print the proportion of ivy growth to console
            if (New_ivy.maxLength / maxLength * 100) > 10 * startPercent // 10:
                #print('%0.2f%% of Ivy nodes have grown' % (New_ivy.maxLength / maxLength * 100))
                startPercent += 10

            # Make an iterator to check if all are alive
            checkAliveIter = (r.alive for r in New_ivy.ivyRoots)

        # Create the curve and leaf geometry
        Ivy3DObject = createIvyGeometry(New_ivy, wm.growLeaves, wm.leavesCollection, supportObject)
            
        
        # and add Custom Properties for param memorization for future edition
        Ivy3DObject["IVY"] = {}
        AddFloatPropertyToObject(Ivy3DObject,'IVY_IsSelf', 0.123)
        AddVectorPropertyToObject(Ivy3DObject,'IVY_StartPos',context.scene.cursor.location)
        
        AddIntPropertyToObject(Ivy3DObject,'IVY_RandSeed',wm.randomSeed)

        AddFloatPropertyToObject(Ivy3DObject,'IVY_TotalIvyLength',wm.totalIvyLength)
        AddFloatPropertyToObject(Ivy3DObject,'IVY_IvySegmentSize',wm.ivySize)
        AddFloatPropertyToObject(Ivy3DObject,'IVY_MaxFloatLength',wm.maxFloatLength)
        AddFloatPropertyToObject(Ivy3DObject,'IVY_MaxAdhesionDistance',wm.maxAdhesionDistance)

        AddFloatPropertyToObject(Ivy3DObject,'IVY_PrimaryWeight',wm.primaryWeight)
        AddFloatPropertyToObject(Ivy3DObject,'IVY_RandomWeight',wm.randomWeight)
        AddFloatPropertyToObject(Ivy3DObject,'IVY_GravityWeight',wm.gravityWeight)
        AddFloatPropertyToObject(Ivy3DObject,'IVY_AdhesionWeight',wm.adhesionWeight)

        AddFloatPropertyToObject(Ivy3DObject,'IVY_BranchingProbability',wm.branchingProbability)
        AddFloatPropertyToObject(Ivy3DObject,'IVY_IvyBranchSize',wm.ivyBranchSize)

        AddFloatPropertyToObject(Ivy3DObject,'IVY_IvyLeafSize',wm.ivyLeafSize)
        AddFloatPropertyToObject(Ivy3DObject,'IVY_LeafProbability',wm.leafProbability)
        AddFloatPropertyToObject(Ivy3DObject,'IVY_LeafDensityFromHeight',wm.leafDensityFromHeight)
        AddBoolPropertyToObject( Ivy3DObject,'IVY_GrowLeaves',wm.growLeaves)
        
        # I won't memorize leaves collection as creation/edition with thousands of complex
        # meshe might be heavy and freeze blender for too long, making ivy edition/creation boring and useless....
        # then i for the use of square leaves and when the user is happy with the preview, he chooses the
        # collection to take the leaves from.
        #Ivy3DObject["IVY"]["IVY_LeavesCollection"] = wm.leavesCollection
        #wm.leavesCollection = Ivy3DObject["IVY"]["IVY_LeavesCollection"] # test: is cool mem okay ?


        
        
        print("Ivy generated in %0.2f s ( but we don't care )" % (time.time() - t))
       
                
        if(context.window_manager.ivy_gen_props.allowIvyEdition):
            # if we are in ivy edition mode then lets reselect the ivy branches
            #if(context.window_manager.ivy_gen_props.allowIvyEdition):
            for ob in bpy.context.selected_objects:
                ob.select_set(False)
                
            
            Ivy3DObject.select_set(True)
            
            bpy.context.view_layer.objects.active = Ivy3DObject
            
        # not sure this is usefull.....    
        context.window_manager.ivy_gen_props.allowIvyCreation = False
        context.window_manager.ivy_gen_props.allowIvyEdition = False
        
        self.resetIvy = False

        return {'FINISHED'}

    def draw(self, context):
        layout = self.layout
        #wm = context.window_manager
        wm = context.window_manager.ivy_gen_props
        col = layout.column(align=True,)

        layout.prop(self, "resetIvy", icon="COLOR_RED")
        
        col = layout.column(align=True)
        col.label(text="Generation Settings:")
        col.prop(wm, "randomSeed")

        col = layout.column(align=True)
        col.label(text="Size Settings:")
        col.prop(wm, "totalIvyLength")
        col.prop(wm, "ivySize")
        col.prop(wm, "maxFloatLength")
        col.prop(wm, "maxAdhesionDistance")

        col = layout.column(align=True)
        col.label(text="Weight Settings:")
        col.prop(wm, "primaryWeight")
        col.prop(wm, "randomWeight")
        col.prop(wm, "gravityWeight")
        col.prop(wm, "adhesionWeight")

        col = layout.column(align=True)
        col.label(text="Branch Settings:")
        col.prop(wm, "branchingProbability")
        col.prop(wm, "ivyBranchSize")

        col = layout.column(align=True)
        ro = layout.row(align=True)
        ro.prop(wm, "growLeaves")

        if wm.growLeaves:
            ro.prop(wm, "leavesCollection")
            #if(wm.leavesCollection is not None):
                #for ooo in wm.leavesCollection.all_objects:
                    #print("obj: ", ooo.name)
            col = layout.column(align=True)
            col.label(text="Leaf Settings:")
            col.prop(wm, "ivyLeafSize")
            col.prop(wm, "leafProbability")
            col.prop(wm, "leafDensityFromHeight")
        


class Ivy_N_Panel(Panel):
    bl_label = "Ivy Generator"
    bl_idname = "CURVE_PT_IvyNPanel"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "Create"
    bl_context = "objectmode"
    bl_options = {"DEFAULT_CLOSED"}

    def draw(self, context):
        layout = self.layout
        col = layout.column(align=True)

        wm = context.window_manager.ivy_gen_props

        ob = bpy.context.view_layer.objects.active # here active object is properly retrieved
        


        # by default, disable ivy edition and creation
        wm.allowIvyEdition = False
        wm.allowIvyCreation = False

        # if an object is selected....                    
        if(ob is not None):
            # if object is an ivy, disable ivy creation and allow edition ( only on branches selection )
            if(ob.get("IVY") is not None):
                #print("object is an ivy !!!!!")
                
                if(ob["IVY"]["IVY_IsSelf"]==0.123):
                    print("Is branches and can be edited")
                    
                    # here we grab the ivy instance and its parameters
                    # and we enable edition
                    wm.allowIvyEdition = True
                    
                    selectedIvyBranches = ob
                    
                    # retrieve all custom properties from ivy branches and set up values in the window controls
                    wm.randomSeed            = selectedIvyBranches["IVY"]['IVY_RandSeed']
                    wm.totalIvyLength        = selectedIvyBranches["IVY"]['IVY_TotalIvyLength']
                    wm.ivySize               = selectedIvyBranches["IVY"]['IVY_IvySegmentSize']
                    wm.maxFloatLength        = selectedIvyBranches["IVY"]['IVY_MaxFloatLength']
                    wm.maxAdhesionDistance   = selectedIvyBranches["IVY"]['IVY_MaxAdhesionDistance']
                    wm.primaryWeight         = selectedIvyBranches["IVY"]['IVY_PrimaryWeight']
                    wm.randomWeight          = selectedIvyBranches["IVY"]['IVY_RandomWeight']
                    wm.gravityWeight         = selectedIvyBranches["IVY"]['IVY_GravityWeight']
                    wm.adhesionWeight        = selectedIvyBranches["IVY"]['IVY_AdhesionWeight']
                    wm.branchingProbability  = selectedIvyBranches["IVY"]['IVY_BranchingProbability']
                    wm.ivyBranchSize         = selectedIvyBranches["IVY"]['IVY_IvyBranchSize']
                    wm.ivyLeafSize           = selectedIvyBranches["IVY"]['IVY_IvyLeafSize']
                    wm.leafProbability       = selectedIvyBranches["IVY"]['IVY_LeafProbability']
                    wm.leafDensityFromHeight = selectedIvyBranches["IVY"]['IVY_LeafDensityFromHeight']
                    wm.growLeaves            = selectedIvyBranches["IVY"]['IVY_GrowLeaves']                    
                    
                else:
                    print("Is leaves and can NOT be edited. Please select branches to enable edition")
              
            else:
                #object is not an ivy: ivy can be created on it if it is an object that have a mesh
                if((ob.type == 'MESH') and (context.mode == 'OBJECT')):
                    wm.allowIvyCreation = True
            


        

        if(wm.allowIvyCreation):
            prop_new = col.operator("curve.ivy_gen", text="Add New Ivy", icon="OUTLINER_OB_CURVE")
            wm.leavesCollection = None # no leaves for ivy creation to avoid blender freeze on big ivis
        else:        
            if(wm.allowIvyEdition):
                prop_def = col.operator("curve.ivy_gen", text="Edit this ivy", icon="CURVE_DATA")
                wm.leavesCollection = None # no leaves for ivy creation to avoid blender freeze on big ivis
            
        
        
        col = layout.column(align=True)
        col.label(text="Generation Time limit:")
        col.prop(wm, "maxTime")

            


class IvyGenProperties(PropertyGroup):
#bpy.types.Object.my_custom_property = bpy.props.BoolProperty(name="Test Property", description="This should show up as API Defined")
#bpy.context.active_object.my_custom_property = True
    
    # values that are used as flags for control, and that are not editable
    allowIvyEdition: BoolProperty(
        name="allowIvyEdition",
        description="allow selected ivy edition",
        default=False
    )

    allowIvyCreation: BoolProperty(
        name="allowIvyCreation",
        description="allow ivy creation on selected object",
        default=False
    )
    # ===========================================================================
            
    # value set in Left N panel
    maxTime: FloatProperty(
        name="Maximum Time",
        description="The maximum time to run the generation for "
                    "in seconds generation (0.0 = Disabled)",
        default=20.0,
        min=0.0,
        soft_max=10
    )
    # ===========================================================================

                    
                    
    # values set in parameters bottom-left panel
    randomSeed: IntProperty(
        name="Random Seed",
        description="The seed governing random generation",
        default=0,
            min=0
        )
    # --------------------------------------------------------
    totalIvyLength: FloatProperty(
        name="Total Ivy Length",
        description="Maximum ivy length in Blender Units",
        default=1.0,
        min=0.0,
        soft_max=3.0,
        subtype='DISTANCE',
        unit='LENGTH',
        precision=3,
        step=10.0
        )
    # --------------------------------------------------------
    ivySize: FloatProperty(
        name="Branch segment Length",
        description="The length of an ivy segment"
                    " Units",
        default=0.02,
        min=0.005,
        soft_max=1.0,
        precision=4,
        step=1.0
    )
    # --------------------------------------------------------
    maxFloatLength: FloatProperty(
        name="Max Float Length",
        description="The maximum distance that a branch "
                    "can live while floating",
        default=0.5,
        min=0.0,
        soft_max=1.0
    )
    # --------------------------------------------------------
    maxAdhesionDistance: FloatProperty(
        name="Max Adhesion Length",
        description="The maximum distance that a branch "
                    "will feel the effects of adhesion",
        default=1.0,
        min=0.0,
        soft_max=2.0,
        precision=2
    )
    # --------------------------------------------------------
    primaryWeight: FloatProperty(
        name="Primary Weight",
        description="Weighting given to the current direction",
        default=0.5,
        min=0.0,
        soft_max=1.0
    )
    # --------------------------------------------------------
    randomWeight: FloatProperty(
        name="Random Weight",
        description="Weighting given to the random direction",
        default=0.2,
        min=0.0,
        soft_max=1.0
    )
    # --------------------------------------------------------
    gravityWeight: FloatProperty(
        name="Gravity Weight",
        description="Weighting given to the gravity direction",
        default=1.0,
        min=0.0,
        soft_max=1.0
    )
    # --------------------------------------------------------
    adhesionWeight: FloatProperty(
        name="Adhesion Weight",
        description="Weighting given to the adhesion direction",
        default=0.1,
        min=0.0,
        soft_max=1.0
    )
    # --------------------------------------------------------
    branchingProbability: FloatProperty(
        name="Branching Probability",
        description="Probability of a new branch forming",
        default=0.05,
        min=0.0,
        soft_max=1.0,
        precision=4,
        step=1.0
    )
    # --------------------------------------------------------
    ivyBranchSize: FloatProperty(
        name="Branch start diameter",
        description="The size of the ivy branches",
        default=0.001,
        min=0.0,
        soft_max=0.1,
        precision=4,
        step=0.1
    )
    # --------------------------------------------------------
    ivyLeafSize: FloatProperty(
        name="Leaf Size",
        description="The size of the ivy leaves",
        default=0.02,
        min=0.0,
        soft_max=0.5,
        precision=3
    )
    # --------------------------------------------------------
    leafProbability: FloatProperty(
        name="Leaf Probability",
        description="Probability of a leaf forming",
        default=0.35,
        min=0.0,
        soft_max=1.0,
        precision=3,
        step=1.0
    )
    # --------------------------------------------------------
    leafDensityFromHeight: FloatProperty(
        name="leaf density VS height factor",
        description="defines wether leaf density is controlled by branches height or not.",
        default=0.35,
        min=0.0,
        soft_max=1.0,
        precision=3,
        step=1.0
    )
    # --------------------------------------------------------
    growLeaves: BoolProperty(
        name="Grow Leaves",
        description="Grow leaves or not",
        default=True
    )
    # --------------------------------------------------------
    leavesCollection: PointerProperty(
        type=bpy.types.Collection,
        name="--->",
        description="Collection of leaves. If empty, squares will be used"
    )
    # --------------------------------------------------------

    


classes = (
    IvyGen,
    IvyGenProperties,
    Ivy_N_Panel
)



def register():
    for cls in classes:
        bpy.utils.register_class(cls)

    bpy.types.WindowManager.ivy_gen_props = PointerProperty(type=IvyGenProperties)


def unregister():
    del bpy.types.WindowManager.ivy_gen_props

    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)


if __name__ == "__main__":
    register()


 

Problems created with this version:

  • use of complex mesh leaves is much heavier than simple square ones. This is the reason why i don’t keep leaves collection between edition and always get back to square leaves.
  • when deleting the collection from the collection property, the parameters window closes. It shouldn’t and i don’t even understand why it does ( shitty python dev i’m )

Thanks all and happy blending ! :smiley:

1 Like