#!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)