#!BPY """ Name: 'MegaBool r4' Blender: 234 Group: 'Object' Tooltip: 'Perform boolean operations on meshes and get nice results' """ ################################################################### # # # Experimental New Mesh Boolean Operations # # # # (C) 2004 Theodore K Schundler (tschundler)(@)(scu)(edu) # # FOR TESTING ONLY, not for redistribution # # Lastest version available for from: # # http://astro.scu.edu/~ted/oss/blender/boolean # # # # USAGE: # # -By Menu- # # * Put this file in ~/.blender/scripts/ # # * Select two objects, then choose "MegaBool" from the # # scripts menu. # # -By Running the script- # # * Load this file into blender's internal text editor # # * Select two objects, then press Alt+P in the test editor # # # # OPERATIONS: # # On execution of the script, there are four operations # # * Intersect - Intersection of the two meshes (logical AND) # # * Union - Union of the two meshes (logical OR) # # * Difference - First selected mesh - Last selected mesh # # * Cookie Cutter - First mesh's faces are subdivided where # # they intersect with the second mesh # # After the script executes, all "inside" vertices are selected # # # # IMPORTANT NOTES: # # This script uses Blender's internal scan fill to fill faces. # # It does not have an official python interface *yet*, so I # # quickly hacked one together. A patch to the bf-blender cvs code # # base is available at the URL above. Patch your blender source # # and recompile. Then this script will run. Without the patched # # blender this script will fail on "fillVertLoops". # # Like any boolean algorithm, normals on the source meshes must # # be all facing outside. To fix normals, select a mesh, enter # # edit mode, select all verticies, and press Ctrl+N. Also, all # # intersections must create closed loops. (Basically the meshes # # should be closed.) If you find a set of meshes that do not work # # send me the .blend file and an explaination of the problem. # # # # TO DO: # # * After the operation, should probably translate results, so # # the mesh isn't centered around <0,0,0>, use one of the base # # meshes instead # # * Set materials, UVs, etc., on new mesh # # * Use correct normals on new mesh # ################################################################### # # # Date: Updates: # # 16-Aug-2004 Initial Revision - working(?) & ready to go # # 17-Aug-2004 Fixed some bugs based on incorrect assumptions. # # that means even more computation. Also made more # # use of the traversalCookies to speed up a some # # sections # # 24-Aug-2004 Smart filling & other stuff... # # 26-Aug-2004 Much cleanup, nothing new / fancy # # # ################################################################### import Blender from Blender import NMesh from Blender import Object from Blender import Draw from Blender import Mathutils from Blender.Mathutils import * traversalCookie=0 messages="" class CCVert: #intialize our vertex object def __init__(self,nmVert,ccObject): global traversalCookie self.parentObject=ccObject if nmVert: self.counterPart=nmVert v=list(nmVert.co) vec=Mathutils.Vector(v) vec.resize4D() vec.w=1 self.tco=Mathutils.VecMultMat(vec,ccObject.matrix) #self.tco=Mathutils.MatMultVex(vec,ccObject.matrix) self.tco.resize3D() #print vec,"->",self.tco else: self.tco=[0,0,0] self.inside=0 self.outside=0 self.neither=0 self.traversalCookie=traversalCookie self.cutBy=[] self.edgeWith=[] self.newCounterpart=0 self.used=0 #print "\tAdded Vertex: ", self.tco class CCEdge: def __init__(self,v1,v2,ccObject): global traversalCookie self.parentObject=ccObject ccObject.edges.append(self) self.index=len(ccObject.edges)-1 self.v=[v1,v2] #print "\t\tNew Edge[%d]: (v%d -> v%d)"%(self.index,v1.counterPart.index,v2.counterPart.index) #Make notes on points for building loops later v1.edgeWith.append([v2,self]) v2.edgeWith.append([v1,self]) self.faces=[] #define a bounding box for fast intersection testing self.bb=[[v1.tco[0],v1.tco[1],v1.tco[2]],[v2.tco[0],v2.tco[1],v2.tco[2]]] for i in range(3): if self.bb[0][i]>self.bb[1][i]: t=self.bb[0][i] self.bb[0][i]=self.bb[1][i] self.bb[1][i]=t self.inside=0 self.outside=0 self.neither=0 self.traversalCookie=traversalCookie def findSide(self): global traversalCookie ne=self v=self.v traversalCookie=traversalCookie+1 FindVertexSide(v[0],traversalCookie) FindVertexSide(v[1],traversalCookie) if (v[0].outside!=v[0].inside): ne.outside=v[0].outside ne.inside=v[0].inside ne.neither=v[0].neither else: ne.outside=v[1].outside ne.inside=v[1].inside ne.neither=v[1].neither def realize(self,newEdges,sweep,face): global traversalCookie v=self.v if len(v)==2: if sweep==1: return else: if sweep==2: return vA=face.verts.index(self.v[0]) vB=face.verts.index(self.v[1]) dir=0 if vB!=(vA+1)%len(face.edges): #print "SD: ",vA,vB dir=1 if len(v)==2: if dir: ne=CCEdge(v[1],v[0],self.parentObject) else: ne=CCEdge(v[0],v[1],self.parentObject) #v[0].edgeWith.append([v[1],self]) #v[1].edgeWith.append([v[0],self]) ne.findSide() newEdges.append(ne) return 0 else: vec=v[1].tco-v[0].tco nv=[] ep=[] #Sort points a long the edge #if AddVert (below) inserts new verts in the right place, we can avoid this for vtx in v: #vtx.edgePos=DotVecs(vtx.tco,vec) vtx.edgePos=(vtx.tco-v[0].tco).length ep.append(vtx.edgePos) nv.append(vtx) ep.sort() for vtx in v: nv[ep.index(vtx.edgePos)]=vtx #print "sort edges",nv[0].outside,nv[0].inside #Make new edges outside=nv[0].outside nv[0].inside=1-outside for n in range(1,len(nv)): if dir: ne=CCEdge(nv[n],nv[n-1],self.parentObject) else: ne=CCEdge(nv[n-1],nv[n],self.parentObject) #print self,"--",outside,"--",ne ne.outside=outside ne.inside=1-ne.outside nv[n-1].outside=nv[n-1].outside|outside nv[n-1].inside=nv[n-1].inside|ne.inside nv[n].outside=outside nv[n].inside=1-outside newEdges.append(ne) outside=1-outside return 1 #add a vertex, and figure out if any others on the edge need to ne redefined as inside or outside #For the real implementation, this should probably be a linked list, and it should add itself in the right place and adjust prev and next accordingly def AddVert(self,v,normal): insideVec=[] insideL=(self.v[1].tco-self.v[0].tco).length outsideVec=[] outsideL=(self.v[1].tco-self.v[0].tco).length for vx in self.v: vec=vx.tco-v.tco if DotVecs(vec,normal)>0: if vec.lengthself.bb[1][i]: self.bb[1][i]=v[i] #Add Edges self.addEdge(nmFace.v[0],nmFace.v[1]) self.addEdge(nmFace.v[1],nmFace.v[2]) if len(nmFace.v)==3: self.addEdge(nmFace.v[2],nmFace.v[0]) else: self.addEdge(nmFace.v[2],nmFace.v[3]) self.addEdge(nmFace.v[3],nmFace.v[0]) #this can probably be more directly integrated into the filling function #to yeild more optimized results (for example, all the extraverts are shared, #so they won't be the starting point for inside or outside loops, so when #building those loops, we don't need to bother with them def GetVertexSet(self): vs=[] for e in self.edges: for v in e.v: try: vs.index(v) except: vs.append(v) for v in self.extraVerts: try: vs.index(v) except: #try: #v.cutBy.index(self) #except: #print self,v vs.append(v) return vs class CCObject: def __init__(self,blenderObject): self.counterPart=blenderObject print "Adding Object %s"%blenderObject.getName() #setup variables self.verts=[] self.faces=[] self.edges=[] #store some matrices for future use omtx=blenderObject.getMatrix() #print "OMTX:",omtx self.matrix=Mathutils.Matrix(list(omtx[0]),list(omtx[1]),list(omtx[2]),list(omtx[3])) #self.matrix.transpose() #print "SELF:",self print "\tAdding verts" #build up vertex list m=NMesh.GetRawFromObject(blenderObject.getName()) self.nmesh=m for v in m.verts: self.verts.append([]) #print "vtx: ",len(self.verts) for v in m.verts: self.verts[v.index]=CCVert(v,self) print "\tAdding Faces & Edges" #build up faces & edges for f in m.faces: if len(f.v)>2: self.faces.append(CCFace(f,self)) def TestPointSideOfLine(point,v0,v1,normal): #Note: Maybe I should store the edge's normal on the edge? #This function gets called from different loops, so it may be worth it cp=CrossVecs(normal,v1-v0) s=DotVecs(point-v0,cp) if s < 0: return 0 else: return 1 #test if a point is within the edges of a convex face def TestPointInFace(p,f): vcount=len(f.verts) lastv=f.verts[vcount-1] for v in f.verts: if not TestPointSideOfLine(p,lastv.tco,v.tco,f.normal): return 0 lastv=v return 1 def TestPointInEdge(v,vl): ev=vl[1].tco-vl[0].tco vA=v-vl[0].tco d=DotVecs(vA,ev) if d<0 or vA.length>ev.length: return 0 return 1 def TestPointInFaceInEdge(v,f,ed): if not TestPointInEdge(v,ed.v): return 0 if not TestPointInFace(v,f): return 0 return 1 #Make a list of points out of a list of edges def EdgeLoopVxLoop(el): global traversalCookie traversalCookie=traversalCookie+1 vxl=[] for e in el: for v in e.v: if v.traversalCookie10: print "Apex Outside" v.outside=1 return sv elif sv==0: if len(v.edgeWith)>10: print "Apex Inside" v.inside=1 return sv #else: # print "NEITHER@!!!!!!!!!!!!!!!!!!!!!!!" # v.neither=1 #if loop failed, this must be floating out in space somewhere #v.neither=1 return -1 #this is needed in cases where none of the original faces are cut def FindEdgeSide(e,tc): if e.inside==0 and e.outside==0 and e.neither==0: #global messages #messages+="Had to find the side of an edge....\n" v1=FindVertexSide(e.v[0],tc) v2=FindVertexSide(e.v[1],tc) if v1<0 or v2<0: e.outside=1 return 1 #print "findlike ",v1,"--",v2 if v1==v2: if v1==1: e.outside=1 elif v1==0: e.inside=1 else: e.neither=1 else: e.neither=1 if e.neither==1: return 0 elif e.inside==1 and e.outside==1: return 3 elif e.inside==1: return 2 else: return 1 #After the call to blender's internal fill function, we have to figure out #What it returns and match it up with its counterpart in out vertex set def FindVertex(vtx,vxlist): for v in vxlist: m=0 for n in range(3): if v.tco[n]==vtx[n]: m=m+1 if m==3: return v print "Failed to find vertex ",vtx return 0 #Starting with an edge and a point in that edge, this builds a loop #of edges to fill later as a face def MakeEdgeLoop(loop,edge,point,inside,outside,tc): v=point for ew in v.edgeWith: if ew[1].inside>=inside and ew[1].outside>=outside: if ew[1].traversalCookie",DotVecs(cp,lcp) dv=DotVecs(cp,lcp) if dv<-0.00001: break lcp=cp #if it is convex, then we should add it if dv>-0.00001: AddFace(f,nm,vxl,objectflip) return else: print "4 Verts",dv for n in range(count): print vxl[n].tco segments=[[],[]] tossed=[] #Esegments=[[],[]] cseg=0 print "E: ",len(el)," V: ",len(vxl) lastV=vxl[count-1] lastE=el[count-1] for i in range(0,count): v=vxl[i] e=el[i] #print i,": ",lastV.tco,"->",v.tco #print i,": ", e.v[0].tco,"->",e.v[1].tco #AddEdge(nm,[e.v[0],e.v[1]]) #AddFace(nm,[lastV,v],0) if ((e.inside != lastE.inside) or (e.outside != lastE.outside)) and skipsplit==0: cseg=(cseg+1)%2 tossed.append(i) else: #print "Plain",i,v.tco segments[cseg].append(i) lastV=v lastE=e print "Segments: ",segments,"\nTossed: ",tossed testnormals=1 if skipsplit==0: if (len(segments[0])==0 or len(segments[1])==0) and len(tossed)>0: if len(segments[0])==0: segments[0]=tossed else: segments[1]=tossed if len(segments[0])==0 or len(segments[1])==0: segments[0]=range(count) segments[1]=range(count) #testnormals=1 Cut=[0,0] Distance=-1 print "Test Edges: ",len(gel) for pi0 in segments[0]: #for this 2nd for loop, in C version, be sure to avoid hitting #all options, just necissary ones when segments[0]==segment[1] for pi1 in segments[1]: nv=(vxl[pi0].tco-vxl[pi1].tco) if (pi0!=pi1 and (Distance<0 or (nv.length0: if not (TestPointSideOfLine(vxl[pi1].tco,v0,v1,f.normal) and TestPointSideOfLine(vxl[pi1].tco,v1,v2,f.normal)): ok=0 print "Falied Normals Test, Convex Case" else: if not (TestPointSideOfLine(vxl[pi1].tco,v0,v1,f.normal) or TestPointSideOfLine(vxl[pi1].tco,v1,v2,f.normal)): ok=0 print "Failed Normals Test, Concave Case" p1=vxl[pi0].tco p2=vxl[pi1].tco d2121=DotVecs(p2-p1,p2-p1) #test for intersections for e in el: if ok==0: break p3=e.v[0].tco p4=e.v[1].tco #Test if the new edge is an edge that already exists in the edge list if (vxl[pi0]==e.v[0] and vxl[pi1]==e.v[1]) or (vxl[pi0]==e.v[1] and vxl[pi1]==e.v[0]): if ok!=0: print "Edge already exists (%f,%f,%f)->(%f,%f,%f)"%(p1[0],p1[1],p1[2],p2[0],p2[1],p2[2]) ok=0 break else: #Test to see of this edge overlaps an existing edge if vxl[pi0]==e.v[0] or vxl[pi1]==e.v[1] or vxl[pi0]==e.v[1] or vxl[pi1]==e.v[0]: if vxl[pi0]==e.v[0]: sp=e.v[0] if vxl[pi0]==e.v[1]: sp=e.v[1] if vxl[pi1]==e.v[0]: sp=e.v[0] if vxl[pi1]==e.v[1]: sp=e.v[1] if (vxl[pi0]==sp): ap=vxl[pi1] else: ap=vxl[pi0] if (e.v[0]==sp): bp=e.v[1] else: bp=e.v[0] vA=ap.tco-sp.tco vA.normalize() vB=bp.tco-sp.tco vB.normalize() d=DotVecs(vA,vB) if d>0.9999999: if ok!=0: print "DP failed at %f -- (%f,%f,%f)->(%f,%f,%f) vs (%f,%f,%f)->(%f,%f,%f)\n"%(d,p1[0],p1[1],p1[2],p2[0],p2[1],p2[2],p3[0],p3[1],p3[2],p4[0],p4[1],p4[2]) ok=0 break #Test if the edges intersect (i.e. in concave shaps) if vxl[pi0]!=e.v[0] and vxl[pi1]!=e.v[1] and vxl[pi0]!=e.v[1] and vxl[pi1]!=e.v[0]: #based on http://astronomy.swin.edu.au/~pbourke/geometry/lineline3d/ d4321=DotVecs(p4-p3,p2-p1) d4343=DotVecs(p4-p3,p4-p3) d1343=DotVecs(p1-p3,p4-p3) d1321=DotVecs(p1-p3,p2-p1) mnad = (d2121*d4343-d4321*d4321) if mnad==0: print "\nParallel! ",p1,p2,p3,p4 messages+="Parllel!!!\n" mna=1.1 else: mna = (d1343*d4321-d1321*d4343)/mnad if (mna>-0.00001 and mna <1.000001): mnb = (d1343+mna*d4321)/d4343 if (mnb > -0.00001 and mnb <1.000001): if ok: print "Intersection",p1,p2,p3,p4,mna,mnb ok=0 break if ok: Distance=nv.length Cut=[pi0,pi1] if Distance < 0: print "Couldn't find good cut point",(len(segments[0]),len(segments[1]),) #if count==4: #AddFace(f,nm,vxl,objectflip,side) #return vl=[] for v in vxl: vl.append([v.tco[0],v.tco[1],v.tco[2]]) #ScanFillLoop([vl],f,nm,objectflip,vxl,side) BishieFill(el,vxl,f,nm,objectflip,side,gel,1) else: print "Cut: ",Cut,Distance if Cut[1]3 or 1: el=[] #cutfaces=0 if (simple==0): #add edges of original face for e in f.edges: e.realize(el,1,f) for e in f.edges: e.realize(el,2,f) f.edges=el if (len(vs)==4 or len(vs)==3) and simple==1: if inside!=outside: for v in vs: if (v.outside\n"%(v.tco[0],v.tco[1],v.tco[2])) for f in object.faces: if f.altered==0: #vs=f.GetVertexSet() vs=f.verts for v in vs: traversalCookie=traversalCookie+1 s=FindVertexSide(v,traversalCookie) if (s==-1): v.neither=1 print "NEITHER!" MakeFace(self.nm,f,vs,inside,outside,1,flip) def TestBounds(b1,b2): for i in range(3): if (b1[0][i]>b2[1][i])|(b2[1][i]0: txt=Blender.Text.New("Boolean Messages") txt.clear() txt.write(messages)