Repository: Jandals/HairNet
Branch: master
Commit: d0a0db0a9265
Files: 6
Total size: 44.0 KB
Directory structure:
gitextract_dv34wfql/
├── .gitignore
├── README.md
└── hairNet/
├── Version Notes.txt
├── __init__.py
├── hairNet.py
└── import_properties.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.project
.pydevproject
Blend Files/HairNet 2.8 Debug.blend
*.blend
*.blend1
src/__pycache__/HairNet.cpython-37.pyc
src/Run.py
src/simp.py
*.pyc
.settings/org.eclipse.ltk.core.refactoring.prefs
Release/
prev/__init__.py
prev/hairNet.py
prev/import_properties.py
hairNet/Run.py
hairNet/simp.py
hairNet/hairNet.zip
================================================
FILE: README.md
================================================
# Attention:
HairNet now works in Blender 2.80.
The buttons for the interface have moved to the Numeric Panel in the 3DView (N-key)
HairNet for 2.79 still works.
# HairNet
HairNet addon for Blender
Blender Wiki page:
https://en.blender.org/index.php/Extensions:2.6/Py/Scripts/Objects/HairNet
Blenderartists Thread:
https://blenderartists.org/t/hair-guides-created-from-mesh-objects/572642
Installation:
Visit the "Releases" page and download version 0.5.1.
https://github.com/Jandals/HairNet/releases
Then use Blender's "User Preferences" window to "Install Addon From File." Choose the ZIP file you just downloaded and that should be all.
================================================
FILE: hairNet/Version Notes.txt
================================================
0.6.1
updated object selection call
0.6.0
Updated many calls for Blender 2.80
Moved interface to 3D_View to fix polling problem while switching to particle edit mode
uses depsgraph to restore editing particle coordinates
0.4.6
Fixed an issue which cause hairs to jump out of place when a second or third hair system is created. The solution was to remove the commands to connect and disconnect the hair system(s).
Changed the behavior of Execute() to pass an existing hair system's name to createHair() so that users don't need to manually select the proper particle system.
0.4.7
Proxy objects have a String tag which leads HairNet to a template hair system setting in the Blend File.
0.4.8
We can now interpolate a hair mesh to create additional hairs in between those defined by the mesh. Next up is to interpolate each hair and subdivide it.
0.4.9
Hair systems can now be applied to a proxy mesh as well as to a head object. The choice is made by how the objects are selected.
0.4.10
Undid the change to using operator overrides for creating particle systems. It was being done incorrectly and I can't find the proper way to do it.
0.4.11
Restored Making hair from curves.
================================================
FILE: hairNet/__init__.py
================================================
# ##### 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 #####
bl_info = {
"name":"hairNet",
"author": "Rhett Jackson",
"version": (0,6,6),
"blender": (2,90,0),
"location": "Properties",
"category": "Particle",
"description": "Creates a particle hair system with hair guides from mesh edges which start at marked seams.",
"wiki_url": "http://wiki.blender.org/index.php?title=Extensions:2.6/Py/Scripts/Objects/HairNet",
"tracker_url":"http://projects.blender.org/tracker/index.php?func=detail&aid=35062&group_id=153&atid=467"
}
if "bpy" in locals():
import importlib
importlib.reload(hairNet)
importlib.reload(import_properties)
else:
from . import hairNet
from . import import_properties
import bpy
# ### REGISTER ###
def register():
hairNet.register()
#import_properties.register()
def unregister():
hairNet.unregister()
#import_properties.unregister()
if __name__ == "__main__":
register()
================================================
FILE: hairNet/hairNet.py
================================================
#---------------------------------------------------
# File HairNet.py
# Written by Rhett Jackson April 1, 2013
# Some routines were copied from "Curve Loop" by Crouch https://sites.google.com/site/bartiuscrouch/scripts/curveloop
# Some routines were copied from other sources
# Very limited at this time:
# NB 1) After running the script to create hair, the user MUST manually enter Particle Mode on the Head object and "touch" each point of each hair guide. Using a large comb brish with very low strength is a good way to do this. If it's not done, the hair strands are likely to be reset to a default/straight-out position during editing.
# NB 2) All meshes must have the same number of vertices in the direction that corresponds to hair growth
#---------------------------------------------------
import bpy
import mathutils
import os
import traceback
from mathutils import Vector
from bpy.props import *
from . import_properties import *
from pathlib import Path
versionString = "0.6.5"
#Start Debug
hnDebFile = os.path.join(os.path.dirname(__file__), 'hairNetDeb.txt')
if Path(hnDebFile).is_file():
print("HN Debug File Exists")
import sys
pydev_path = '/Users/rhett/.p2/pool/plugins/org.python.pydev.core_7.7.0.202008021154/pysrc'
if sys.path.count(pydev_path) < 1: sys.path.append(pydev_path)
import pydevd
pydevd.settrace(stdoutToServer=True, stderrToServer=True, suspend=False)
else:
print("HN Debug File Doesn't exist")
#End Debug
class UnionFindList: # a combination of unionfind and singlelinkedlist
def __init__(self, n):
self.parent = [ i for i in range(n) ]
self.next = [ -1 for _ in range(n) ]
self.rank = [ 1 for _ in range(n) ]
def findRoots(self):
return [ i for i, v in enumerate(self.parent) if i == v and self.rank[i] != 1 ] # ignore single point
def findRoot(self, x):
if self.parent[x] == x:
return x
self.parent[x] = self.findRoot(self.parent[x])
return self.parent[x]
def getNext(self, x): # return -1 if no next
return self.next[x]
def getChainLength(self, x): # x must be the root
return self.rank[x]
def getChain(self, x):
ret = []
x_next = x
while x_next != -1:
ret.append(x_next)
x_next = self.next[x_next]
return ret
def reverseChain(self, x_root, x): # x_root is the head of list, x is the end of list
if self.rank[x_root] == 1: return
x_pre = -1
x_cur = x_root
while x_cur != -1:
x_next = self.next[x_cur]
self.next[x_cur] = x_pre
self.parent[x_cur] = x # parent of all nodes should be x after reversing
x_pre = x_cur
x_cur = x_next
self.rank[x] = self.rank[x_root]
def union(self, x, y):
x_root = self.findRoot(x)
y_root = self.findRoot(y)
if x_root == y_root: # already connected
return
if y_root != y and x_root != x:
# Case 1: two root points are independent, should reverse one of chains, choose shorter chain to reverse
# and transform this situation to Case 2
if self.rank[x_root] <= self.rank[y_root]:
self.reverseChain(x_root, x)
x_root = x
else:
self.reverseChain(y_root, y)
y_root = y
if y_root == y: # Case 2: one of two points is dependent or all two points are dependent
if self.next[x] != -1:
self.parent[x] = y
self.next[y] = x
self.rank[y] = self.rank[y] + self.rank[x] # y become new root
else:
self.parent[y] = x_root
self.next[x] = y
self.rank[x_root] = self.rank[x_root] + self.rank[y]
elif x_root == x:
if self.next[y] != -1:
self.parent[y] = x
self.next[x] = y
self.rank[x] = self.rank[x] + self.rank[y] # x become new root
else:
self.parent[x] = y_root
self.next[y] = x
self.rank[y_root] = self.rank[y_root] + self.rank[x]
# It is always good to use wrapper prop when attacking to common data block such as Object to reduce blend junk
class HairNetConfig(PropertyGroup):
masterHairSystem: StringProperty(
name="hnMasterHairSystem",
description="Name of the hair system to be copied by this proxy object.",
default="")
isHairProxy: BoolProperty(
name="hnIsHairProxy",
description="Is this object a hair proxy object?",
default=False)
isEmitter: BoolProperty(
name="hnIsEmitter",
description="Is this object a hair emitter object?",
default=False)
sproutHairs: IntProperty(
name="hnSproutHairs",
description="Number of additional hairs to add.",
default=0)
# subdivideHairSections: IntProperty(
# name="hnSubdivideHairSections",
# description="Number of subdivisions to add along the guide hairs",
# default=0)
def debPrintVertEdges(vert_edges):
print("vert_edges: ")
for vert in vert_edges:
print(vert, ": ", vert_edges[vert])
def debPrintEdgeFaces(edge_faces):
print("edge_faces: ")
for edge in edge_faces:
print(edge, ": ", edge_faces[edge])
def debPrintEdgeKeys(edges):
for edge in edges:
print(edge, " : ", edge.key)
def debPrintHairGuides(hairGuides):
print("Hair Guides:")
guideN=0
for group in hairGuides:
print("Guide #",guideN)
i=0
for guide in group:
print(i, " : ", guide)
i += 1
guideN+=1
def debPrintSeams(seamVerts, seamEdges):
print("Verts in the seam: ")
for vert in seamVerts:
print(vert)
print("Edges in the seam: ")
for edge in seamEdges:
print(edge.key)
def debPrintLoc(func=""):
obj = bpy.context.object
print(obj.name, " ", func)
print("Coords", obj.data.vertices[0].co)
def getEdgeFromKey(mesh,key):
v1 = key[0]
v2 = key[1]
theEdge = 0
for edge in mesh.edges:
if v1 in edge.vertices and v2 in edge.vertices:
#print("Found edge :", edge.index)
return edge
return 0
# returns all edge loops that a vertex is part of
def getLoops(obj, v1, vert_edges, edge_faces, seamEdges):
debug = False
me = obj.data
if not vert_edges:
# Create a dictionary with the vert index as key and edge-keys as value
#It's a list of verts and the keys are the edges the verts belong to
vert_edges = dict([(v.index, []) for v in me.vertices if v.hide!=1])
for ed in me.edges:
for v in ed.key:
if ed.key[0] in vert_edges and ed.key[1] in vert_edges:
vert_edges[v].append(ed.key)
if debug: debPrintVertEdges(vert_edges)
if not edge_faces:
# Create a dictionary with the edge-key as key and faces as value
# It's a list of edges and the faces they belong to
edge_faces = dict([(ed.key, []) for ed in me.edges if (me.vertices[ed.vertices[0]].hide!=1 and me.vertices[ed.vertices[1]].hide!=1)])
for f in me.polygons:
for key in f.edge_keys:
if key in edge_faces and f.hide!=1:
edge_faces[key].append(f.index)
if debug : debPrintEdgeFaces(edge_faces)
ed_used = [] # starting edges that are already part of a loop that is found
edgeloops = [] # to store the final results in
for ed in vert_edges[v1.index]: #ed is all the edges v1 is a part of
if ed in ed_used:
continue
seamTest = getEdgeFromKey(me, ed)
if seamTest.use_seam:
#print("Edge ", seamTest.index, " is a seam")
continue
vloop = [] # contains all verts of the loop
poles = [] # contains the poles at the ends of the loop
circle = False # tells if loop is circular
n = 0 # to differentiate between the start and the end of the loop
for m in ed: # for each vert in the edge
n+=1
active_ed = ed
active_v = m
if active_v not in vloop:
vloop.insert(0,active_v)
else:
break
stillGrowing = True
while stillGrowing:
stillGrowing = False
active_f = edge_faces[active_ed] #List of faces the edge belongs to
new_ed = vert_edges[active_v] #list of edges the vert belongs to
if len(new_ed)<3: #only 1 or 2 edges
break
if len(new_ed)>4: #5-face intersection
# detect poles and stop growing
if n>1:
poles.insert(0,vloop.pop(0))
else:
poles.append(vloop.pop(-1))
break
for i in new_ed: #new_ed - must have 3 or 4 edges coming from the vert
eliminate = False # if edge shares face, it has to be eliminated
for j in edge_faces[i]: # j is one of the face indices in edge_faces
if j in active_f:
eliminate = True
break
if not eliminate: # it's the next edge in the loop
stillGrowing = True
active_ed = i
if active_ed in vert_edges[v1.index]: #the current edge contains v1
ed_used.append(active_ed)
for k in active_ed:
if k != active_v:
if k not in vloop:
if n>1:
vloop.insert(0,k)
else:
vloop.append(k)
active_v = k
break
else:
stillGrowing = False # we've come full circle
circle = True
break
#TODO: Function to sort vloop. Use v1 and edge data to walk the ring in order
vloop = sortLoop(obj, vloop, v1, seamEdges, vert_edges)
edgeloops.append([vloop, poles, circle])
for loop in edgeloops:
for vert in loop[0]:
me.vertices[vert].select=True
#me.edges[edge].select=True
return edgeloops, vert_edges, edge_faces
def getSeams(obj):
debug = False
#Make a list of all edges marked as seams
error = 0
seamEdges = []
for edge in obj.data.edges:
if edge.use_seam:
seamEdges.append(edge)
#Sort the edges in seamEdges
# seamEdges = sortEdges(seamEdges)
#Make a list of all verts in the seam
seamVerts = []
for edge in seamEdges:
for vert in edge.vertices:
if vert not in seamVerts:
seamVerts.append(vert)
if(len(seamEdges) < 2):
error = 2
return 0, 0, error
seamVerts = sortSeamVerts(seamVerts, seamEdges)
if debug: debPrintSeams(seamVerts, seamEdges)
if(len(seamEdges) == 0):
error = 2
return seamVerts, seamEdges, error
def getNextVertInEdge(edge, vert):
if vert == edge.vertices[0]:
return edge.vertices[1]
else:
return edge.vertices[0]
def makeNewHairSystem(headObject,systemName):
bpy.ops.object.mode_set(mode='OBJECT')
#Adding a particle modifier also works but requires pushing/pulling the active object and selection.
headObject.modifiers.new("HairNet", 'PARTICLE_SYSTEM')
#Set up context override
# override = {"object": headObject, "particle_system": systemName}
# bpy.ops.object.particle_system_add(override)
headObject.particle_systems[-1].name = systemName
headObject.particle_systems[-1].settings.type = 'HAIR'
headObject.particle_systems[-1].settings.render_step = 5
return headObject.particle_systems[systemName]
def makePolyLine(objName, curveName, cList):
#objName and curveName are strings cList is a list of vectors
curveData = bpy.data.curves.new(name=curveName, type='CURVE')
curveData.dimensions = '3D'
# objectData = bpy.data.objects.new(objName, curveData)
# objectData.location = (0,0,0) #object origin
# bpy.context.scene.objects.link(objectData)
polyline = curveData.splines.new('BEZIER')
polyline.bezier_points.add(len(cList)-1)
for num in range(len(cList)):
x, y, z = cList[num]
polyline.bezier_points[num].co = (x, y, z)
polyline.bezier_points[num].handle_left_type = polyline.bezier_points[num].handle_right_type = "AUTO"
# return objectData
return curveData
def preserveSelection():
#Preserve Active and selected objects
storedActive = bpy.context.object
storedSelected = []
for sel in bpy.context.selected_objects:
storedSelected.append(sel)
return storedActive, storedSelected
def changeSelection(thisObject):
storedActive, storedSelected = preserveSelection()
bpy.ops.object.select_all(action='DESELECT')
bpy.context.view_layer.objects.active=thisObject
thisObject.select_set(state=True)
return storedActive, storedSelected
def restoreSelection(storedActive, storedSelected):
#Restore active object and selection
bpy.context.view_layer.objects.active=storedActive
bpy.ops.object.select_all(action='DESELECT')
for sel in storedSelected:
sel.select = True
def removeParticleSystem(object, particleSystem):
override = {"object": object, "particle_system": particleSystem}
bpy.ops.object.particle_system_remove(override)
def sortEdges(edgesList):
sorted = []
debPrintEdgeKeys(edgesList)
return edgesList
def sortLoop(obj, vloop, v1, seamEdges, vert_edges):
#The hair is either forward or reversed. If it's reversed, reverse it again. Otherwise do nothing.
loop = []
loopRange = len(vloop)-1
if vloop[0] == v1.index:
loop = vloop.copy()
else:
loop = vloop[::-1]
return loop
def sortSeamVerts(verts, edges):
debug = False
sortedVerts = []
usedEdges = []
triedVerts = []
triedEdges = []
startingVerts = []
#Make a list of starting points so that each island will have a starting point. Make another "used edges" list
def findEndpoint(vert):
for thisVert in verts:
count = 0
if thisVert not in triedVerts:
triedVerts.append(thisVert)
#get all edges with thisVert in it
all_edges = [e for e in edges if thisVert in e.vertices]
if len(all_edges) == 1:
#The vert is in only one edge and is thus an endpoint
startingVerts.append(thisVert)
#walk to the other end of the seam and add verts to triedVerts
walking = True
thatVert = thisVert
beginEdge = thatEdge = all_edges[0]
while walking:
#get the other vert in the edge
if thatVert == thatEdge.key[0]:
thatVert = thatEdge.key[1]
else:
thatVert = thatEdge.key[0]
#Add current edge to triedEdges
triedEdges.append(thatEdge)
if thatVert not in triedVerts: triedVerts.append(thatVert)
#Put next edge in thatEdge
nextEdge = [e for e in edges if thatVert in e.vertices and e not in triedEdges]
if len(nextEdge) == 1:
#This means one edge was found that wasn't already used
thatEdge = nextEdge[0]
else:
#No unused edges were found
walking = False
# break
#at this point, we have found an endpoint
if debug:
print("seam endpoint", thisVert)
print("ending edge", beginEdge.key)
#get the edge the vert is in
#for thisEdge in edges:
return beginEdge, thisVert
for aVert in verts:
if aVert not in triedVerts:
thisEdge, thisVert = findEndpoint(aVert)
#Now, walk through the edges to put the verts in the right order
for thisVert in startingVerts:
thisEdge = [x for x in edges if (thisVert in x.key)][0]
sortedVerts.append(thisVert)
keepRunning = True
while keepRunning:
for newVert in thisEdge.key:
if debug: print("next vert is #", newVert)
if thisVert != newVert:
#we have found the other vert if this edge
#store it and find the next edge
thisVert = newVert
sortedVerts.append(thisVert)
usedEdges.append(thisEdge)
break
try:
thisEdge = [x for x in edges if ((thisVert in x.key) and (x not in usedEdges))][0]
except:
keepRunning = False
if debug: print("next vert is in edge", thisEdge.key)
return sortedVerts
def totalNumberSubdivisions(points, cuts):
return points + (points - 1)*cuts
mesh_kinds=[
("SHEET", "Sheets","Create hair from sheets"),
("FIBER", "Fibermesh","Create hair from loose edges"),
("CURVE", "Curves","Create hair from curve splines")
]
class HAIRNET_OT_operator (bpy.types.Operator):
bl_idname = "hairnet.operator"
bl_label = "HairNet"
bl_options = {"REGISTER", 'UNDO'}
bl_description = "Makes hair guides from mesh edges."
meshKind : EnumProperty(items=mesh_kinds, name="Generator kind", default="FIBER")
targetHead = False
headObj = 0
hairObjList = []
hairProxyList = []
@classmethod
def poll(self, context):
return(context.mode == 'OBJECT')
def execute(self, context):
debug = False
error = 0 #0 = All good
#1 = Hair guides have different lengths
#2 = No seams in hair object
#3 = Bevel on curve object
targetObject = self.headObj
for thisHairObj in self.hairObjList:
options = [
0, #0 the hair system's previous settings
thisHairObj, #1 The hair object
0, #2 The hair system. So we don't have to rely on the selected system
self.targetHead, #3 Target a head object?
targetObject, #4 targetObject
"name" #5 particle system name
]
#Get dependency graph
"""depsgraph = bpy.context.evaluated_depsgraph_get()
thisHairObj = thisHairObj.evaluated_get(depsgraph)
options[1] = thisHairObj"""
#A new hair object gets a new guides list
hairGuides = []
#if not self.targetHead:
if thisHairObj.hn_cfg.isEmitter:
targetObject = thisHairObj
#targetObject = targetObject.evaluated_get(depsgraph)
#targetObject = targetObject.evaluated_get(depsgraph)
config=thisHairObj.hn_cfg
sysName = ''.join(["HN", thisHairObj.name])
options[5] = sysName
if sysName in targetObject.particle_systems:
#if this proxy object has an existing hair system on the target object, preserve its current settings
if config.masterHairSystem == "":
'''_TS Preserve and out'''
options[0] = targetObject.particle_systems[sysName].settings
options[2] = targetObject.particle_systems[sysName]
else:
'''TS Delete settings, copy, and out'''
#Store a link to the system settings so we can delete the settings
delSet = targetObject.particle_systems[sysName].settings
#Get active_index of desired particle system
bpy.context.object.particle_systems.active_index = bpy.context.object.particle_systems.find(sysName)
#Delete Particle System
removeParticleSystem(targetObject, targetObject.particle_systems[sysName])
#Delete Particle System Settings
bpy.data.particles.remove(delSet)
#Copy Hair settings from master.
options[0] = bpy.data.particles[config.masterHairSystem].copy()
options[2] = makeNewHairSystem(targetObject,sysName)
else:
#Create a new hair system
if config.masterHairSystem != "":
'''T_S copy, create new and out'''
options[0] = bpy.data.particles[config.masterHairSystem].copy()
# options[2] = self.headObj.particle_systems[sysName]
'''_T_S create new and out'''
options[2] = makeNewHairSystem(targetObject,sysName)
if (self.meshKind=="SHEET"):
if debug: print("Hair sheet "+ thisHairObj.name)
#Create all hair guides
#for hairObj in self.hairObjList:
#Identify the seams and their vertices
#Start looking here for multiple mesh problems.
seamVerts, seamEdges, error = getSeams(thisHairObj)
if(error == 0):
vert_edges = edge_faces = False
#For every vert in a seam, get the edge loop spawned by it
for thisVert in seamVerts:
edgeLoops, vert_edges, edge_faces = getLoops(thisHairObj, thisHairObj.data.vertices[thisVert], vert_edges, edge_faces, seamEdges)
'''Is loopsToGuides() adding to the count of guides instead of overwriting?'''
hairGuides = self.loopsToGuides(thisHairObj, edgeLoops, hairGuides)
if debug: debPrintHairGuides(hairGuides)
#Take each edge loop and extract coordinate data from its verts
if (self.meshKind=="FIBER"):
hairObj = thisHairObj
if debug: print("Hair fiber")
hairGuides = self.fibersToGuides(hairObj)
if (self.meshKind=="CURVE"):
#Preserve Active and selected objects
tempActive = headObj = bpy.context.object
tempSelected = []
tempSelected.append(bpy.context.selected_objects[0])
tempSelected.append(bpy.context.selected_objects[1])
#hairObj = bpy.context.selected_objects[0]
hairObj = thisHairObj
bpy.ops.object.select_all(action='DESELECT')
if hairObj.data.bevel_depth > 0.0:
error = 3
bpy.context.view_layer.objects.active=hairObj
hairObj.select_set(state=True)
if debug: print("Curve Head: ", headObj.name)
bpy.ops.object.convert(target='MESH', keep_original=True)
fiberObj = bpy.context.active_object
if debug:
print("Hair Fibers: ", fiberObj.name)
print("Hair Curves: ", hairObj.name)
hairGuides = self.fibersToGuides(fiberObj)
bpy.ops.object.delete(use_global=False)
#Restore active object and selection
bpy.context.view_layer.objects.active=tempActive
bpy.ops.object.select_all(action='DESELECT')
for sel in tempSelected:
sel.select_set(state=True)
# return {'FINISHED'}
if (self.checkGuides(hairGuides)):
error = 1
#Process errors
if error != 0:
if error == 1:
self.report(type = {'ERROR'}, message = "Mesh guides have different lengths")
if error == 2:
self.report(type = {'ERROR'}, message = ("No seams were defined in " + targetObject.name))
removeParticleSystem(targetObject, options[2])
if error == 3:
self.report(type = {'ERROR'}, message = "Cannot create hair from curves with a bevel object")
return{'CANCELLED'}
#Subdivide hairs
hairGuides = self.subdivideGuideHairs(hairGuides, thisHairObj)
#Create the hair guides on the hair object
self.createHair(targetObject, hairGuides, options)
return {'FINISHED'}
def invoke (self, context, event):
self.headObj = bpy.context.object
#Get a list of hair objects
self.hairObjList = []
for obj in bpy.context.selected_objects:
if obj != self.headObj or obj.hn_cfg.isEmitter:
self.hairObjList.append(obj)
#if the last object selected is not flagged as a self-emitter, then assume we are creating hair on a head
#Otherwise, each proxy will grow its own hair
if not self.headObj.hn_cfg.isEmitter:
self.targetHead=True
if len(bpy.context.selected_objects) < 2:
self.report(type = {'ERROR'}, message = "Selection too small. Please select two objects")
return {'CANCELLED'}
else:
self.targetHead=False
return self.execute(context)
def checkGuides(self, hairGuides):
length = 0
for guide in hairGuides:
if length == 0:
length = len(guide)
else:
if length != len(guide):
return 1
return 0
def createHair(self, ob, guides, options):
debug = False
tempActive = bpy.context.active_object
bpy.context.view_layer.objects.active = ob
if debug: print("Active Object: ", bpy.context.active_object.name)
nGuides = len(guides)
if debug: print("nGguides", nGuides)
nSteps = len(guides[0])
if debug: print("nSteps", nSteps)
# Create hair particle system if needed
#bpy.ops.object.mode_set(mode='OBJECT')
#bpy.ops.object.particle_system_add()
psys = options[2]
# Particle settings
pset = psys.settings
if options[0] != 0:
#Use existing settings
psys.settings = options[0]
pset = options[0]
else:
#Create new settings
#pset.type = 'HAIR'
pset.emit_from = 'FACE'
ob.show_instancer_for_render = False
pset.use_strand_primitive = True
# Children
pset.child_type = 'SIMPLE'
pset.child_nbr = 6
pset.rendered_child_count = 50
pset.child_length = 1.0
pset.child_length_threshold = 0.0
pset.child_radius = 0.1
pset.child_roundness = 1.0
#Rename Hair Settings
#pset.name = ''.join([options[2].name, " Hair Settings"])
pset.hair_step = nSteps-1
#This set the number of guides for the particle system. It may have to be the same for every instance of the system.
pset.count = nGuides
#Render the emitter object?
if options[3]:
ob.show_instancer_for_render = True
else:
ob.show_instancer_for_render = False
# Disconnect hair and switch to particle edit mode
# Connect hair to mesh
# Segmentation violation during render if this line is absent.
# Connecting hair moves the mesh points by an amount equal to the object's location
bpy.ops.particle.particle_edit_toggle()
#bpy.context.scene.tool_settings.particle_edit.tool = 'COMB'
bpy.ops.particle.brush_edit(stroke=[{'name': '', 'location': (0, 0, 0), 'mouse': (0, 0), 'mouse_event':(0, 0), 'pressure': 0, 'size': 0, 'pen_flip': False, "x_tilt":0, "y_tilt":0, 'time': 0, 'is_start': False}])
bpy.ops.particle.particle_edit_toggle()
bpy.context.scene.tool_settings.particle_edit.use_emitter_deflect = False
bpy.context.scene.tool_settings.particle_edit.use_preserve_root = False
bpy.context.scene.tool_settings.particle_edit.use_preserve_length = False
bpy.ops.particle.disconnect_hair(all=True)
#Connecting and disconnecting hair causes them to jump when other particle systems are created.
bpy.ops.particle.connect_hair(all=True)
targetObj = options[4]
depsgraph = bpy.context.evaluated_depsgraph_get()
depObj = targetObj.evaluated_get(depsgraph)
psys = depObj.particle_systems[options[5]]
for m in range(0, nGuides):
#print("Working on guide #", m)
nSteps = len(guides[m])
guide = guides[m]
part = psys.particles[m]
part.location = guide[0]
#print("Guide #", m)
for n in range(0, nSteps):
point = guide[n]
#print("Hair point #", n, ": ", point)
h = part.hair_keys[n]
#h.co_local = point
h.co = point
#print("h.co = ", h.co)
# Toggle particle edit mode
bpy.ops.particle.particle_edit_toggle()
bpy.ops.particle.particle_edit_toggle()
bpy.context.view_layer.objects.active = tempActive
return
def createHairGuides(self, obj, edgeLoops):
hairGuides = []
#For each loop
for loop in edgeLoops:
thisGuide = []
#For each vert in the loop
for vert in loop[0]:
thisGuide.append(obj.data.vertices[vert].co)
hairGuides.append(thisGuide)
return hairGuides
def fibersToGuides(self, hairObj):
import time # evaluation
time_start = time.time()
me = hairObj.data
uf = UnionFindList(len(me.vertices))
for ed in me.edges:
# the edge wouldn't exist if one of points is hidden
if me.vertices[ed.key[0]].hide == True or me.vertices[ed.key[1]].hide == True: continue
uf.union(ed.key[0], ed.key[1])
ret = [ [ hairObj.data.vertices[vertIdx].co.to_tuple() for vertIdx in uf.getChain(vert) ] for vert in uf.findRoots() ]
time_end = time.time()
print("Function getHairsFromFibers cost:", time_end-time_start)
#default cost: 139.5683515071869 # 33,192 points
#now cost: 0.5224146842956543 # 33,192 points
#now cost: 1.9567747116088867 # 137,238 points
return ret
def loopsToGuides(self, obj, edgeLoops, hairGuides):
guides = hairGuides
#guides = []
for loop in edgeLoops:
hair = []
#hair is a list of coordinate sets. guides is a list of lists
for vert in loop[0]:
#co originally came through as a tuple. Is a Vector better?
hair.append(obj.data.vertices[vert].co)
# hair.append(obj.data.vertices[vert].co.to_tuple())
guides.append(hair)
return guides
def subdivideGuideHairs(self, guides, hairObj):
debug = True
#number of points in original guide hair
hairLength = len(guides[0])
#original number of hairs
numberHairs = len(guides)
#number of hairs added between existing hairs
hairSprouts = hairObj.hn_cfg.sproutHairs
#subdivide hairs
if hairObj.hn_cfg.sproutHairs > 0:
#initialize an empty array so we don't have to think about inserting entries into lists. Check into this for later?
newHairs = [[0 for i in range(hairLength)] for j in range(totalNumberSubdivisions(numberHairs, hairSprouts))]
if debug: print ("Subdivide Hairs")
newNumber = 1
#initial condition
start = guides[0][0]
newHairs[0][0] = start
# debPrintHairGuides(newHairs)
#for every hair pair, start at the root and send groups of four guide points to the interpolator
#index identifies which row is current
#kndex identifies the current hair in the list of new points
#jndex identifies the current hair in the old list of hairs
for index in range(0, hairLength):
if debug: print("Hair Row ", index)
#add the first hair's points
newHairs[0][index] = guides[0][index]
#Make a curve from the points in this row
thisRow = []
for aHair in guides:
thisRow.append(aHair[index])
curveObject = makePolyLine("rowCurveObj", "rowCurve", thisRow)
for jndex in range(0, numberHairs-1):
# knot1 = curveObject.data.splines[0].bezier_points[jndex]
# knot2 = curveObject.data.splines[0].bezier_points[jndex + 1]
knot1 = curveObject.splines[0].bezier_points[jndex]
knot2 = curveObject.splines[0].bezier_points[jndex + 1]
handle1 = knot1.handle_right
handle2 = knot2.handle_left
newPoints = mathutils.geometry.interpolate_bezier(knot1.co, handle1, handle2, knot2.co, hairSprouts+2)
#add new points to the matrix
#interpolate_bezier includes the endpoints so, for now, skip over them. re-write later to be a cleaner algorithm
for kndex in range(0, len(newPoints)-2):
newHairs[1+kndex+jndex*(1+hairSprouts)][index] = newPoints[kndex+1]
# if debug: print("newHairs[", 1+kndex+jndex*(1+hairSprouts), "][", index, "] = ", newPoints[kndex], "SubD")
# newHairs[jndex*(1+hairSprouts)][index] = newPoints[kndex]
# print("knot1 = ", knot1)
# print("knot2 = ", knot2)
# print("newHairs[", 1+kndex+jndex*(1+hairSprouts), "][", index, "] = ", newPoints[kndex])
newNumber = newNumber + 1
#add the end point
newHairs[(jndex+1)*(hairSprouts+1)][index] = guides[jndex+1][index]
# if debug: print("newHairs[", (jndex+1)*(hairSprouts+1), "][", index, "] = ", guides[jndex][index], "Copy")
newNumber = newNumber + 1
#clean up the curve we created
bpy.data.curves.remove(curveObject)
if debug:
print("NewHairs")
debPrintHairGuides(newHairs)
guides = newHairs
return guides
class HAIRNET_PT_panel(bpy.types.Panel):
bl_idname = "HAIRNET_PT_HairNet"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "particle"
bl_label = "HairNet " + versionString
def draw(self, context):
self.headObj = context.object
#Get a list of hair objects
self.hairObjList = context.selected_objects
if self.headObj in self.hairObjList:
self.hairObjList.remove(self.headObj)
layout = self.layout
row = layout.row()
row.label(text = "Objects Start here")
#Is this a hair object?
row = layout.row()
try:
row.prop(self.headObj.hn_cfg, 'isEmitter', text = "Emit Hair on Self")
except:
pass
#Draw this if this is a head object
if not self.headObj.hn_cfg.isEmitter:
box = layout.box()
row = box.row()
row.label(text = "Hair Object:")
row.label(text = "Master Hair System:")
for thisHairObject in self.hairObjList:
row = box.row()
row.prop_search(thisHairObject.hn_cfg, 'masterHairSystem', bpy.data, "particles", text = thisHairObject.name)
row = box.row()
row.label(text = "Guide Subdivisions:")
row.prop(thisHairObject.hn_cfg, 'sproutHairs', text = "Subdivide U")
# row.prop(thisHairObject, 'hnSubdivideHairSections', text = "Subdivide V")
#Draw this if it's a self-emitter object
else:
box = layout.box()
try:
row = box.row()
row.label(text = "Master Hair System")
row = box.row()
row.prop_search(self.headObj.hn_cfg, 'masterHairSystem', bpy.data, "particles", text = self.headObj.name)
except:
pass
row = box.row()
row.label(text = "Guide Subdivisions:")
row.prop(self.headObj.hn_cfg, 'sproutHairs', text = "Subdivide U")
class HAIRNET_PT_view_panel(bpy.types.Panel):
bl_label = "HairNet"
bl_idname = "HAIRNET_PT_view_panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Hair"
bl_context = "objectmode"
bl_options = {"DEFAULT_CLOSED"}
def draw(self, context):
object = context.active_object
if object is not None:
self.drawButtons(self.layout)
self.drawDetails(self.layout, context)
def drawButtons(self, layout):
col = layout.box().column(align = True)
row = col.row(align = True)
row.label(text="Make Hair")
row = col.row()
row.label(text ="Add Hair From:")
row = col.row(align = True)
for kind in mesh_kinds:
row = col.row(align = True)
row.operator("hairnet.operator", text=kind[1]).meshKind=kind[0]
def drawDetails(self, layout, context):
self.headObj = context.object
#Get a list of hair objects
self.hairObjList = context.selected_objects
if self.headObj in self.hairObjList:
self.hairObjList.remove(self.headObj)
layout = self.layout
row = layout.row()
#row.label(text = "Objects Start here")
'''Is this a hair object?'''
row = layout.row()
try:
row.prop(self.headObj.hn_cfg, 'isEmitter', text = "Emit Hair on Self")
except:
pass
#Draw this if this is a head object
if not self.headObj.hn_cfg.isEmitter:
box = layout.box()
row = box.row()
row.label(text = "Hair Object:")
row.label(text = "Use Settings:")
for thisHairObject in self.hairObjList:
config=thisHairObject.hn_cfg
row = box.row()
row.prop_search(config, 'masterHairSystem', bpy.data, "particles", text = thisHairObject.name)
row = box.row()
row.label(text = "Add Guides:")
row.prop(config, 'sproutHairs', text = "SubD")
# row.prop(thisHairObject, 'hnSubdivideHairSections', text = "Subdivide V")
#Draw this if it's a self-emitter object
else:
box = layout.box()
try:
row = box.row()
row.label(text = "Use Settings")
row = box.row()
row.prop_search(self.headObj.hn_cfg, 'masterHairSystem', bpy.data, "particles", text = self.headObj.name)
except:
pass
row = box.row()
row.label(text = "Guide Subdivisions:")
row.prop(self.headObj.hn_cfg, 'sproutHairs', text = "SubD")
classes = (
HAIRNET_OT_operator,
HAIRNET_PT_panel,
HAIRNET_PT_view_panel,
HairNetConfig,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Object.hn_cfg=PointerProperty(type=HairNetConfig)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if __name__ == '__main__':
register()
================================================
FILE: hairNet/import_properties.py
================================================
import bpy
from bpy.props import (StringProperty,
BoolProperty,
IntProperty,
FloatProperty,
FloatVectorProperty,
EnumProperty,
PointerProperty,
CollectionProperty,
)
from bpy.types import PropertyGroup
gitextract_dv34wfql/
├── .gitignore
├── README.md
└── hairNet/
├── Version Notes.txt
├── __init__.py
├── hairNet.py
└── import_properties.py
SYMBOL INDEX (50 symbols across 2 files)
FILE: hairNet/__init__.py
function register (line 47) | def register():
function unregister (line 52) | def unregister():
FILE: hairNet/hairNet.py
class UnionFindList (line 44) | class UnionFindList: # a combination of unionfind and singlelinkedlist
method __init__ (line 45) | def __init__(self, n):
method findRoots (line 49) | def findRoots(self):
method findRoot (line 51) | def findRoot(self, x):
method getNext (line 56) | def getNext(self, x): # return -1 if no next
method getChainLength (line 58) | def getChainLength(self, x): # x must be the root
method getChain (line 60) | def getChain(self, x):
method reverseChain (line 67) | def reverseChain(self, x_root, x): # x_root is the head of list, x is ...
method union (line 78) | def union(self, x, y):
class HairNetConfig (line 114) | class HairNetConfig(PropertyGroup):
function debPrintVertEdges (line 140) | def debPrintVertEdges(vert_edges):
function debPrintEdgeFaces (line 145) | def debPrintEdgeFaces(edge_faces):
function debPrintEdgeKeys (line 150) | def debPrintEdgeKeys(edges):
function debPrintHairGuides (line 154) | def debPrintHairGuides(hairGuides):
function debPrintSeams (line 166) | def debPrintSeams(seamVerts, seamEdges):
function debPrintLoc (line 174) | def debPrintLoc(func=""):
function getEdgeFromKey (line 179) | def getEdgeFromKey(mesh,key):
function getLoops (line 190) | def getLoops(obj, v1, vert_edges, edge_faces, seamEdges):
function getSeams (line 290) | def getSeams(obj):
function getNextVertInEdge (line 321) | def getNextVertInEdge(edge, vert):
function makeNewHairSystem (line 327) | def makeNewHairSystem(headObject,systemName):
function makePolyLine (line 340) | def makePolyLine(objName, curveName, cList):
function preserveSelection (line 359) | def preserveSelection():
function changeSelection (line 371) | def changeSelection(thisObject):
function restoreSelection (line 379) | def restoreSelection(storedActive, storedSelected):
function removeParticleSystem (line 386) | def removeParticleSystem(object, particleSystem):
function sortEdges (line 391) | def sortEdges(edgesList):
function sortLoop (line 397) | def sortLoop(obj, vloop, v1, seamEdges, vert_edges):
function sortSeamVerts (line 409) | def sortSeamVerts(verts, edges):
function totalNumberSubdivisions (line 495) | def totalNumberSubdivisions(points, cuts):
class HAIRNET_OT_operator (line 504) | class HAIRNET_OT_operator (bpy.types.Operator):
method poll (line 518) | def poll(self, context):
method execute (line 521) | def execute(self, context):
method invoke (line 672) | def invoke (self, context, event):
method checkGuides (line 699) | def checkGuides(self, hairGuides):
method createHair (line 709) | def createHair(self, ob, guides, options):
method createHairGuides (line 810) | def createHairGuides(self, obj, edgeLoops):
method fibersToGuides (line 823) | def fibersToGuides(self, hairObj):
method loopsToGuides (line 844) | def loopsToGuides(self, obj, edgeLoops, hairGuides):
method subdivideGuideHairs (line 858) | def subdivideGuideHairs(self, guides, hairObj):
class HAIRNET_PT_panel (line 929) | class HAIRNET_PT_panel(bpy.types.Panel):
method draw (line 938) | def draw(self, context):
class HAIRNET_PT_view_panel (line 990) | class HAIRNET_PT_view_panel(bpy.types.Panel):
method draw (line 1000) | def draw(self, context):
method drawButtons (line 1006) | def drawButtons(self, layout):
method drawDetails (line 1021) | def drawDetails(self, layout, context):
function register (line 1083) | def register():
function unregister (line 1089) | def unregister():
Condensed preview — 6 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (47K chars).
[
{
"path": ".gitignore",
"chars": 313,
"preview": "\n.project\n.pydevproject\nBlend Files/HairNet 2.8 Debug.blend\n*.blend\n*.blend1\nsrc/__pycache__/HairNet.cpython-37.pyc\nsrc/"
},
{
"path": "README.md",
"chars": 648,
"preview": "# Attention:\nHairNet now works in Blender 2.80. \n\nThe buttons for the interface have moved to the Numeric Panel in the 3"
},
{
"path": "hairNet/Version Notes.txt",
"chars": 1184,
"preview": "0.6.1\nupdated object selection call\n\n0.6.0\nUpdated many calls for Blender 2.80\nMoved interface to 3D_View to fix polling"
},
{
"path": "hairNet/__init__.py",
"chars": 1743,
"preview": "# ##### BEGIN GPL LICENSE BLOCK #####\n#\n# This program is free software; you can redistribute it and/or\n# modify it un"
},
{
"path": "hairNet/hairNet.py",
"chars": 40731,
"preview": "#---------------------------------------------------\n# File HairNet.py\n# Written by Rhett Jackson April 1, 2013\n# Some r"
},
{
"path": "hairNet/import_properties.py",
"chars": 386,
"preview": "import bpy\nfrom bpy.props import (StringProperty,\n BoolProperty,\n IntPropert"
}
]
About this extraction
This page contains the full source code of the Jandals/HairNet GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 6 files (44.0 KB), approximately 10.5k tokens, and a symbol index with 50 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.