From 44e0a904a49524b9d1410f347b499916636256fd Mon Sep 17 00:00:00 2001 From: gandalf3 Date: Thu, 22 Jun 2017 17:41:41 -0700 Subject: [PATCH] Pull some code in from 2016 project; add tests --- blenderpack.py | 99 +++ tests/__init__.py | 0 tests/test_blenderpack.py | 28 + .../addons/add_curve_extra_objects.zip | Bin 0 -> 51971 bytes tests/test_helpers/addons/add_curve_ivygen.py | 748 ++++++++++++++++++ tests/test_helpers/extra_objects_blinfo.txt | 1 + tests/test_helpers/ivy_gen_blinfo.txt | 1 + 7 files changed, 877 insertions(+) mode change 100644 => 100755 blenderpack.py create mode 100644 tests/__init__.py create mode 100644 tests/test_blenderpack.py create mode 100644 tests/test_helpers/addons/add_curve_extra_objects.zip create mode 100644 tests/test_helpers/addons/add_curve_ivygen.py create mode 100644 tests/test_helpers/extra_objects_blinfo.txt create mode 100644 tests/test_helpers/ivy_gen_blinfo.txt diff --git a/blenderpack.py b/blenderpack.py old mode 100644 new mode 100755 index b582533..c8244af --- a/blenderpack.py +++ b/blenderpack.py @@ -3,10 +3,109 @@ # HACK: seems 'requests' module bundled with blender isn't bundled with 'idna' module. So force system python for now import sys sys.path.insert(0, '/usr/lib/python3.6/site-packages') + import requests +import json +import os +import ast +import argparse +import zipfile +import logging + +log = logging.getLogger(__name__) + +def report(msg): + # if __name__ == '__main__': + print("blenderpack:", msg) + +def fatal_report(msg, code=1): + report(msg) + exit(code) def fetch(url): # TODO: do conditional request re = requests.get(url) print(re.json()) +def parse_blinfo(source, addon_name="unknown"): + """Parse a python file and return its bl_info dict, if there is one (else return None)""" + + try: + tree = ast.parse(source) + except SyntaxError as ex: + log.warning('Skipping addon: SyntaxError in %s: %s', addon_name, ex) + return None + + for body in tree.body: + if body.__class__ != ast.Assign: + continue + if len(body.targets) != 1: + continue + if getattr(body.targets[0], 'id', '') != 'bl_info': + continue + + return ast.literal_eval(body.value) + + log.warning('Unable to find bl_info dict in %s', addon_name) + return None + + +def extract_blinfo(path): + """Extract bl_info dict from addon at path (can be single file, module, or zip)""" + + source = None + # get last component of path, even with trailing slash + addon_name = os.path.split(path.rstrip(os.path.sep))[1] + + if os.path.isdir(path): + with open(os.path.join(path, '__init__.py' 'r')) as f: + source = f.read() + else: + + # HACK: perhaps not the best approach determining filetype..? + try: + with zipfile.ZipFile(path, 'r') as z: + for fname in z.namelist(): + # HACK: this seems potentially fragile; depends on zipfile listing root contents first + if fname.endswith('__init__.py'): + source = z.read(fname) + break + except zipfile.BadZipFile: + with open(path, 'r') as f: + source = f.read() + + if source == None: + raise RuntimeError("Could not read addon '%s'" % addon_name) + + return parse_blinfo(source, addon_name) + + + +def make_repo(path): + """Make repo.json for files in directory 'path'""" + # try: + for addon in os.listdir(path): + extract_blinfo(os.path.join(path, addon)) + # except FileNotFoundError: + # fatal_report("No such file or directory: '%s'" % path) + + + +def main(): + pass + # print(args) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Search, install, and manage packages for Blender') + subparsers = parser.add_subparsers() + + make = subparsers.add_parser('make') + make.add_argument('path') + make.set_defaults(func=lambda args: make_repo(args.path)) + + args = parser.parse_args() + args.func(args) + + main() + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_blenderpack.py b/tests/test_blenderpack.py new file mode 100644 index 0000000..5146eaa --- /dev/null +++ b/tests/test_blenderpack.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +import unittest +import os.path +import blenderpack + +class test_blenderpack_make_repo(unittest.TestCase): + + helper_path = os.path.join('tests', 'test_helpers') + + def test_extract_blinfo_from_file(self): + with open(os.path.join(self.helper_path, 'ivy_gen_blinfo.txt'), 'r') as f: + expectation = f.read() + + reality = str(blenderpack.extract_blinfo(os.path.join(self.helper_path, 'addons', 'add_curve_ivygen.py'))) + self.assertEqual(expectation, reality) + + def test_extract_blinfo_from_zip(self): + with open(os.path.join(self.helper_path, 'extra_objects_blinfo.txt'), 'r') as f: + expectation = f.read() + + reality = str(blenderpack.extract_blinfo(os.path.join(self.helper_path, 'addons', 'add_curve_extra_objects.zip'))) + self.assertEqual(expectation, reality) + + # def test_validpath(self): + # blenderpack.make_repo(os.path.join('test_helpers', 'addons')) + + diff --git a/tests/test_helpers/addons/add_curve_extra_objects.zip b/tests/test_helpers/addons/add_curve_extra_objects.zip new file mode 100644 index 0000000000000000000000000000000000000000..c08089b0740d330333cc4e79d3d55db618aafee4 GIT binary patch literal 51971 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 + +bl_info = { + "name": "IvyGen", + "author": "testscreenings, PKHG, TrumanBlending", + "version": (0, 1, 2), + "blender": (2, 59, 0), + "location": "View3D > Add > Curve", + "description": "Adds generated ivy to a mesh object starting " + "at the 3D cursor", + "warning": "", + "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/" + "Scripts/Curve/Ivy_Gen", + "category": "Add Curve", +} + + +import bpy +from bpy.props import ( + FloatProperty, + IntProperty, + BoolProperty, + ) +from mathutils import ( + Vector, + Matrix, + ) +from collections import deque +from math import ( + pow, cos, + pi, atan2, + ) +from random import ( + random as rand_val, + seed as rand_seed, + ) +import time + + +def createIvyGeometry(IVY, growLeaves): + """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 intialise it + curve = bpy.data.curves.new("IVY", type='CURVE') + curve.dimensions = '3D' + curve.bevel_depth = 1 + curve.fill_mode = 'FULL' + curve.resolution_u = 4 + + if growLeaves: + # 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), + ) + + # Get the local size + # local_ivyLeafSize = IVY.ivyLeafSize # * radius * IVY.ivySize + + # Initialise the vertex and face lists + vertList = deque() + + # Store the methods for faster calling + addV = vertList.extend + rotMat = Matrix.Rotation + + # 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) + 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 + if (probability * weight) > IVY.leafProbability: + + # Generate the random vector + randomVector = Vector((rand_val() - 0.5, + rand_val() - 0.5, + rand_val() - 0.5, + )) + + # 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 + basisVecY *= leafSize + + addV([k1 * basisVecX + k2 * basisVecY + center for + k1, k2 in signList]) + + # Add the object and link to scene + newCurve = bpy.data.objects.new("IVY_Curve", curve) + bpy.context.scene.objects.link(newCurve) + + if growLeaves: + faceList = [[4 * i + l for l in range(4)] for i in + range(len(vertList) // 4)] + + # Generate the new leaf mesh and link + me = bpy.data.meshes.new('IvyLeaf') + me.from_pydata(vertList, [], faceList) + me.update(calc_edges=True) + ob = bpy.data.objects.new('IvyLeaf', me) + bpy.context.scene.objects.link(ob) + + me.uv_textures.new("Leaves") + + # Set the uv texture coords + # TODO, this is non-functional, default uvs are ok? + ''' + for d in tex.data: + uv1, uv2, uv3, uv4 = signList + ''' + + ob.parent = newCurve + + +''' +def computeBoundingSphere(ob): + # Get the mesh data + me = ob.data + # Intialise the center + center = Vector((0.0, 0.0, 0.0)) + # Add all vertex coords + for v in me.vertices: + center += v.co + # Average over all verts + center /= len(me.vertices) + # Create the iterator and find its max + length_iter = ((center - v.co).length for v in me.vertices) + radius = max(length_iter) + return radius +''' + + +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', '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, + 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.ivySize = ivySize + self.ivyLeafSize = ivyLeafSize + self.ivyBranchSize = ivyBranchSize + self.maxFloatLength = maxFloatLength + self.maxAdhesionDistance = maxAdhesionDistance + self.maxLength = 0.0 + + # Normalize all the weights only on intialisation + 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): + # 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, ob, + 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 = collision(ob, 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, ob, max_l): + # Get transfor vector and transformed loc + tran_mat = ob.matrix_world.inverted() + tran_loc = tran_mat * loc + + # Compute the adhesion vector by finding the nearest point + nearest_result = ob.closest_point_on_mesh(tran_loc, max_l) + adhesion_vector = Vector((0.0, 0.0, 0.0)) + if nearest_result[0]: + # Compute the distance to the nearest point + adhesion_vector = ob.matrix_world * nearest_result[1] - 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(ob, pos, new_pos): + # Check for collision with the object + climbing = False + + # Transform vecs + tran_mat = ob.matrix_world.inverted() + tran_pos = tran_mat * pos + tran_new_pos = tran_mat * new_pos + tran_dir = tran_new_pos - tran_pos + + ray_result = ob.ray_cast(tran_pos, tran_dir, tran_dir.length) + # If there's a collision we need to check it + if ray_result[0]: + # Check whether the collision is going into the object + if tran_dir.dot(ray_result[2]) < 0.0: + # Find projection of the piont onto the plane + p0 = tran_new_pos - (tran_new_pos - + ray_result[1]).project(ray_result[2]) + # Reflect in the plane + tran_new_pos += 2 * (p0 - tran_new_pos) + new_pos *= 0 + new_pos += ob.matrix_world * tran_new_pos + climbing = True + return climbing + + +def check_mesh_faces(ob): + me = ob.data + if len(me.polygons) > 0: + return True + + return False + + +class IvyGen(bpy.types.Operator): + bl_idname = "curve.ivy_gen" + bl_label = "IvyGen" + bl_description = "Generate Ivy on an Mesh Object" + bl_options = {'REGISTER', 'UNDO'} + + maxIvyLength = FloatProperty( + name="Max Ivy Length", + description="Maximum ivy length in Blender Units", + default=1.0, + min=0.0, + soft_max=3.0, + subtype='DISTANCE', + unit='LENGTH' + ) + 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 + ) + leafProbability = FloatProperty( + name="Leaf Probability", + description="Probability of a leaf forming", + default=0.35, + min=0.0, + soft_max=1.0 + ) + ivySize = FloatProperty( + name="Ivy Size", + description="The length of an ivy segment in Blender" + " Units", + default=0.02, + min=0.0, + soft_max=1.0, + precision=3 + ) + ivyLeafSize = FloatProperty( + name="Ivy Leaf Size", + description="The size of the ivy leaves", + default=0.02, + min=0.0, + soft_max=0.5, + precision=3 + ) + ivyBranchSize = FloatProperty( + name="Ivy Branch Size", + description="The size of the ivy branches", + default=0.001, + min=0.0, + soft_max=0.1, + precision=4 + ) + 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 + ) + randomSeed = IntProperty( + name="Random Seed", + description="The seed governing random generation", + default=0, + min=0 + ) + maxTime = FloatProperty( + name="Maximum Time", + description="The maximum time to run the generation for " + "in seconds generation (0.0 = Disabled)", + default=0.0, + min=0.0, + soft_max=10 + ) + growLeaves = BoolProperty( + name="Grow Leaves", + description="Grow leaves or not", + default=True + ) + updateIvy = BoolProperty( + name="Update Ivy", + default=False + ) + + @classmethod + def poll(self, context): + # Check if there's an object and whether it's a mesh + ob = context.active_object + return ((ob is not None) and + (ob.type == 'MESH') and + (context.mode == 'OBJECT')) + + def invoke(self, context, event): + self.updateIvy = True + return self.execute(context) + + def execute(self, context): + if not self.updateIvy: + return {'PASS_THROUGH'} + + bpy.ops.object.mode_set(mode='EDIT', toggle=False) + bpy.ops.object.mode_set(mode='OBJECT', toggle=False) + + # Get the selected object + ob = context.active_object + + # 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(ob) + if check_face is False: + self.report({'WARNING'}, + "Mesh Object doesn't have at least one Face. " + "Operation Cancelled") + return {"CANCELLED"} + + # Compute bounding sphere radius + # radius = computeBoundingSphere(ob) # Not needed anymore + + # Get the seeding point + seedPoint = context.scene.cursor_location + + # Fix the random seed + rand_seed(self.randomSeed) + + # Make the new ivy + IVY = Ivy(**self.as_keywords(ignore=('randomSeed', 'growLeaves', + 'maxIvyLength', 'maxTime', 'updateIvy'))) + + # Generate first root and node + IVY.seed(seedPoint) + + checkTime = False + maxLength = self.maxIvyLength # * radius + + # If we need to check time set the flag + if self.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 + (IVY.maxLength < maxLength) and + (not checkTime or (time.time() - t < self.maxTime))): + # Grow the ivy for this iteration + IVY.grow(ob) + + # Print the proportion of ivy growth to console + if (IVY.maxLength / maxLength * 100) > 10 * startPercent // 10: + print('%0.2f%% of Ivy nodes have grown' % + (IVY.maxLength / maxLength * 100)) + startPercent += 10 + if IVY.maxLength / maxLength > 1: + print("Halting Growth") + + # Make an iterator to check if all are alive + checkAliveIter = (r.alive for r in IVY.ivyRoots) + + # Create the curve and leaf geometry + createIvyGeometry(IVY, self.growLeaves) + print("Geometry Generation Complete") + + print("Ivy generated in %0.2f s" % (time.time() - t)) + + self.updateIvy = False + + return {'FINISHED'} + + def draw(self, context): + layout = self.layout + + layout.prop(self, 'updateIvy', icon='CURVE_DATA') + + properties = layout.operator('curve.ivy_gen', text="Add New Ivy") + properties.randomSeed = self.randomSeed + properties.maxTime = self.maxTime + properties.maxIvyLength = self.maxIvyLength + properties.ivySize = self.ivySize + properties.maxFloatLength = self.maxFloatLength + properties.maxAdhesionDistance = self.maxAdhesionDistance + properties.primaryWeight = self.primaryWeight + properties.randomWeight = self.randomWeight + properties.gravityWeight = self.gravityWeight + properties.adhesionWeight = self.adhesionWeight + properties.branchingProbability = self.branchingProbability + properties.leafProbability = self.leafProbability + properties.ivyBranchSize = self.ivyBranchSize + properties.ivyLeafSize = self.ivyLeafSize + properties.updateIvy = True + + prop_def = layout.operator('curve.ivy_gen', text="Add New Default Ivy") + prop_def.updateIvy = True + + layout.prop(self, 'growLeaves') + + box = layout.box() + box.label("Generation Settings:") + box.prop(self, 'randomSeed') + box.prop(self, 'maxTime') + + box = layout.box() + box.label("Size Settings:") + box.prop(self, 'maxIvyLength') + box.prop(self, 'ivySize') + box.prop(self, 'maxFloatLength') + box.prop(self, 'maxAdhesionDistance') + + box = layout.box() + box.label("Weight Settings:") + box.prop(self, 'primaryWeight') + box.prop(self, 'randomWeight') + box.prop(self, 'gravityWeight') + box.prop(self, 'adhesionWeight') + + box = layout.box() + box.label("Branch Settings:") + box.prop(self, 'branchingProbability') + box.prop(self, 'ivyBranchSize') + + if self.growLeaves: + box = layout.box() + box.label("Leaf Settings:") + box.prop(self, 'ivyLeafSize') + box.prop(self, 'leafProbability') + + +def menu_func(self, context): + self.layout.operator(IvyGen.bl_idname, text="Add Ivy to Mesh", + icon='OUTLINER_DATA_CURVE').updateIvy = True + + +def register(): + bpy.utils.register_module(__name__) + bpy.types.INFO_MT_curve_add.append(menu_func) + + +def unregister(): + bpy.types.INFO_MT_curve_add.remove(menu_func) + bpy.utils.unregister_module(__name__) + + +if __name__ == "__main__": + register() diff --git a/tests/test_helpers/extra_objects_blinfo.txt b/tests/test_helpers/extra_objects_blinfo.txt new file mode 100644 index 0000000..f7a28bd --- /dev/null +++ b/tests/test_helpers/extra_objects_blinfo.txt @@ -0,0 +1 @@ +{'name': 'Extra Objects', 'author': 'Multiple Authors', 'version': (0, 1, 2), 'blender': (2, 76, 0), 'location': 'View3D > Add > Curve > Extra Objects', 'description': 'Add extra curve object types', 'warning': '', 'wiki_url': 'https://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Curve/Curve_Objects', 'category': 'Add Curve'} \ No newline at end of file diff --git a/tests/test_helpers/ivy_gen_blinfo.txt b/tests/test_helpers/ivy_gen_blinfo.txt new file mode 100644 index 0000000..7543bc5 --- /dev/null +++ b/tests/test_helpers/ivy_gen_blinfo.txt @@ -0,0 +1 @@ +{'name': 'IvyGen', 'author': 'testscreenings, PKHG, TrumanBlending', 'version': (0, 1, 2), 'blender': (2, 59, 0), 'location': 'View3D > Add > Curve', 'description': 'Adds generated ivy to a mesh object starting at the 3D cursor', 'warning': '', 'wiki_url': 'https://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Curve/Ivy_Gen', 'category': 'Add Curve'} \ No newline at end of file