#!BPY
"""
Name: 'Curve Stroker'
Blender: 245
Group: 'Misc'
Tooltip: 'Strokes a curve with a flat mesh'
"""
__author__ = "Kevin Morgan"
__url__ = ["Author's homepage, http://gamulabs.freepgs.com", "blenderartists.org"]
__version__ = "1.2 5/17/2008"
__email__ = "Please PM user forTe on BlenderArtists.org"
__bpydoc__ = """\
To contact: Please PM user forTe on BlenderArtists.org
This script strokes a bezier curve using either random or smooth parameters
For more information see: http://www.blenderartists.org/forum/showthread.php?t=122047
"""
# Copyright 2008, Kevin Morgan
# --------------------------------------------------------------------------
# ***** 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# ***** END GPL LICENCE BLOCK *****
# --------------------------------------------------------------------------
#############################
# Version 1.0
# "Random Curve Tracer"
# Had the basic functionality outlined in the BA thread
##############################
# Version 1.1
# "Curve Stroker"
# Fixes/Additions:
# 1.) Misspellings -
# 2.) Object Select Bug -
# 3.) Smooth Option -
# 4.) Cyclic Curve Tracing -
# 5.) Unified Terms (Resolution) +
# 6.) New GUI (more organized, fixed errors) -
###############################
# Version 1.2
# 1.) New Spacing Options: Point Spacing -
# 2.) Resolution: Number of points Per Segment -
# 3.) More GUI Work -
###############################
# --Future--
# Needed Improvements:
# 1.) More Generic Algorithm for Smoothed versus Random
# 2.) More stroking options?
# 3.) Better Support for 3D Curves (Using the twist attribute)
# 4.) Reintroduce U Resolution Settings/Options
# 5.) Number of points per curve
###############################
import Blender
from Blender import Draw
from Blender import BGL
from Blender import Mathutils as mu
import bpy
#The global dictionary for the GUI
PARAMS = {
'MinWidth': Draw.Create(0.1),
'Width': Draw.Create(0.5),
'Smooth': Draw.Create(0),
'AvgDist': Draw.Create(0.1),
'UseDist': Draw.Create(1),
'PerSeg': Draw.Create(0),
'NPerSeg': Draw.Create(50),
'Resolution': Draw.Create(100)
}
#Button Events
DO_SCRIPT = 1
SLIDE = 2
DTOG = 3
STOG = 4
NO_EVT = 1000
REDRAW = 1001
# This function is taken from Campbell Barton's Tree Wizard Script,
# which is okay, since he ripped it from the Blender Source ;)
def bezierDiff(q0, q1, q2, q3, pointlist, steps, axis):
f = float(steps)
rt0 = q0
rt1 = 3.0*(q1-q0)/f
f *= f
rt2 = 3.0*(q0-2.0*q1+q2)/f
f *= steps
rt3 = (q3 - q0+3.0*(q1-q2))/f
q0 = rt0
q1 = rt1+rt2+rt3
q2 = 2*rt2 + 6*rt3
q3 = 6*rt3
if axis == None:
for a in xrange(steps+1):
pointlist[a] = q0
q0 += q1
q1 += q2
q2 += q3
else:
for a in xrange(steps+1):
pointlist[a][axis] = q0
q0 += q1
q1 += q2
q2 += q3
# Generates the requested number of points between two bezier handles (taken from Campbell's Script as well)
def pointsFromBezierSegment(steps, pointlist, bez1_vec, bez2_vec):
for ii in (0, 1, 2):
bezierDiff(bez1_vec[1][ii], bez1_vec[2][ii], bez2_vec[0][ii], bez2_vec[1][ii], pointlist, steps, ii)
# Generates normals from coordinate locations.
# It calculates the tangent at the point and then performs the following transform:
# f' = (x, y, z) => normal = (y, -x, z)
# Direction is unimportant since verts are translated along the positive and negative normals
# The accuracy of this algorithm improves with the amount of points
def normalsFromPoints(pointlist, normallist):
for i in xrange(len(pointlist)):
if i == 0:
tangent = mu.Vector(pointlist[i+1]) - mu.Vector(pointlist[i])
elif i == len(pointlist)-1:
tangent = mu.Vector(pointlist[i]) - mu.Vector(pointlist[i-1])
else:
tangent = mu.Vector(pointlist[i+1]) - mu.Vector(pointlist[i-1])
normallist[i] = mu.Vector(tangent[1], -tangent[0], tangent[2]).normalize()
# Performs integration to determine the length of the curve given the points on the
# curve by summing the magnitude of the distance between points
# Like the normals calculation, accuracy improves with the number of samples
def curveLength(pointlist):
cLen = 0
for i in xrange(len(pointlist[:-1])):
diff = mu.Vector(pointlist[i+1]) - mu.Vector(pointlist[i])
cLen += diff.length
return cLen
# Not actually a Probability Distribution function, but I was at a loss for function names
# Its primary function is to return a boolean value based off random numbers
def pdf():
chance1 = mu.Rand(0, 10)
chance2 = mu.Rand(0, 15)
testVal = chance1 - chance2
if testVal < 0:
return 0
if testVal >= 0:
return 1
# Do the actual random tracing of the curve
def traceCurve():
global PARAMS
# Loads the usual
scn = bpy.data.scenes.active
# Grab the Selected Curve
curveOb = scn.objects.active
if not curveOb:
ErrorBox('No Object Selected')
return
obMatrix = curveOb.matrix
if curveOb.getType() != 'Curve':
ErrorBox('Object Must Be a Curve Object')
return
curveData = curveOb.data
if curveData[0].type != 1:
ErrorBox('Curve Must be a Bezier Curve')
return
resolution = PARAMS['Resolution'].val
useDist = PARAMS['UseDist'].val
useSeg = PARAMS['PerSeg'].val
# Steps are now based off of resolution value (no longer directly exposed to user as v1.2)
if useDist:
avgDist = PARAMS['AvgDist'].val
steps = resolution/avgDist
elif useSeg:
numPerSeg = PARAMS['NPerSeg'].val
steps = resolution * numPerSeg
else:
steps = curveData.resolu * resolution
vpoints = []
normals = []
for sN, spline in enumerate(curveData):
for p in xrange(len(spline)-1):
pointlist =[[None, None, None] for i in xrange(steps+1)]
normallist = [None for i in xrange(steps + 1)]
pointsFromBezierSegment(steps, pointlist, spline[p].vec, spline[p+1].vec)
normalsFromPoints(pointlist, normallist)
if useDist:
pointLen = len(pointlist) - 1 # Don't use the point under the handle
cLen = curveLength(pointlist)
numPoints = cLen/avgDist
for i in xrange(0, pointLen, int(pointLen/numPoints)):
vpoints.append(pointlist[i])
normals.append(normallist[i])
elif useSeg:
pointLen = len(pointlist) - 1
for i in xrange(0, pointLen, int(pointLen/numPerSeg)):
vpoints.append(pointlist[i])
normals.append(normallist[i])
else:
# Use Random Placement
# Don't use the end handles as a point
for ii, point in enumerate(pointlist[:-2]):
randProb = pdf()
if randProb or ii == 0 or PARAMS['Smooth'].val:
vpoints.append(point)
normals.append(normallist[ii])
#Add the final extrusion
if p == len(spline)-2:
vpoints.append(pointlist[-1])
normals.append(normallist[-1])
# Need to do the -1 to 0 segment on cyclic curves
# Same Functionality as above, but special case for cyclic curves. Consider combining somehow?
if spline.isCyclic():
pointlist = [[None, None, None] for i in xrange(steps+1)]
normallist = [None for i in xrange(steps + 1)]
pointsFromBezierSegment(steps, pointlist, spline[-1].vec, spline[0].vec)
normalsFromPoints(pointlist, normallist)
if useDist:
pointLen = len(pointlist) - 1 # Don't use the point under the handle
cLen = curveLength(pointlist)
numPoints = cLen/avgDist
for i in xrange(0, pointLen, int(pointLen/numPoints)):
vpoints.append(pointlist[i])
normals.append(normallist[i])
elif useSeg:
pointLen = len(pointlist) - 1
for i in xrange(0, pointLen, int(pointLen/numPerSeg)):
vpoints.append(pointlist[i])
normals.append(normallist[i])
else:
for ii, point in enumerate(pointlist[:-2]):
randProb = pdf()
if (randProb or PARAMS['Smooth'].val) and ii > 0: #0th point already added in above algorithm
vpoints.append(point)
normals.append(normallist[ii])
maxWidth = PARAMS['Width'].val
minWidth = PARAMS['MinWidth'].val
verte = []
for i, point in enumerate(vpoints):
if PARAMS['Smooth'].val:
dist = normals[i] * (maxWidth * 0.5)
else:
dist = normals[i] * mu.Rand(minWidth, maxWidth) * 0.5
point1 = (mu.Vector(point) + dist) * obMatrix
point2 = (mu.Vector(point) - dist) * obMatrix
verte.append(point1)
verte.append(point2)
faces = [[i, i+1, i+3, i+2] for i in xrange(0, len(verte)-2, 2)]
#Need to build another face on cyclic curves to complete them
if spline.isCyclic():
faces.append([0, 1, len(verte)-1, len(verte)-2])
me = bpy.data.meshes.new('StrokeCurve')
me.verts.extend(verte)
me.faces.extend(faces)
scn.objects.new(me, 'StrokeCurve')
return
# GUI FUNCTIONS
BGColor = [0.7, 0.7, 0.7]
MainColor = [0.9, 0.9, 0.9]
BorderColor = [0.0, 0.0, 0.0]
TitleColor = [0.8, 0.1, 0.1]
TextColor = [0.1, 0.1, 0.1]
def ErrorBox(stri):
Draw.PupMenu('ERROR:%t|' + stri + '%x0')
#Double Border Box
def BoxBorder2(x1, y1, x2, y2, col, bcol):
BGL.glColor3f(bcol[0], bcol[1], bcol[2])
BGL.glRecti(x1-4, y1-4, x2+4, y2+4)
BGL.glColor3f(bcol[0] + 0.5, bcol[1] + 0.5, bcol[2]+0.5)
BGL.glRecti(x1-3, y1-3, x2+3, y2+3)
BGL.glColor3f(bcol[0], bcol[1], bcol[2])
BGL.glRecti(x1-1, y1-1, x2+1, y2+1)
BGL.glColor3f(col[0], col[1], col[2])
BGL.glRecti(x1, y1, x2, y2)
#Determines location for centered text
def CenterText(leftBound, rightBound, string, size):
wid = Draw.GetStringWidth(string, size)
boundW = rightBound - leftBound
return (int(boundW * 0.5))-(int(wid * 0.5))
def Gui():
global PARAMS
BGL.glClearColor(BGColor[0], BGColor[1], BGColor[2], 1.0)
BGL.glClear(BGL.GL_COLOR_BUFFER_BIT)
maxx = 305
maxy = 305
minx = miny = 5
BoxBorder2(minx, miny, maxx, maxy, MainColor, BorderColor)
rows = [i for i in xrange(miny, maxy, 25)]
title = "Curve Stroker v1.2"
titleTextCol = CenterText(minx, maxx, title, "large")
BGL.glColor3f(TitleColor[0], TitleColor[1], TitleColor[2])
BGL.glRasterPos2i(titleTextCol, rows[-1])
Draw.Text(title, "large")
row = -2
BGL.glColor3f(TextColor[0], TextColor[1], TextColor[2])
BGL.glRasterPos2i(minx+2, rows[row])
Draw.Text("Samples:")
PARAMS['UseDist'] = Draw.Toggle("Average Distance", DTOG, minx+2, rows[row-1], int((maxx - (minx+2)) / 2)-1, 19, PARAMS['UseDist'].val, "When Enabled, Uses an Average Distance Between Samples")
PARAMS['PerSeg'] = Draw.Toggle("Per Segment", STOG, int((maxx - (minx+2)) / 2) + minx+1, rows[row-1], int((maxx - (minx+2)) / 2) -1, 19, PARAMS['PerSeg'].val, "When Enabled, Attempts to Use a Fixed Number of Points Per Curve Segment")
if PARAMS['UseDist'].val:
PARAMS['AvgDist'] = Draw.Slider("Dist: ", NO_EVT, minx+2, rows[row-2], maxx-(minx+4), 19, PARAMS['AvgDist'].val, 0.001, 0.5, 0, "Average Distance between Base Points")
if PARAMS['PerSeg'].val:
PARAMS['NPerSeg'] = Draw.Slider("Num: ", NO_EVT, minx+2, rows[row-2], maxx-(minx+4), 19, PARAMS['NPerSeg'].val, 10, 1000, 0, "Number of Points per Segment")
if not PARAMS['PerSeg'].val and not PARAMS['UseDist'].val:
BGL.glColor3f(TextColor[0], TextColor[1], TextColor[2])
BGL.glRasterPos2i(minx+2, rows[row-2])
Draw.Text("Default: Random Points with U Resolution as Base")
PARAMS['Resolution'] = Draw.Slider("Res: ", NO_EVT, minx+2, rows[row-3], maxx-(minx+4), 19, PARAMS['Resolution'].val, 10, 1000, 0, "Resolution Multiplier: higher improves accuracy for Distance and Fixed Count Calculations, but can reduce speed")
row -= 4
BGL.glColor3f(TextColor[0], TextColor[1], TextColor[2])
BGL.glRasterPos2i(minx+2, rows[row])
Draw.Text("Stroke Settings:")
PARAMS['Smooth'] = Draw.Toggle("Smooth Stroke", NO_EVT, minx+2, rows[row-1], 100, 19, PARAMS['Smooth'].val, "Use a smooth stroke, ignoring random parameters and using the Maximum Width as a Stroke Setting")
PARAMS['MinWidth'] = Draw.Slider("Min Width: ", SLIDE, minx+2, rows[row-2], maxx - minx - 4, 19, PARAMS['MinWidth'].val, 0.001, 10.0, 0, "Set the Minimum Stroke Width for Drawing")
PARAMS['Width'] = Draw.Slider("Max Width: ", SLIDE, minx+2, rows[row-3], maxx - minx - 4, 19, PARAMS['Width'].val, 0.001, 10.0, 0, "Set the Maximum Stroke Width for Drawing")
row -= 4
Draw.PushButton("Go", DO_SCRIPT, minx+2, rows[0]+4, maxx - minx - 4, rows[row]-rows[0], "Run Script")
def Bevt(evt):
global PARAMS
if evt == DO_SCRIPT:
Blender.Window.DrawProgressBar(0.0, "Stroking Curve...")
traceCurve()
Blender.Window.DrawProgressBar(1.0, "Finished...")
Blender.Window.RedrawAll()
elif evt == SLIDE:
if PARAMS['MinWidth'].val > PARAMS['Width'].val:
temp = PARAMS['Width'].val
PARAMS['Width'].val = PARAMS['MinWidth'].val
PARAMS['MinWidth'].val = temp
elif PARAMS['MinWidth'].val == PARAMS['Width'].val:
PARAMS['MinWidth'].val -= 0.001
Draw.Redraw(1)
elif evt == DTOG:
if PARAMS['UseDist'].val and PARAMS['PerSeg'].val:
PARAMS['PerSeg'].val = 0
Draw.Redraw(1)
elif evt == STOG:
if PARAMS['UseDist'].val and PARAMS['PerSeg'].val:
PARAMS['UseDist'].val = 0
Draw.Redraw(1)
else:
Draw.Redraw(1)
def Evt(evt, val):
if evt == Draw.ESCKEY:
Draw.Exit()
return
Draw.Register(Gui, Evt, Bevt)