Improve make_repo.py
This cleans up make_repo.py a bit, using file extensions to determine file type. This also loosens the testing repo generation, as the existing test required matching a predifed expected output which had to be updated on every change (essentially making it a moot test, as the reference output was obtained from the functions output). The new test just checks if the output has the same number of packages as the input dir has addons. Tips on how best to test these sorts of "higher level" functions (if at all) would be welcome :)
This commit is contained in:
145
make_repo.py
145
make_repo.py
@@ -54,86 +54,120 @@ def extract_blinfo(item: Path) -> dict:
|
||||
"""
|
||||
|
||||
blinfo = None
|
||||
addon_name = item.name
|
||||
|
||||
if not item.exists():
|
||||
raise FileNotFoundError("Cannot extract blinfo from '%s'; no such file or directory" % item)
|
||||
|
||||
if item.is_dir():
|
||||
fname = item / '__init__.py'
|
||||
|
||||
try:
|
||||
with fname.open("r") as f:
|
||||
blinfo = parse_blinfo(f.read())
|
||||
f = (item / '__init__.py').open("r")
|
||||
except FileNotFoundError as err:
|
||||
# directory with no __init__.py: not an addon
|
||||
raise BadAddon("Directory '%s' doesn't contain __init__.py; not a python package" % item) from err
|
||||
with f:
|
||||
blinfo = parse_blinfo(f.read())
|
||||
|
||||
elif item.is_file():
|
||||
try:
|
||||
with zipfile.ZipFile(str(item), 'r') as z:
|
||||
if len(z.namelist()) == 1:
|
||||
# zipfile with one item: just read that item
|
||||
blinfo = parse_blinfo(z.read(z.namelist()[0]))
|
||||
else:
|
||||
# zipfile with multiple items: try all __init__.py files
|
||||
for fname in z.namelist():
|
||||
# TODO: zips with multiple bl_infos might be a problem,
|
||||
# not sure how such cases should be handled (if at all)
|
||||
# for now we just break after the first one
|
||||
if fname.endswith('__init__.py'):
|
||||
try:
|
||||
blinfo = parse_blinfo(z.read(fname))
|
||||
break
|
||||
except BadAddon:
|
||||
continue
|
||||
if blinfo is None:
|
||||
raise BadAddon("Zipfile '%s' doesn't contain a readable bl_info dict" % item)
|
||||
|
||||
except zipfile.BadZipFile:
|
||||
# If it's not a valid zip, assume file is just a normal file
|
||||
ext = item.suffix.lower()
|
||||
if ext == '.zip':
|
||||
try:
|
||||
with item.open() as f:
|
||||
blinfo = parse_blinfo(f.read())
|
||||
except: #HACK
|
||||
# If it's not a zip and its not parse-able python, then it's not an addon
|
||||
raise BadAddon("File '%s' doesn't appear to be an addon" % item)
|
||||
with zipfile.ZipFile(str(item), 'r') as z:
|
||||
if len(z.namelist()) == 1:
|
||||
# zipfile with one item: just read that item
|
||||
blinfo = parse_blinfo(z.read(z.namelist()[0]))
|
||||
else:
|
||||
# zipfile with multiple items: try all __init__.py files
|
||||
for fname in z.namelist():
|
||||
# TODO: zips with multiple bl_infos might be a problem,
|
||||
# not sure how such cases should be handled (if at all)
|
||||
# for now we just break after the first one
|
||||
if fname.endswith('__init__.py'):
|
||||
try:
|
||||
blinfo = parse_blinfo(z.read(fname))
|
||||
break
|
||||
except BadAddon:
|
||||
continue
|
||||
if blinfo is None:
|
||||
raise BadAddon("Zipfile '%s' doesn't contain a readable bl_info dict" % item)
|
||||
except zipfile.BadZipFile as e:
|
||||
raise BadAddon("Bad zipfile '%s'" % item) from e
|
||||
|
||||
elif ext == '.py':
|
||||
with item.open() as f:
|
||||
blinfo = parse_blinfo(f.read())
|
||||
|
||||
else:
|
||||
raise BadAddon("File '%s' doesn't have a .zip or .py extension; not an addon" % item)
|
||||
|
||||
# This should not happen
|
||||
if blinfo == None:
|
||||
raise RuntimeError("Could not read addon '%s'" % addon_name)
|
||||
if blinfo is None:
|
||||
raise RuntimeError("Could not read addon '%s'" % item.name)
|
||||
|
||||
return blinfo
|
||||
|
||||
|
||||
class Package:
|
||||
def __init__(self, path: Path, bl_info: dict, baseurl=None):
|
||||
self.bl_info = bl_info
|
||||
self.path = path
|
||||
self.url = None
|
||||
|
||||
def make_repo(path: Path):
|
||||
def dict(self) -> dict:
|
||||
return {
|
||||
'bl_info': self.bl_info,
|
||||
'url': self.url,
|
||||
}
|
||||
|
||||
class Repository:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self.url = None
|
||||
self.packages = []
|
||||
|
||||
def add_package(self, pkg: Package):
|
||||
# if pkg.url is None:
|
||||
# pkg.url =
|
||||
self.packages.append(pkg)
|
||||
|
||||
def dict(self) -> dict:
|
||||
return {
|
||||
'name': self.name,
|
||||
'packages': [p.dict() for p in self.packages],
|
||||
'url': self.url,
|
||||
}
|
||||
|
||||
def dump(self, path: Path):
|
||||
with (path / 'repo.json').open('w', encoding='utf-8') as repo_file:
|
||||
json.dump(self.dict(), repo_file, indent=4, sort_keys=True)
|
||||
log.info("repo.json written to %s" % path)
|
||||
|
||||
|
||||
def json(self) -> str:
|
||||
return json.dumps(self.__dict__)
|
||||
|
||||
|
||||
def make_repo(path: Path, name: str) -> Repository:
|
||||
"""Make repo.json for files in directory 'path'"""
|
||||
|
||||
repo_data = {}
|
||||
package_data = []
|
||||
repo = Repository(name)
|
||||
|
||||
if not path.is_dir():
|
||||
raise FileNotFoundError(path)
|
||||
|
||||
for addon, bl_info in iter_addons(path):
|
||||
package_datum = {}
|
||||
|
||||
# Check if we have all bl_info fields we want
|
||||
if not REQUIRED_KEYS.issubset(set(bl_info)):
|
||||
log.warning(
|
||||
"Required key(s) '{}' not found in bl_info of '{}'".format(
|
||||
"', '".join(REQUIRED_KEYS.difference(set(bl_info))), addon)
|
||||
)
|
||||
|
||||
package_datum['bl_info'] = bl_info
|
||||
package_datum['type'] = 'addon'
|
||||
package_data.append(package_datum)
|
||||
|
||||
repo_data['packages'] = package_data
|
||||
package = Package(addon, bl_info)
|
||||
repo.add_package(package)
|
||||
|
||||
log.info("Repository generation successful")
|
||||
cwd = Path.cwd()
|
||||
dump_repo(cwd, repo_data)
|
||||
log.info("repo.json written to %s" % cwd)
|
||||
return repo
|
||||
|
||||
|
||||
def main():
|
||||
@@ -143,18 +177,25 @@ def main():
|
||||
help="Increase verbosity (can be used multiple times)",
|
||||
action="count",
|
||||
default=0)
|
||||
parser.add_argument('-n', '--name',
|
||||
help="Name of repo (defaults to basename of 'path')")
|
||||
parser.add_argument('-o', '--output',
|
||||
help="Directory in which to write repo.json file",
|
||||
type=Path,
|
||||
default=Path.cwd())
|
||||
parser.add_argument('path',
|
||||
type=Path,
|
||||
nargs='?',
|
||||
default=Path.cwd(),
|
||||
help="Path to addon directory")
|
||||
|
||||
args = parser.parse_args()
|
||||
log.level = args.verbose
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(levelname)8s: %(message)s')
|
||||
if args.name is None:
|
||||
args.name = args.path.name
|
||||
|
||||
make_repo(args.path)
|
||||
logging.basicConfig(format='%(levelname)8s: %(message)s', level=logging.INFO)
|
||||
log.level += args.verbose
|
||||
|
||||
repo = make_repo(args.path, args.name)
|
||||
repo.dump(args.output)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
BIN
tests/test_helpers/addons/invalid_addon.ZiP
Normal file
BIN
tests/test_helpers/addons/invalid_addon.ZiP
Normal file
Binary file not shown.
763
tests/test_helpers/addons/singlefile_addon.PY
Normal file
763
tests/test_helpers/addons/singlefile_addon.PY
Normal file
@@ -0,0 +1,763 @@
|
||||
# ##### 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",
|
||||
# "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",
|
||||
# }
|
||||
|
||||
# just use one blinfo for all addons to simplify testing
|
||||
bl_info = {
|
||||
"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"
|
||||
}
|
||||
|
||||
|
||||
|
||||
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()
|
@@ -17,29 +17,35 @@ class test_make_repo(unittest.TestCase):
|
||||
|
||||
def test_extract_blinfo_from_nonexistent(self):
|
||||
test_file = 'file_that_doesnt_exist'
|
||||
self.assertRaises(
|
||||
FileNotFoundError,
|
||||
make_repo.extract_blinfo,
|
||||
self.addon_path / test_file
|
||||
)
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
make_repo.extract_blinfo(self.addon_path / test_file)
|
||||
|
||||
def test_make_repo_valid(self):
|
||||
make_repo.make_repo(self.helper_path / 'addons')
|
||||
repojson = Path.cwd() / 'repo.json'
|
||||
reference_repojson = self.helper_path / 'repo.json'
|
||||
# def test_make_repo_valid(self):
|
||||
# reference_repo = make_repo.make_repo(self.helper_path / 'addons', "test repo")
|
||||
# repojson = Path.cwd() / 'repo.json'
|
||||
# reference_repojson = self.helper_path / 'repo.json'
|
||||
#
|
||||
# try:
|
||||
# with repojson.open('r') as repolist_f:
|
||||
# with reference_repojson.open('r') as ref_repolist_f:
|
||||
# repolist = json.loads(repolist_f.read())
|
||||
# ref_repolist = json.loads(ref_repolist_f.read())
|
||||
# self.assertEqual(repolist, ref_repolist)
|
||||
# finally:
|
||||
# # repojson.unlink()
|
||||
# pass
|
||||
|
||||
try:
|
||||
with repojson.open('r') as repolist_f:
|
||||
with reference_repojson.open('r') as ref_repolist_f:
|
||||
repolist = json.loads(repolist_f.read())
|
||||
ref_repolist = json.loads(ref_repolist_f.read())
|
||||
self.assertEqual(repolist, ref_repolist)
|
||||
finally:
|
||||
repojson.unlink()
|
||||
def test_package_quantity(self):
|
||||
repo = make_repo.make_repo(self.addon_path, "name of the repo")
|
||||
acceptible_addons = [
|
||||
f for f in self.addon_path.iterdir()
|
||||
if not f.match('*nonaddon*')
|
||||
]
|
||||
self.assertEqual(len(repo.packages), len(acceptible_addons))
|
||||
|
||||
def test_make_repo_from_nonexistent(self):
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
make_repo.make_repo(Path('in_a_galaxy_far_far_away'))
|
||||
make_repo.make_repo(Path('in_a_galaxy_far_far_away'), "somename")
|
||||
|
||||
# addons which should contain bl_infos
|
||||
yes_blinfo = [
|
||||
|
Reference in New Issue
Block a user