Newer
Older
bl_info = {
"name": "Icicle Generator",
"author": "Eoin Brennan (Mayeoin Bread)",
"version": (2, 2, 1),
"location": "View3D > Add > Mesh",
"description": "Construct a linear string of icicles of different sizes",
"warning": "",
"wiki_url": "",
"tracker_url": "",
"category": "Add Mesh"
}
import bpy
import bmesh
from mathutils import Vector
from math import (
pi, sin,
cos, atan,
)
from bpy.props import (
EnumProperty,
FloatProperty,
IntProperty,
)
import random
class IcicleGenerator(bpy.types.Operator):
bl_idname = "mesh.icicle_gen"
bl_label = "Icicle Generator"
bl_description = ("Create Icicles on selected Edges of an existing Mesh Object\n"
"Note: doesn't work with vertical Edges")
bl_options = {"REGISTER", "UNDO"}
# Maximum radius
name="Max",
description="Maximum radius of a cone",
default=0.15,
min=0.01,
max=1.0,
unit="LENGTH"
)
name="Min",
description="Minimum radius of a cone",
default=0.025,
min=0.01,
max=1.0,
unit="LENGTH"
)
name="Max",
description="Maximum depth (height) of cone",
default=2.0,
min=0.2,
max=2.0,
unit="LENGTH"
)
name="Min",
description="Minimum depth (height) of cone",
default=1.5,
min=0.2,
max=2.0,
unit="LENGTH"
)
# Number of verts at base of cone
name="Vertices",
description="Number of vertices at the icicle base",
default=8,
min=3,
max=24
)
name="Fill cap",
description="Fill the icicle cone base",
items=[
('NGON', "Ngon", "Fill with Ngons"),
('NOTHING', "None", "Do not fill"),
('TRIFAN', "Triangle fan", "Fill with triangles")
],
default='NGON',
)
# Number of iterations before giving up trying to add cones
# Prevents crashes and freezes
# Obviously, the more iterations, the more time spent calculating.
# Max value (10,000) is safe but can be slow,
# 2000 to 5000 should be adequate for 95% of cases
name="Iterations",
description="Number of iterations before giving up, prevents freezing/crashing",
default=2000,
min=1,
max=10000
)
verticalEdges = False
@classmethod
def poll(cls, context):
obj = context.active_object
return obj and obj.type == "MESH"
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
col.prop(self, "minR")
col.prop(self, "maxR")
col.prop(self, "minD")
col.prop(self, "maxD")
col.prop(self, "verts")
col.prop(self, "addCap", text="")
layout.prop(self, "its")
def execute(self, context):
# Variables
if self.minR > self.maxR:
self.maxR = self.minR
if self.minD > self.maxD:
self.maxD = self.minD
rad = self.maxR
radM = self.minR
depth = self.maxD
minD = self.minD
addCap = self.addCap
self.verticalEdges = False
# --- Nested utility functions START --- #
def test_data(obj):
me = obj.data
is_edges = bool(len(me.edges) > 0)
is_selected = False
for edge in me.edges:
if edge.select:
is_selected = True
break
return (is_edges and is_selected)
def flip_to_edit_mode():
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.mode_set(mode='EDIT')
# Add cone function
def add_cone(x, y, z, randrad, rd, fill="NGON"):
bpy.ops.mesh.primitive_cone_add(
vertices=self.verts,
radius1=randrad,
radius2=0.0,
depth=rd,
end_fill_type=fill,
view_align=False,
location=(x, y, z),
rotation=(pi, 0.0, 0.0)
)
def add_icicles(rad, radM, depth, minD):
pos1 = Vector((0.0, 0.0, 0.0))
pos2 = Vector((0.0, 0.0, 0.0))
pos = 0
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
wm = obj.matrix_world
# Vectors for selected verts
for v in bm.verts:
if v.select:
if pos == 0:
p1 = v.co
pos = 1
elif pos == 1:
p2 = v.co
pos = 2
# Set first to left most vert on X-axis...
if(p1.x > p2.x):
pos1 = p2
pos2 = p1
# Or bottom-most on Y-axis if X-axis not used
elif(p1.x == p2.x):
if(p1.y > p2.y):
pos1 = p2
pos2 = p1
else:
pos1 = p1
pos2 = p2
else:
pos1 = p1
pos2 = p2
# World matrix for positioning
pos1 = pos1 * wm
pos2 = pos2 * wm
# X values not equal, working on X-Y-Z planes
if pos1.x != pos2.x:
# Get the angle of the line
angle = atan((pos2.x - pos1.x) / (pos2.y - pos1.y))
else:
angle = pi / 2
# Total length of line, neglect Z-value (Z only affects height)
xLength = (((pos2.x - pos1.x)**2) + ((pos2.y - pos1.y)**2))**0.5
# Slopes if lines
zSlope = (pos2.z - pos1.z) / (pos2.x - pos1.x)
# Fixes positioning error with some angles
if (angle < 0):
i = pos2.x
j = pos2.y
k = pos2.z
else:
i = pos1.x
j = pos1.y
k = pos1.z
l = 0.0
zInt = k - (zSlope * i)
# if equal values, radius should be that size otherwise randomize it
randrad = rad if (radM == rad) else (rad - radM) * random.random()
# Depth, as with radius above
rd = depth if (depth == minD) else (depth - minD) * random.random()
# Get user iterations
iterations = self.its
# Counter for iterations
c = 0
while (l < xLength) and (c < iterations):
rr = randrad if (radM == rad) else randrad + radM
dd = rd if (depth == minD) else rd + minD
# Icicles generally taller than wider, check if true
# If the new icicle won't exceed line length
# Fix for overshooting lines
if (l + rr + rr <= xLength):
# Using sine/cosine of angle keeps icicles consistently spaced
i = i + (rr) * sin(angle)
j = j + (rr) * cos(angle)
l = l + rr
# Add a cone in new position
add_cone(i, j, (i * zSlope) + (zInt - (dd) / 2), rr, dd, addCap)
# Add another radius to i and j to prevent overlap
i = i + (rr) * sin(angle)
j = j + (rr) * cos(angle)
l = l + rr
# New values for rad and depth
randrad = rad if (radM == rad) else (rad - radM) * random.random()
rd = depth if (depth == minD) else (depth - minD) * random.random()
# If overshoot, try find smaller cone
else:
randrad = rad if (radM == rad) else (rad - radM) * random.random()
rd = depth if (depth == minD) else (depth - minD) * random.random()
# If wider than taller, try find taller than wider
else:
randrad = rad if (radM == rad) else (rad - radM) * random.random()
rd = depth if (depth == minD) else (depth - minD) * random.random()
# Increase iterations by 1
c = c + 1
# If X values equal, then just working in Y-Z plane,
# Provided Y values not equal
elif (pos1.x == pos2.x) and (pos1.y != pos2.y):
# Absolute length of Y line
xLength = ((pos2.y - pos1.y)**2)**0.5
i = pos1.x
j = pos1.y
k = pos1.z
l = 0.0
# Z-slope and intercept
zSlope = (pos2.z - pos1.z) / (pos2.y - pos1.y)
zInt = k - (zSlope * j)
# Same as above for X-Y-Z plane, just X values don't change
randrad = rad if (radM == rad) else (rad - radM) * random.random()
rd = depth if (depth == minD) else (depth - minD) * random.random()
iterations = self.its
c = 0
while(l < xLength) and (c < iterations):
rr = randrad if (radM == rad) else randrad + radM
dd = rd if (depth == minD) else rd + minD
if (dd > rr):
if (l + rr + rr <= xLength):
j = j + (rr)
l = l + (rr)
add_cone(i, j, (i * zSlope) + (zInt - (dd) / 2), rr, dd, addCap)
j = j + (rr)
l = l + (rr)
randrad = rad if (radM == rad) else (rad - radM) * random.random()
rd = depth if (depth == minD) else (depth - minD) * random.random()
randrad = rad if (radM == rad) else (rad - radM) * random.random()
rd = depth if (depth == minD) else (depth - minD) * random.random()
randrad = rad if (radM == rad) else (rad - radM) * random.random()
rd = depth if (depth == minD) else (depth - minD) * random.random()
c = c + 1
else:
# Otherwise X and Y values the same, so either verts are on top of each other
# Or its a vertical line. Either way, we don't like it
self.verticalEdges = True
def runIt(rad, radM, depth, minD):
# Check that min values are less than max values
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
if not((rad >= radM) and (depth >= minD)):
return False
obj = bpy.context.active_object
if obj.mode == 'EDIT':
# List of initial edges
oEdge = []
bm = bmesh.from_edit_mesh(obj.data)
bm.edges.ensure_lookup_table()
for e in bm.edges:
if e.select:
# Append selected edges to list
oEdge.append(e.index)
# For every initially selected edge, add cones
for e in oEdge:
bpy.ops.mesh.select_all(action='DESELECT')
bm.edges.ensure_lookup_table()
bm.edges[e].select = True
add_icicles(rad, radM, depth, minD)
return True
return False
# --- Nested utility functions END --- #
# Run the function
obj = context.active_object
if obj and obj.type == 'MESH':
flip_to_edit_mode()
if not test_data(obj):
self.report({'INFO'},
"Active Mesh has to have at least one selected Edge. Operation Cancelled")
return {"CANCELLED"}
check = runIt(rad, radM, depth, minD)
if check is False:
self.report({'INFO'}, "Operation could not be completed")
if self.verticalEdges:
self.report({'INFO'},
"Some selected vertical Edges were skipped during icicles creation")
return {'FINISHED'}
# Add to menu and register/unregister stuff
def menu_func(self, context):
self.layout.operator(IcicleGenerator.bl_idname, text="Icicle")
def register():
bpy.utils.register_class(IcicleGenerator)
Brecht Van Lommel
committed
bpy.types.VIEW3D_MT_mesh_add.append(menu_func)
def unregister():
bpy.utils.unregister_class(IcicleGenerator)
Brecht Van Lommel
committed
bpy.types.VIEW3D_MT_mesh_add.remove(menu_func)
if __name__ == "__main__":
register()