import poser, math, os, gzip, time version = float(poser.AppVersion().split(".")[0]) if version < 8.0: from Tkinter import * else: import wx, wx.py, wx.aui man = poser.WxAuiManager() root = man.GetManagedWindow() try: import Numeric oldnumeric = Numeric except: import numpy as Numeric Numeric.Int = Numeric.int Numeric.Float = Numeric.float Numeric.matrixmultiply = Numeric.dot import numpy.oldnumeric as oldnumeric FLT_EPSILON = 1.19209290e-07 scene = poser.Scene() """ MIT License for original Java CSG code (and all derivative works): Copyright (c) 2011 Evan Wallace (http://madebyevan.com/) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ """ - Set up the script for broader use. - Build the GUI - Activate 'multiple' fxn: overlapped merged cut objects will lead to one of the overlap objects giving bad results. Apparently the code can't figure out the front/back properly, in such a case. - Complicated. Requires multiple myMesh and csg instances to process. Post-process requires merged myMesh data and merged post-csg data. - Set up options for weld handling. - Find hard edges in source meshes. - Get edges for cut mesh. - Correlate vertices. - Locate hard edges on cut mesh by correlated vertices of edges. - Options for full weld, split by sources, retain source hard edges - Add user-configurable version of 'test' csg fxn. Select node fxns to stack, then build a csg fxn from those. Provide recipes listing fxns in main features. This may be best as a variant script. The GUI will require two additional listboxes, for the recipes. - CSG primitives need mats, tverts, and tracking to be compatible with current process. """ def run(toCut="wall",cutBy=["door","window","window2"],bool_type="subtract",weld=2,multiple=0,colors=0,use_clean_weld=0,testing=0): theGUI.status_update("Getting mesh data from actors...") toCut_unsplit = toCut toCut, toCut_fig = toCut_unsplit.replace(">","").split(" <") if toCut_fig == "PropActor": cutAct = scene.Actor(toCut) else: cutAct = scene.Figure(toCut_fig).Actor(toCut) cutBy_unsplit = [i for i in cutBy] cutBy = [i.replace(">","").split(" <") for i in cutBy_unsplit] #cutAct = scene.Actor(toCut) toCut_mesh = myMesh(cutAct,vertexPos(cutAct.Geometry())) if multiple: # Not enabled pass else: #merge_acts = [scene.Actor(i) for i in cutBy] # Merge multiple cutBy actors up front, to reduce processing merge_acts = [] for name,fig in cutBy: if fig == "PropActor": merge_acts.append(scene.Actor(name)) else: merge_acts.append(scene.Figure(fig).Actor(name)) newprop = merge_act_geoms(merge_acts,sep_mats=0,name="toCut_merged") cutBy_mesh = myMesh(newprop,vertexPos(newprop.Geometry())) scene.SelectActor(newprop) scene.DeleteCurrentProp() if testing: prop_from_myMesh(toCut_mesh,"toCut") prop_from_myMesh(cutBy_mesh,"cutBy") return # Verify that there is intersection. toCut_bbox = b_box_ock(toCut_mesh.verts) cutBy_bbox = b_box_ock(cutBy_mesh.verts) intersect = intersectAABBs(toCut_bbox,cutBy_bbox) if not intersect: theGUI.status_update("Selected objects do not intersect.") theGUI.status_update("Quitting.") return theGUI.status_update("Creating CSG instances...") new_csg = CSG() cutBy_csg = new_csg.custom(cutBy_mesh) toCut_csg = new_csg.custom(toCut_mesh) """ if multiple: multiple = [[2,door_csg],[2,w_csg],[2,w2_csg]] multiple = [["subtract",door_csg],["subtract",w_csg],["subtract",w2_csg]] polygons = toCut_csg.multiple_runs(multiple).toPolygons() """ theGUI.status_update("Performing Boolean functions with CSG...") if bool_type == "subtract": # subtract, union, intersect, sub_return_cutBy, sub_return_toCut polygons = toCut_csg.subtract(cutBy_csg).toPolygons() elif bool_type == "union": invert = 0 recombine = 0 polygons = toCut_csg.union(cutBy_csg).toPolygons() elif bool_type == "intersect": recombine = 0 polygons = toCut_csg.intersect(cutBy_csg).toPolygons() elif bool_type == "sub_return_cutBy": polygons = toCut_csg.sub_return_cutBy(cutBy_csg).toPolygons() elif bool_type == "sub_return_toCut": polygons = toCut_csg.sub_return_toCut(cutBy_csg).toPolygons() elif bool_type == "sub_return_hole": polygons = toCut_csg.sub_return_hole(cutBy_csg).toPolygons() elif bool_type == "test": polygons = toCut_csg.test(cutBy_csg).toPolygons() elif bool_type == "skip": polygons = toCut_csg.toPolygons() verts, pverts, uvs, puvs, mats, tracking = new_csg.export_to_mymesh(polygons,colors=colors) #============================================================================ # A lot of cleanup stuff follows. PyCSG just sort of whips through and makes a mess of the polys, # and the process it uses leaves little room for refinement. Post-cut cleanup is necessary, sadly. #============================================================================ cleanup = 0 if weld == 1: theGUI.status_update("Welding vertices to remove duplicates...") toCut = toCut_mesh.name cutBy = cutBy_mesh.name screen_toCut = [0 for i in verts] screen_cutBy = [0 for i in verts] for pi in range(len(tracking)): name = tracking[pi][0] poly = pverts[pi] for vi in poly: if name == toCut: screen_toCut[vi] = 1 if name == cutBy: screen_cutBy[vi] = 1 verts,pverts = geom_weld(verts,pverts,screenlist=screen_toCut) screen_toCut = [0 for i in verts] screen_cutBy = [0 for i in verts] for pi in range(len(tracking)): name = tracking[pi][0] poly = pverts[pi] for vi in poly: if name == toCut: screen_toCut[vi] = 1 if name == cutBy: screen_cutBy[vi] = 1 verts,pverts = geom_weld(verts,pverts,screenlist=screen_cutBy) elif weld == 2: verts,pverts = geom_weld(verts,pverts)# May want to screen by toCut and cutBy actors if weld: theGUI.status_update("Adding new vertex cuts where needed...") merge = [[] for i in toCut_mesh.pverts] # Source polys to cut polys inv_merge = [-1 for i in pverts] #pneighbors = [[j for j in i] for i in toCut_mesh.pneighbors] for pi in range(len(tracking)): data = tracking[pi] if data[0] == cutAct.Name(): merge[data[1]].append(pi) inv_merge[pi] = data[1] cutBy_merge = [[] for i in cutBy_mesh.pverts] for pi in range(len(tracking)): data = tracking[pi] if data[0] == cutBy_mesh.name: cutBy_merge[data[1]].append(pi) inv_merge[pi] = data[1] for pi in range(len(cutBy_merge)): p = cutBy_merge[pi] if not p: continue merge.append([i for i in p]) #pneighbors.append([i for i in cutBy_mesh.pneighbors[pi]]) #cutBy_merge = [] #pneighbors = [[] for i in pneighbors] fix_pverts = [[j for j in i] for i in pverts] fix_p_uvs = [[j for j in i] for i in puvs] for pi in range(len(merge)): # Add new cuts to edges which were missed by CSG due to order-of-events polys = merge[pi] if not polys: # Not all entries in merge will have listings continue #pnbs = pneighbors[pi] fix_polys = [[j for j in pverts[i]] for i in polys] fix_puvs = [[j for j in puvs[i]] for i in polys] #for nbi in pnbs: #fix_polys.append([j for j in pverts[inv_merge[nbi]]]) #fix_puvs.append([j for j in puvs[inv_merge[nbi]]]) fix_polys,fix_puvs,uvs = fix_poly_cuts(fix_polys,fix_puvs,verts,uvs) #fix_polys = [[j for j in fix_polys[i]] for i in range(len(polys))] # Strip out neighbor listings #fix_puvs = [[j for j in fix_puvs[i]] for i in range(len(polys))] for polys_i in range(len(fix_polys)): polys_f = fix_polys[polys_i] index = polys[polys_i] puvs_f = fix_puvs[polys_i] fix_pverts[index] = [i for i in polys_f] fix_p_uvs[index] = [i for i in puvs_f] pverts = [[j for j in i] for i in fix_pverts] puvs = [[j for j in i] for i in fix_p_uvs] #===== Cleanup: Merge the cut polygons ===== if cleanup: # sort_tris() is not properly configured yet """ Problems with merging back into original source polys: - Some polys may have been cut into two or more pieces by the Boolean process - Poser does not cooperate with geometry creation with certain types of Ngons. Some polys may be concave or degenerate or otherwise wonky. Identifying these and correcting them is problematic. - Poser would require that the polys be re-cut anyway, into tris or quads. - It seems best to retain the structure inherited from PyCSG. """ theGUI.status_update("Sorting edges to remove junk cuts...") fix_pverts = [] fix_p_uvs = [] fix_mats = [] for pi,group in enumerate(merge):# Also need to fix mats and puvs if not group: continue if len(group) > 1: gverts = [[j for j in pverts[i]] for i in group] gtverts = [[j for j in puvs[i]] for i in group] final,final_uv = sort_tris(gverts,gtverts) fix_pverts.append([i for i in final]) fix_p_uvs.append([i for i in final_uv]) #print pi, final else: fix_pverts.append([i for i in pverts[group[0]]]) fix_p_uvs.append([i for i in puvs[group[0]]]) fix_mats.append(mats[group[0]]) pverts = [[j for j in i] for i in fix_pverts] puvs = [[j for j in i] for i in fix_p_uvs] mats = [i for i in fix_mats] #===== Cleanup: Combine pre-processing attempts with post-processing ===== if cleanup: """ This removes redundant vertices, after they have been disconnected by cleanup6. There are conceptual problems with that effort, even though the process here works as intended. """ theGUI.status_update("Removing junk vertices...") if not cleanup5: merge = [[] for i in toCut_mesh.pverts] # Source polys to cut polys for pi in range(len(tracking)): data = tracking[pi] if data[0] == cutAct.Name(): merge[data[1]].append(pi) inv_merge[pi] = data[1] cutBy_merge = [[] for i in cutBy_mesh.pverts] for pi in range(len(tracking)): data = tracking[pi] if data[0] == cutBy_mesh.name: cutBy_merge[data[1]].append(pi) hole_polygons = toCut_csg.sub_return_hole(cutBy_csg).toPolygons() hole_verts = new_csg.export_to_mymesh(hole_polygons,justverts=1) hole_match = ["%.6f,%.6f,%.6f" %(v[0],v[1],v[2]) for v in hole_verts] hole_polygons = hole_verts = [] verts,pverts,uvs,puvs = remove_cuts(verts,pverts,uvs,puvs,hole_match, toCut_mesh.verts,cutBy_mesh.verts,merge,cutBy_merge) #=================================================================================================== # End cleanup #=================================================================================================== toCut_mesh.verts = verts toCut_mesh.pverts = pverts toCut_mesh.uvs = uvs toCut_mesh.puvs = puvs toCut_mesh.mats = mats toCut_mesh.geom_polys(toCut_mesh.geom,toCut_mesh.pverts,toCut_mesh.puvs) verts = Numeric.array(toCut_mesh.verts,Numeric.Float) polys = toCut_mesh.gpolys sets = toCut_mesh.gsets tverts = Numeric.array(toCut_mesh.uvs,Numeric.Float) tpolys = toCut_mesh.gtpolys tsets = toCut_mesh.gtsets """ # Triangulate by simple fanning: toCut_mesh.triverts(toCut_mesh.pverts,matlist=toCut_mesh.mats,puv=toCut_mesh.puvs) toCut_mesh.geom_polys(toCut_mesh.geom,toCut_mesh.tverts,toCut_mesh.tuvverts) verts = Numeric.array(toCut_mesh.verts,Numeric.Float) polys = toCut_mesh.gpolys sets = toCut_mesh.gsets tverts = Numeric.array(toCut_mesh.uvs,Numeric.Float) tpolys = toCut_mesh.gtpolys tsets = toCut_mesh.gtsets """ # Create the new geometry newgeom = poser.NewGeometry() newgeom.AddGeneralMesh(polys,sets,verts,tpolys,tsets,tverts) theGUI.status_update("Creating geometry...") # Make a prop from the geometry #newprop = scene.CreatePropFromGeom(newgeom,"boolean_test") newprop = scene.CreatePropFromGeom(newgeom,"%s_%s"%(toCut,bool_type)) if not newprop: theGUI.status_update("Script failed! No prop created.") return scene.SelectActor(newprop) newprop.SetSmoothPolys(0) # Turn off micropolygon smoothing # Refresh the geometry by pulling a copy of it from the created prop newgeom = newprop.Geometry() # This is necessary to enable Poser 5 and 6 to add new materials to the geom. theGUI.status_update("Adding materials...") matnames = [mat.Name() for mat in toCut_mesh.geom.Materials()] for matname in matnames: newgeom.AddMaterialName(matname) mats = toCut_mesh.mats for pi in range(len(mats)): m = mats[pi] p = newgeom.Polygon(pi) p.SetMaterialName(m) # Set the new geometry with added material for the prop newprop.SetGeometry(newgeom) random_colors = colors # For testing only. Apply random colors, based on source poly correlations in cut mesh. if random_colors == 1: # Works with "colors" arg in csg.export_to_mymesh() set to 1 import random for mat in newgeom.Materials(): matname = mat.Name() if matname: if matname.find("_") != -1: parts = matname.split("_") try: i = int(parts[len(parts)-1]) except ValueError: continue g = float(i) * 0.025 mat.SetDiffuseColor(random.random(), random.random(), random.random()) if use_clean_weld: clean_weld(newgeom,sets=[],replace=1,has_UVs=1) # Apply clean weld after the actor has been created. newprop.SetGeometry(newgeom) #theGUI.status_update("Done!") scene.DrawAll() def prop_from_myMesh(mesh,name): mesh.geom_polys(mesh.geom,mesh.pverts,mesh.puvs) verts = Numeric.array(mesh.verts,Numeric.Float) polys = mesh.gpolys sets = mesh.gsets tverts = Numeric.array(mesh.uvs,Numeric.Float) tpolys = mesh.gtpolys tsets = mesh.gtsets newgeom = poser.NewGeometry() newgeom.AddGeneralMesh(polys,sets,verts,tpolys,tsets,tverts) newprop = scene.CreatePropFromGeom(newgeom,name) scene.DrawAll() """ Need to verify that the selected objects overlap, or the CSG code will just keep looping. """ def intersectAABBs(box1,box2,intersect=0): """ Adapted from '3D Math Primer for Graphics and Game Development', by Fletcher Dunn and Ian Parberry www.gamemath.com original code is avaialble in downloads section of website. bounding box format is [minX,maxX,minY,maxY,minZ,maxZ] """ # Check for no overlap if box1[0] > box2[1]: return 0 if box1[1] < box2[0]: return 0 if box1[2] > box2[3]: return 0 if box1[3] < box2[2]: return 0 if box1[4] > box2[5]: return 0 if box1[5] < box2[4]: return 0 # We have overlap. Compute AABB of intersection, if they want it if intersect: boxIntersect = [0.0,0.0,0.0,0.0,0.0,0.0] boxIntersect[0] = max(box1[0], box2[0]) boxIntersect[1] = min(box1[1], box2[1]) boxIntersect[2] = max(box1[2], box2[2]) boxIntersect[3] = min(box1[3], box2[3]) boxIntersect[4] = max(box1[4], box2[4]) boxIntersect[5] = min(box1[5], box2[5]) return 1,boxIntersect # They intersected return 1 #================================================================================== # Mesh cleanup functions #================================================================================== def geom_weld(verts,pverts,screenlist=[]): """Replicates clean_weld() function, using myMesh verts and pverts as input""" screen = 1 if not screenlist: screen = 0 matchdict = {} replace = range(len(verts)) for vi in range(len(verts)): if (screen == 0) or ((screen ==1) and (screenlist[vi] == 1)): v = verts[vi] tup = "%.6f,%.6f,%.6f" %(v[0],v[1],v[2]) # Clamp the values to 5 decimal places to facilitate matches. if not tup in matchdict.keys(): matchdict[tup] = vi else: replace[vi] = matchdict[tup] # Find duplicated vertices pverts_replace = [] for pi in range(len(pverts)): pv = pverts[pi] pverts_replace.append([replace[i] for i in pv]) # Revise polys to remove duplicates replace2 = [] for vi in replace: if not vi in replace2: replace2.append(vi) # Trim the vert indices to remove duplicates verts_replace = [[j for j in verts[i]] for i in replace2] # Build new verts list pverts_replace = [[replace2.index(j) for j in i] for i in pverts_replace] # Re-format polys with corrected indices return verts_replace,pverts_replace def clean_weld(geom,sets=[],replace=1,has_UVs=0): """ Weld() preserves vert count by creating stray verts without any poly association. These strays can create potential Python scripting problems, as well as unnecessarily inflate the object size and morph delta count. This applies weld, then creates a new geometry which has the strays removed. If the actor has no UV data, a planar map is created and applied to the new geometry. """ geom.Weld() if not sets: sets = [i for i in geom.Sets()] polys = [[p.Start(),p.NumVertices()] for p in geom.Polygons()] vlist = [1 for i in range(geom.NumVertices())] for vi in sets: vlist[vi] = 0 verts = geom.Vertices() verts = [[verts[i].X(),verts[i].Y(),verts[i].Z()] for i in range(geom.NumVertices()) if not vlist[i]] count = 0 remverts = [0 for i in range(geom.NumVertices())] # Remverts data can be used to re-order morph targets for vi in range(geom.NumVertices()): remverts[vi] = vi - count count += vlist[vi] for si in range(len(sets)): s = sets[si] sets[si] = remverts[s] sets = Numeric.array(sets,Numeric.Int) polys = Numeric.array(polys,Numeric.Int) verts = Numeric.array(verts,Numeric.Float) tverts = Numeric.array([[i.U(),i.V()] for i in geom.TexVertices()],Numeric.Float) tsets = [i for i in geom.TexSets()] tpolys = [[p.Start(),p.NumTexVertices()] for p in geom.TexPolygons()] tsets = Numeric.array(tsets,Numeric.Int) tpolys = Numeric.array(tpolys,Numeric.Int) return verts, polys, sets, tsets, tpolys, tverts def matched_verts(verts,cutverts): """""" matchdict = {} correlated = [[] for i in verts] for vi in range(len(verts)): v = verts[vi] tup = "%.6f,%.6f,%.6f" %(v[0],v[1],v[2]) # Clamp the values to 5 decimal places to facilitate matches. if not tup in matchdict.keys(): matchdict[tup] = [vi] else: matchdict[tup].append(vi) for vi in range(len(cutverts)): v = cutverts[vi] tup = "%.6f,%.6f,%.6f" %(v[0],v[1],v[2]) if tup in matchdict.keys(): for i in matchdict[tup]: correlated[i].append(vi) #testing_boxes(v,0.0025,"hits",0,1) return correlated def geom_edges(polys,puvs=[]): """""" if puvs: use_uvs = 1 len_polys = len(polys) edges = [] if use_uvs: uv_edges = [] for ti in range(len(polys)): # Gather all the edges t = polys[ti] if not t: continue if use_uvs: tuv = puvs[ti] e = [] if use_uvs: uv_e = [] for i in range(len(t)): if i == len(t)-1: j = 0 else: j = i + 1 e.append([t[i],t[j]]) if use_uvs: uv_e.append([tuv[i],tuv[j]]) edges.append([[j for j in i] for i in e]) if use_uvs: uv_edges.append([[j for j in i] for i in uv_e]) if use_uvs: return edges, uv_edges else: return edges def vector_copy(v): return Numeric.array([v[0],v[1],v[2]],Numeric.Float) def point_on_line(v,e1,e2,tolerance=None): """ http://www.mathworks.com/matlabcentral/newsreader/view_thread/170200 ============================ If P1 and P2 are vectors giving the endpoint coordinates of the line segment and P the coordinates of an arbitrary point, then P lies within the line segment whenever (norm(cross(P-P1,P2-P1)) < tol) & ... (dot(P-P1,P2-P1) >= 0) & (dot(P-P2,P2-P1) <= 0) is true, where 'tol' is just large enough to allow for worst case round-off error in performing the 'cross', 'dot', and 'norm' operations. Roger Stafford """ """ v = point, as Numeric array e1, e2 = endpoints of line, both as Numeric arrays """ if not isinstance(v,Numeric.ndarray): v = Numeric.array(v,Numeric.Float) if not isinstance(e2,Numeric.ndarray): e2 = Numeric.array(e2,Numeric.Float) if not isinstance(e1,Numeric.ndarray): e1 = Numeric.array(e1,Numeric.Float) if not tolerance: tolerance = 1.19209290e-07**2 # Tested as best setting for Poser-scaled geometries if vector_length(vector_crossproduct(v-e1,e2-e1)) < tolerance: if vector_dotproduct(v-e1,e2-e1) >= 0.0: if vector_dotproduct(v-e2,e2-e1) <= 0.0: return 1 # Point is on line return 0 # Point not on line def point_in_polygon(test_point,poly_points,test_value=6.2): """ http://paulbourke.net/geometry/polygonmesh/ 'Determining if a point lies on the interior of a polygon' Solution 4 (3D) This solution was motivated by solution 2 and correspondence with Reinier van Vliet and Remco Lam. To determine whether a point is on the interior of a convex polygon in 3D one might be tempted to first determine whether the point is on the plane, then determine it's interior status. Both of these can be accomplished at once by computing the sum of the angles between the test point (q below) and every pair of edge points p[i]->p[i+1]. This sum will only be 2pi if both the point is on the plane of the polygon AND on the interior. The angle sum will tend to 0 the further away from the polygon point q becomes. The following code snippet returns the angle sum between the test point q and all the vertex pairs. Note that the angle sum is returned in radians. """ #================================================= # This function works best for Poser scale if sent vertices are multiplied by 10.0. #pvs = [] #for i in p: # pvs.append(vector_copy(toCut_verts[i]*10.0)) # Poser scale is too small; scale up #... #cpv = vector_copy(verts[cpvi])*10.0 # Poser scale is too small; scale up #inside = point_in_polygon(cpv,pvs) #if inside: # do stuff #================================================= EPSILON = 0.0000001 TWOPI = 6.283185307179586476925287 RTOD = 57.2957795 def modulus(p): return math.sqrt(p[0]*p[0] + p[1]*p[1] + p[2]*p[2]) def CalcAngleSum(q,p,n): anglesum=0 p1 = [0.0,0.0,0.0] p2 = [0.0,0.0,0.0] for i in range(n): p1[0] = p[i][0] - q[0] p1[1] = p[i][1] - q[1] p1[2] = p[i][2] - q[2] p2[0] = p[(i+1)%n][0] - q[0] p2[1] = p[(i+1)%n][1] - q[1] p2[2] = p[(i+1)%n][2] - q[2] #m1 = modulus(p1) #m2 = modulus(p2) m1 = math.sqrt(p1[0]*p1[0] + p1[1]*p1[1] + p1[2]*p1[2]) m2 = math.sqrt(p2[0]*p2[0] + p2[1]*p2[1] + p2[2]*p2[2]) if m1*m2 <= EPSILON: return TWOPI # We are on a node, consider this inside else: costheta = (p1[0]*p2[0] + p1[1]*p2[1] + p1[2]*p2[2]) / (m1*m2) anglesum += math.acos(costheta) return anglesum inside = CalcAngleSum(test_point,poly_points,len(poly_points)) #if inside == TWOPI: #return 1 #return 0 if inside >= test_value: # Code needs some wiggle-room, apparently.... return 1 return 0 def fix_poly_cuts(polys,puvs,verts,uvs,pneighbors=[]): """ Work with cut polys from same source poly, checking for vertices that lie on the line of a neighboring cut poly but are not part of that poly. Integrate these verts where necessary. Once cut polys are processed, spread out and compare each cut poly to its neighbors, the same way. After these two passes, all edges where vertex cuts were not properly propagated should have been fixed. After this, inner edges can be isolated using sort_tris() and the cut polys can be merged back into their source elements. Neighbors of polys may need to be drawn from the mesh data of the source geometry, since the cut geom is unreliably welded at this point and finding neighbors requires shared verts and edges. polys = pverts of polys in this merge-set. verts = full verts list """ tolerance = 1.19209290e-07**2 len_polys = len(polys) edges = [] uv_edges = [] hits = [] verts_match = ["%.6f,%.6f,%.6f" %(v[0],v[1],v[2]) for v in verts] for ti in range(len(polys)): # Gather all the edges t = polys[ti] tuv = puvs[ti] e = [] uv_e = [] for i in range(len(t)): if i == len(t)-1: j = 0 else: j = i + 1 e.append([t[i],t[j]]) uv_e.append([tuv[i],tuv[j]]) edges.append([[j for j in i] for i in e]) uv_edges.append([[j for j in i] for i in uv_e]) hits.append([[] for i in e]) hit_found = 0 for pi_1 in range(len(polys)): poly = polys[pi_1] for vi in poly: v = vector_copy(verts[vi]) for pi in range(len_polys): if pi == pi_1: continue for ei in range(len(edges[pi])): edge = edges[pi][ei] if edge[0] == vi or edge[1] == vi: continue if (verts_match[vi] == verts_match[edge[0]]) or (verts_match[vi] == verts_match[edge[1]]): continue e1 = vector_copy(verts[edge[0]]) e2 = vector_copy(verts[edge[1]]) # Point on line code follows: if vector_length(vector_crossproduct(v-e1,e2-e1)) < tolerance: if vector_dotproduct(v-e1,e2-e1) >= 0.0: if vector_dotproduct(v-e2,e2-e1) <= 0.0: hits[pi][ei].append(vi) hit_found = 1 if hit_found: polys_replace = [[] for i in polys] puvs_replace = [[] for i in puvs] for pi in range(len_polys): # Insert any hit verts into polys, then move on. for ei in range(len(edges[pi])): polys_replace[pi].append(edges[pi][ei][0]) puvs_replace[pi].append(uv_edges[pi][ei][0]) if hits[pi][ei] != []: edge_hit = [i for i in hits[pi][ei]] sort_hits = {} for i in range(len(edge_hit)): vi = edge_hit[i] sort_hits[vector_length(vector_copy(verts[edges[pi][ei][0]])-vector_copy(verts[vi]))] = vi keys = [i for i in sort_hits.keys()] keys.sort() edge_hit = [] for k in keys: edge_hit.append(sort_hits[k]) for vi in edge_hit: polys_replace[pi].append(vi) shortv = vector_length(vector_copy(verts[edges[pi][ei][0]])-vector_copy(verts[vi])) longv = vector_length(vector_copy(verts[edges[pi][ei][0]])-vector_copy(verts[edges[pi][ei][1]])) if longv < FLT_EPSILON: longv = FLT_EPSILON distv = shortv/longv s_uv = Numeric.array(uvs[uv_edges[pi][ei][0]],Numeric.Float) e_uv = Numeric.array(uvs[uv_edges[pi][ei][1]],Numeric.Float) pos = s_uv + ((e_uv - s_uv) * distv) uvs.append([pos[0],pos[1]]) index = len(uvs)-1 puvs_replace[pi].append(index) return polys_replace,puvs_replace,uvs else: return polys,puvs,uvs def sort_tris(polys,puvs): """ Find the valid outer edges of the source polys, remove the invalid edges. Flag the vertices of invalid edges for removal. Merge the cut polygons back into their source form. """ len_polys = len(polys) edges = [] uv_edges = [] for ti in range(len(polys)): # Gather all the edges t = polys[ti] tuv = puvs[ti] e = [] uv_e = [] for i in range(len(t)): if i == len(t)-1: j = 0 else: j = i + 1 e.append([t[i],t[j]]) uv_e.append([tuv[i],tuv[j]]) edges.append([[j for j in i] for i in e]) uv_edges.append([[j for j in i] for i in uv_e]) valid = [[0 for j in i] for i in polys] for i in range(len_polys): # Locate outer and inner edges current_poly = edges[i] for j in range(len_polys): if j == i: # Omit the current edge itself continue other_poly = edges[j] for edge_index in range(len(current_poly)): # For each current poly... edges1 = current_poly[edge_index] for edge_index2 in range(len(other_poly)): # Loop through all other polys edges2 = other_poly[edge_index2] if edges1[0] == edges2[0] and edges1[1] == edges2[1]: # Find shared edges valid[j][edge_index2] = -1 valid[i][edge_index] = -1 elif edges1[0] == edges2[1] and edges1[1] == edges2[0]: # Find shared edges valid[j][edge_index2] = 1 valid[i][edge_index] = 1 #print valid good = [] good_uv = [] for pi in range(len_polys): # Only the outer edges p = valid[pi] for edge_index in range(len(p)): e = p[edge_index] if e == 0: if not edges[pi][edge_index] in good: good.append(edges[pi][edge_index]) good_uv.append(uv_edges[pi][edge_index]) #print good final = [good[0][0]] final_uv = [good_uv[0][0]] for edge_index in range(len(good)): # Put the outer edges in order for edge_index1 in range(len(good)): e = good[edge_index1] euv = good_uv[edge_index1] if e[1] == final[len(final)-1]: final.append(e[0]) final_uv.append(euv[0]) break #final = final[:-1] # Drop the last entry final.reverse() # Reverse list #final_uv = final[:-1] final_uv.reverse() return final, final_uv def remove_cuts(verts,pverts,uvs,puvs,hole_match,toCut_verts,cutBy_verts,merge,cutBy_merge): """ verts to pverts to puvs to uvs """ toCut_match = ["%.6f,%.6f,%.6f" %(v[0],v[1],v[2]) for v in toCut_verts] cutBy_match = ["%.6f,%.6f,%.6f" %(v[0],v[1],v[2]) for v in cutBy_verts] verts_match = ["%.6f,%.6f,%.6f" %(v[0],v[1],v[2]) for v in verts] verts_screen = [0 for i in verts] for vi,v in enumerate(verts): pos = verts_match[vi] if (pos in hole_match) or (pos in toCut_match) or (pos in cutBy_match): verts_screen[vi] = 1 verts_convert = [-1 for i in verts] fix_verts = [] for vi,v in enumerate(verts): if verts_screen[vi]: fix_verts.append([i for i in verts[vi]]) verts_convert[vi] = len(fix_verts)-1 fix_pverts = [] fix_puvs = [] uvs_screen = [0 for i in uvs] for pi,poly in enumerate(pverts): fix_poly = [] fix_puv = [] for i,vi in enumerate(poly): if verts_screen[vi]: fix_poly.append(verts_convert[vi]) fix_puv.append(puvs[pi][i]) uvs_screen[puvs[pi][i]] = 1 fix_pverts.append([i for i in fix_poly]) fix_puvs.append([i for i in fix_puv]) uvs_convert = [-1 for i in uvs] fix_uvs = [] for uvi,uv in enumerate(uvs): if uvs_screen[uvi]: fix_uvs.append([i for i in uvs[uvi]]) uvs_convert[uvi] = len(fix_uvs)-1 for pi,p in enumerate(fix_pverts): fix_puv = [] for uvi in p: fix_puv.append(uvs_convert[uvi]) fix_puvs[pi] = [i for i in fix_puv] return fix_verts,fix_pverts,fix_uvs,fix_puvs #======================================================================================================= # pycsg-0.2 # https://github.com/evanw/csg.js (Evan Wallace original) # https://pypi.python.org/pypi/pycsg/0.2 # http://pythonhosted.org//pycsg/#implementation-details # https://github.com/timknip/pycsg/blob/master/examples/pyopengl.py # https://github.com/timknip/pycsg # Copyright (c) 2011 Evan Wallace (http://madebyevan.com/), under the MIT license. # Python port Copyright (c) 2012 Tim Knip (http://www.floorplanner.com), under the MIT license. # This is a Poser Python adaptation of PyCSG, itself an adaptation of Java code. # It is released under the MIT License, as required by the sources from which it is derived. # A copy of the MIT License should have been included in the zip file which contained this script. # The MIT License is presented at the top of this script. # Modifications have been made here to the core.py CSG class (adding a new custom geometry type, to "import" Poser # into the pycsg format) and to geom.py classes (to retain materials and UV data). #======================================================================================================= """ Metadata-Version: 1.1 Name: pycsg Version: 0.2 Summary: Constructive Solid Geometry (CSG) Home-page: https://github.com/timknip/pycsg Author: Tim Knip Author-email: tim@floorplanner.com License: MIT Description: pycsg ===== Constructive Solid Geometry (CSG) is a modeling technique that uses Boolean operations like union and intersection to combine 3D solids. This library implements CSG operations on meshes elegantly and concisely using BSP trees, and is meant to serve as an easily understandable implementation of the algorithm. All edge cases involving overlapping coplanar polygons in both solids are correctly handled. examples ======== $ python examples/pyopengl depends on: PyOpenGL PyOpenGL_accelerate Keywords: constructive solid geometry csg utilities Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Topic :: Utilities Classifier: License :: OSI Approved :: MIT License """ #__init__.py #__version__ = 0.2 #core.py #import math #from geom import * class CSG(object): """ Constructive Solid Geometry (CSG) is a modeling technique that uses Boolean operations like union and intersection to combine 3D solids. This library implements CSG operations on meshes elegantly and concisely using BSP trees, and is meant to serve as an easily understandable implementation of the algorithm. All edge cases involving overlapping coplanar polygons in both solids are correctly handled. Example usage:: from csg.core import CSG cube = CSG.cube(); sphere = CSG.sphere({'radius': 1.3}); polygons = cube.subtract(sphere).toPolygons(); ## Implementation Details All CSG operations are implemented in terms of two functions, `clipTo()` and `invert()`, which remove parts of a BSP tree inside another BSP tree and swap solid and empty space, respectively. To find the union of `a` and `b`, we want to remove everything in `a` inside `b` and everything in `b` inside `a`, then combine polygons from `a` and `b` into one solid:: a.clipTo(b); b.clipTo(a); a.build(b.allPolygons()); The only tricky part is handling overlapping coplanar polygons in both trees. The code above keeps both copies, but we need to keep them in one tree and remove them in the other tree. To remove them from `b` we can clip the inverse of `b` against `a`. The code for union now looks like this:: a.clipTo(b); b.clipTo(a); b.invert(); b.clipTo(a); b.invert(); a.build(b.allPolygons()); Subtraction and intersection naturally follow from set operations. If union is `A | B`, subtraction is `A - B = ~(~A | B)` and intersection is `A & B = ~(~A | ~B)` where `~` is the complement operator. ## License Copyright (c) 2011 Evan Wallace (http://madebyevan.com/), under the MIT license. Python port Copyright (c) 2012 Tim Knip (http://www.floorplanner.com), under the MIT license. """ def __init__(self): self.polygons = [] @classmethod def fromPolygons(cls, polygons): csg = CSG() csg.polygons = polygons return csg def clone(self): csg = CSG() csg.polygons = map(lambda p: p.clone(), self.polygons) return csg def toPolygons(self): return self.polygons def union(self, csg): """ Return a new CSG solid representing space in either this solid or in the solid `csg`. Neither this solid nor the solid `csg` are modified.:: A.union(B) +-------+ +-------+ | | | | | A | | | | +--+----+ = | +----+ +----+--+ | +----+ | | B | | | | | | | +-------+ +-------+ """ a = csgNode(self.clone().polygons) b = csgNode(csg.clone().polygons) a.clipTo(b) b.clipTo(a) b.invert() b.clipTo(a) b.invert() a.build(b.allPolygons()) return CSG.fromPolygons(a.allPolygons()) def subtract(self, csg): """ Return a new CSG solid representing space in this solid but not in the solid `csg`. Neither this solid nor the solid `csg` are modified.:: A.subtract(B) +-------+ +-------+ | | | | | A | | | | +--+----+ = | +--+ +----+--+ | +----+ | B | | | +-------+ """ a = csgNode(self.clone().polygons) b = csgNode(csg.clone().polygons) a.invert() a.clipTo(b) b.clipTo(a) b.invert() b.clipTo(a) b.invert() a.build(b.allPolygons()) a.invert() return CSG.fromPolygons(a.allPolygons()) def intersect(self, csg): """ Return a new CSG solid representing space both this solid and in the solid `csg`. Neither this solid nor the solid `csg` are modified.:: A.intersect(B) +-------+ | | | A | | +--+----+ = +--+ +----+--+ | +--+ | B | | | +-------+ """ a = csgNode(self.clone().polygons) b = csgNode(csg.clone().polygons) a.invert() b.clipTo(a) b.invert() a.clipTo(b) b.clipTo(a) a.build(b.allPolygons()) a.invert() return CSG.fromPolygons(a.allPolygons()) def inverse(self): """ Return a new CSG solid with solid and empty space switched. This solid is not modified. """ csg = self.clone() map(lambda p: p.flip(), csg.polygons) return csg def test(self, csg): # Added for PPyCSG """ Modify this function and send to it, to experimentally determine results of different combinations of the Node functions. """ a = csgNode(self.clone().polygons) b = csgNode(csg.clone().polygons) #a.invert() a.clipTo(b) b.clipTo(a) #b.invert() #b.clipTo(a) #b.invert() a.invert() b.invert()# a.build(b.allPolygons()) #a.invert() return CSG.fromPolygons(a.allPolygons()) def sub_return_hole(self, csg): # Added for PPyCSG """ Perform subtract, return the "hole" portion which would be cut out of the surface of "a" with a full subtract. """ a = csgNode(self.clone().polygons) b = csgNode(csg.clone().polygons) a.invert() b.clipTo(a) b.invert() a.clipTo(b) b.clipTo(a) a.invert() return CSG.fromPolygons(a.allPolygons()) def sub_return_cutBy(self, csg): # Added for PPyCSG """Perform subtract, return only the cutBy portions""" a = csgNode(self.clone().polygons) b = csgNode(csg.clone().polygons) a.invert() a.clipTo(b) b.clipTo(a) b.invert() b.clipTo(a) return CSG.fromPolygons(b.allPolygons()) def sub_return_toCut(self, csg): # Added for PPyCSG """Perform subtract, return only the toCut portions.""" a = csgNode(self.clone().polygons) b = csgNode(csg.clone().polygons) a.invert() a.clipTo(b) b.clipTo(a) b.invert() b.clipTo(a) a.invert() return CSG.fromPolygons(a.allPolygons()) def multiple_runs(self, multiple_csgs): # Added for PPyCSG """ Perform multiple runs of CSG with one call. Example: multiple = [[2,door_csg],[2,w_csg],[2,w2_csg]] multiple = [["subtract",door_csg],["subtract",w_csg],["subtract",w2_csg]] polygons = toCut_csg.multiple_runs(multiple).toPolygons() """ for runtype,csg in multiple_csgs: if runtype == 1 or runtype == "union": self = self.union(csg) elif runtype == 2 or runtype == "subtract": self = self.subtract(csg) elif runtype == 3 or runtype == "intersect": self = self.intersect(csg) return self @classmethod def print_info(cls, bsp): # Added for PPyCSG c = bsp.flattened_pre_order() for item in c: if item.parent: theGui.status_update(item.name, item.level, item.parent.name, item.parent.level) else: theGUI.status_update(item.name, item.level) @classmethod def custom(cls, mesh): # Added for PPyCSG """ Added by Cagedrei for Poser Python version. Import a geometry as PyCSG vertices and polygons, for use with Boolean functions. Argument mesh is a myMesh class instance. """ pverts = mesh.pverts # Vertex indices by polygon verts = mesh.verts # Vertices uvs = mesh.uvs # UV data = texverts puvs = mesh.puvs # UV indices by polygon = texpolygons: sent to Polygon() class objects mats = mesh.mats # Material names by polygon polygons = [] for pi in range(len(pverts)): vertices = [csgVertex([i for i in verts[vi]]) for vi in pverts[pi]] tverts = [csgTexVertex([uvs[vi][0],uvs[vi][1],1.0]) for vi in puvs[pi]] # U,V,W: W will be ignored mat = mats[pi] tracking = [mesh.name,pi] polygons.append(csgPolygon(vertices, shared=tracking, mat=mat,tverts=tverts)) return CSG.fromPolygons(polygons) @classmethod def export_to_mymesh(cls, polygons,colors=1,justverts=0): # Added for PPyCSG """ Receive a csg polygons list, convert to Poser geometry data in myMesh format. This can then be further modified or reformatted for use in creating an actual Poser geometry. Use myMesh.geompolys() to send to arrays to be used as a poser.NewGeometry() object. """ verts = [] pverts = [] puvs = [] uvs = [] mats = [] total_v = 0 total_uv = 0 tracking = [] for pi,p in enumerate(polygons): if colors: mats.append("polygon_%s_%s"%(p.shared[0],p.shared[1])) else: mats.append(p.mat) tracking.append([i for i in p.shared]) v = p.vertices pv = [] for vi in range(len(v)): vt = v[vi] verts.append([vt.pos[0],vt.pos[1],vt.pos[2]]) pv.append(vi + total_v) pverts.append([i for i in pv]) uv = p.tverts puv = [] for vi in range(len(uv)): uvvt = uv[vi] uvs.append([uvvt.pos[0],uvvt.pos[1]]) puv.append(vi + total_uv) puvs.append([i for i in puv]) total_v += len(v) total_uv += len(uv) if justverts: return verts return verts, pverts, uvs, puvs, mats, tracking #=============================================================================== # PyCSG built-in primitives are not currently compatible with this script. # Need to add tverts, mats, and tracking. #=============================================================================== @classmethod def cube(cls, center=[0,0,0], radius=[1,1,1]): """ Construct an axis-aligned solid cuboid. Optional parameters are `center` and `radius`, which default to `[0, 0, 0]` and `[1, 1, 1]`. The radius can be specified using a single number or a list of three numbers, one for each axis. Example code:: cube = CSG.cube( center=[0, 0, 0], radius=1 ) """ c = csgVector(0, 0, 0) r = [1, 1, 1] if isinstance(center, list): c = csgVector(center) if isinstance(radius, list): r = radius else: r = [radius, radius, radius] polygons = map( lambda v: csgPolygon( map(lambda i: csgVertex( csgVector( c.x + r[0] * (2 * bool(i & 1) - 1), c.y + r[1] * (2 * bool(i & 2) - 1), c.z + r[2] * (2 * bool(i & 4) - 1) ), None ), v[0])), [ [[0, 4, 6, 2], [-1, 0, 0]], [[1, 3, 7, 5], [+1, 0, 0]], [[0, 1, 5, 4], [0, -1, 0]], [[2, 6, 7, 3], [0, +1, 0]], [[0, 2, 3, 1], [0, 0, -1]], [[4, 5, 7, 6], [0, 0, +1]] ]) return CSG.fromPolygons(polygons) @classmethod def sphere(cls, **kwargs): """ Returns a sphere. Kwargs: center (list): Center of sphere, default [0, 0, 0]. radius (float): Radius of sphere, default 1.0. slices (int): Number of slices, default 16. stacks (int): Number of stacks, default 8. """ center = kwargs.get('center', [0.0, 0.0, 0.0]) if isinstance(center, float): center = [center, center, center] c = csgVector(center) r = kwargs.get('radius', 1.0) if isinstance(r, list) and len(r) > 2: r = r[0] slices = kwargs.get('slices', 16) stacks = kwargs.get('stacks', 8) polygons = [] sl = float(slices) st = float(stacks) def vertex(vertices, theta, phi): theta *= math.pi * 2.0 phi *= math.pi d = Vector( math.cos(theta) * math.sin(phi), math.cos(phi), math.sin(theta) * math.sin(phi)) vertices.append(csgVertex(c.plus(d.times(r)), d)) for i in range(0, slices): for j in range(0, stacks): vertices = [] vertex(vertices, i / sl, j / st) if j > 0: vertex(vertices, (i + 1) / sl, j / st) if j < stacks - 1: vertex(vertices, (i + 1) / sl, (j + 1) / st) vertex(vertices, i / sl, (j + 1) / st) polygons.append(csgPolygon(vertices)) return CSG.fromPolygons(polygons) @classmethod def cylinder(cls, **kwargs): """ Returns a cylinder. Kwargs: start (list): Start of cylinder, default [0, -1, 0]. end (list): End of cylinder, default [0, 1, 0]. radius (float): Radius of cylinder, default 1.0. slices (int): Number of slices, default 16. """ s = kwargs.get('start', csgVector(0.0, -1.0, 0.0)) e = kwargs.get('end', csgVector(0.0, 1.0, 0.0)) if isinstance(s, list): s = csgVector(*s) if isinstance(e, list): e = csgVector(*e) r = kwargs.get('radius', 1.0) slices = kwargs.get('slices', 16) ray = e.minus(s) axisZ = ray.unit() isY = (math.fabs(axisZ.y) > 0.5) axisX = csgVector(float(isY), float(not isY), 0).cross(axisZ).unit() axisY = axisX.cross(axisZ).unit() start = csgVertex(s, axisZ.negated()) end = csgVertex(e, axisZ.unit()) polygons = [] def point(stack, slice, normalBlend): angle = slice * math.pi * 2.0 out = axisX.times(math.cos(angle)).plus( axisY.times(math.sin(angle))) pos = s.plus(ray.times(stack)).plus(out.times(r)) normal = out.times(1.0 - math.fabs(normalBlend)).plus( axisZ.times(normalBlend)) return csgVertex(pos, normal) for i in range(0, slices): t0 = i / float(slices) t1 = (i + 1) / float(slices) polygons.append(csgPolygon([start, point(0., t0, -1.), point(0., t1, -1.)])) polygons.append(csgPolygon([point(0., t1, 0.), point(0., t0, 0.), point(1., t0, 0.), point(1., t1, 0.)])) polygons.append(csgPolygon([end, point(1., t1, 1.), point(1., t0, 1.)])) return CSG.fromPolygons(polygons) #geom.py #import math #============================================================ # Namespace changes have been made to geom.py functions. # the prefix "csg" has been added to Vector(), Vertex(), Polygon(), # Plane(), Node(), and (added for PPyCSG) TexVertex(). # The inherited class names were too generic for flexible use of this # code, when embedding it into a Poser Python script. #============================================================ class csgVector(object): """ class Vector Represents a 3D vector. Example usage: Vector(1, 2, 3); Vector([1, 2, 3]); Vector({ 'x': 1, 'y': 2, 'z': 3 }); """ def __init__(self, *args): if len(args) == 3: self.x = args[0] self.y = args[1] self.z = args[2] elif len(args) == 1 and isinstance(args[0], csgVector): self.x = args[0][0] self.y = args[0][1] self.z = args[0][2] elif len(args) == 1 and isinstance(args[0], list): self.x = args[0][0] self.y = args[0][1] self.z = args[0][2] elif len(args) == 1 and args[0] and 'x' in args[0]: self.x = args[0]['x'] self.y = args[0]['y'] self.z = args[0]['z'] else: self.x = 0.0 self.y = 0.0 self.z = 0.0 def clone(self): """ Clone. """ return csgVector(self.x, self.y, self.z) def negated(self): """ Negated. """ return csgVector(-self.x, -self.y, -self.z) def plus(self, a): """ Add. """ return csgVector(self.x+a.x, self.y+a.y, self.z+a.z) def minus(self, a): """ Subtract. """ return csgVector(self.x-a.x, self.y-a.y, self.z-a.z) def times(self, a): """ Multiply. """ return csgVector(self.x*a, self.y*a, self.z*a) def dividedBy(self, a): """ Divide. """ if not a: # Added for PPyCSG return self # Added for PPyCSG return csgVector(self.x/a, self.y/a, self.z/a) def dot(self, a): """ Dot. """ return self.x*a.x + self.y*a.y + self.z*a.z def lerp(self, a, t): """ Lerp. """ return self.plus(a.minus(self).times(t)) def length(self): """ Length. """ return math.sqrt(self.dot(self)) def unit(self): """ Normalize. """ return self.dividedBy(self.length()) def cross(self, a): """ Cross. """ return csgVector( self.y * a.z - self.z * a.y, self.z * a.x - self.x * a.z, self.x * a.y - self.y * a.x) def __getitem__(self, key): return (self.x, self.y, self.z)[key] def __setitem__(self, key, value): l = [self.x, self.y, self.z] l[key] = value self.x, self.y, self.z = l def __len__(self): return 3 def __iter__(self): return iter((self.x, self.y, self.z)) def __repr__(self): return 'csgVector(%.2f, %.2f, %0.2f)' % (self.x, self.y, self.z) class csgVertex(object): """ Class Vertex Represents a vertex of a polygon. Use your own vertex class instead of this one to provide additional features like texture coordinates and vertex colors. Custom vertex classes need to provide a `pos` property and `clone()`, `flip()`, and `interpolate()` methods that behave analogous to the ones defined by `Vertex`. This class provides `normal` so convenience functions like `CSG.sphere()` can return a smooth vertex normal, but `normal` is not used anywhere else. """ def __init__(self, pos, normal=None): self.pos = csgVector(pos) self.normal = csgVector(normal) def clone(self): return csgVertex(self.pos.clone(), self.normal.clone()) def flip(self): """ Invert all orientation-specific data (e.g. vertex normal). Called when the orientation of a polygon is flipped. """ self.normal = self.normal.negated() def interpolate(self, other, t): """ Create a new vertex between this vertex and `other` by linearly interpolating all properties using a parameter of `t`. Subclasses should override this to interpolate additional properties. """ return csgVertex(self.pos.lerp(other.pos, t), self.normal.lerp(other.normal, t)) class csgTexVertex(object): # Added for PPyCSG """ Class TexVertex Represents a texture vertex (UV) of a polygon. Use your own vertex class instead of this one to provide additional features like texture coordinates and vertex colors. Custom vertex classes need to provide a `pos` property and `clone()`, `flip()`, and `interpolate()` methods that behave analogous to the ones defined by `Vertex`. This class provides `normal` so convenience functions like `CSG.sphere()` can return a smooth vertex normal, but `normal` is not used anywhere else. """ def __init__(self, pos, normal=None): self.pos = csgVector(pos) if normal: self.normal = normal # May use for W in [UVW]? else: self.normal = csgVector([0.0,0.0,1.0]) def clone(self): return csgTexVertex(self.pos.clone(), self.normal.clone()) def flip(self): """ Invert all orientation-specific data (e.g. vertex normal). Called when the orientation of a polygon is flipped. """ self.normal = self.normal.negated() def interpolate(self, other, t): """ Create a new vertex between this vertex and `other` by linearly interpolating all properties using a parameter of `t`. Subclasses should override this to interpolate additional properties. """ return csgTexVertex(self.pos.lerp(other.pos, t), self.normal.lerp(other.normal, t)) class csgPlane(object): """ class Plane Represents a plane in 3D space. """ """ `Plane.EPSILON` is the tolerance used by `splitPolygon()` to decide if a point is on the plane. """ EPSILON = 1e-5 def __init__(self, normal, w): self.normal = normal self.w = w @classmethod def fromPoints(cls, a, b, c): n = b.minus(a).cross(c.minus(a)).unit() return csgPlane(n, n.dot(a)) def clone(self): return csgPlane(self.normal.clone(), self.w) def flip(self): self.normal = self.normal.negated() self.w = -self.w def splitPolygon(self, polygon, coplanarFront, coplanarBack, front, back): # Modified for PPyCSG """ Split `polygon` by this plane if needed, then put the polygon or polygon fragments in the appropriate lists. Coplanar polygons go into either `coplanarFront` or `coplanarBack` depending on their orientation with respect to this plane. Polygons in front or in back of this plane go into either `front` or `back` """ """ COPLANAR | COPLANAR = COPLANAR COPLANAR | FRONT = FRONT COPLANAR | BACK = BACK FRONT | COPLANAR = FRONT FRONT | FRONT = FRONT FRONT | BACK = SPANNING BACK | COPLANAR = BACK BACK | FRONT = SPANNING BACK | BACK = BACK """ COPLANAR = 0 FRONT = 1 BACK = 2 SPANNING = 3 # Classify each point as well as the entire polygon into one of the above # four classes. polygonType = 0 types = [] for i in range(0, len(polygon.vertices)): t = self.normal.dot(polygon.vertices[i].pos) - self.w type = -1 if t < -csgPlane.EPSILON: type = BACK elif t > csgPlane.EPSILON: type = FRONT else: type = COPLANAR polygonType |= type types.append(type) # Put the polygon in the correct list, splitting it when necessary. if polygonType == COPLANAR: if self.normal.dot(polygon.plane.normal) > 0: coplanarFront.append(polygon) else: coplanarBack.append(polygon) elif polygonType == FRONT: front.append(polygon) elif polygonType == BACK: back.append(polygon) elif polygonType == SPANNING: f = [] b = [] tvf = [] # Added for PPyCSG tvb = [] # Added for PPyCSG for i in range(0, len(polygon.vertices)): j = (i+1) % len(polygon.vertices) ti = types[i] tj = types[j] vi = polygon.vertices[i] vj = polygon.vertices[j] tvi = polygon.tverts[i] # Added for PPyCSG tvj = polygon.tverts[j] # Added for PPyCSG if ti != BACK: f.append(vi) tvf.append(tvi) # Added for PPyCSG if ti != FRONT: if ti != BACK: b.append(vi.clone()) tvb.append(tvi.clone()) # Added for PPyCSG else: b.append(vi) tvb.append(tvi) # Added for PPyCSG if (ti | tj) == SPANNING: # This would be where an in-bounds test would go t = (self.w - self.normal.dot(vi.pos)) / self.normal.dot(vj.pos.minus(vi.pos)) v = vi.interpolate(vj, t) tv = tvi.interpolate(tvj, t) # Added for PPyCSG f.append(v) b.append(v.clone()) tvf.append(tv) # Added for PPyCSG tvb.append(tv.clone()) # Added for PPyCSG #polygon.mat = "spanning" # Added for PPyCSG - testing if len(f) >= 3: front.append(csgPolygon(f, polygon.shared, polygon.mat, tvf)) # Modified for PPyCSG if len(b) >= 3: back.append(csgPolygon(b, polygon.shared, polygon.mat, tvb)) # Modified for PPyCSG class csgPolygon(object): # Modified for PPyCSG """ class Polygon Represents a convex polygon. The vertices used to initialize a polygon must be coplanar and form a convex loop. They do not have to be `Vertex` instances but they must behave similarly (duck typing can be used for customization). Each convex polygon has a `shared` property, which is shared between all polygons that are clones of each other or were split from the same polygon. This can be used to define per-polygon properties (such as surface color). """ def __init__(self, vertices, shared=None, mat="surface",tverts=[]): # Modified for PPyCSG self.vertices = vertices self.shared = shared self.plane = csgPlane.fromPoints(vertices[0].pos, vertices[1].pos, vertices[2].pos) self.mat = mat # Added for PPyCSG self.tverts = tverts # Added for PPyCSG def clone(self): vertices = map(lambda v: v.clone(), self.vertices) tverts = map(lambda v: v.clone(), self.tverts) return csgPolygon(vertices, self.shared, mat=self.mat, tverts=self.tverts) def flip(self): self.vertices.reverse() self.tverts.reverse() map(lambda v: v.flip(), self.vertices) map(lambda v: v.flip(), self.tverts) self.plane.flip() # Node class required mofication to avoid recursion limit errors in Node.build() # Conversion from recursive to iterative explained by Renderosity Poser user Bagginsbill. # Useful resources: # http://blog.moertel.com/posts/2013-05-11-recursive-to-iterative.html # http://blog.moertel.com/posts/2013-05-14-recursive-to-iterative-2.html # http://blog.moertel.com/posts/2013-06-03-recursion-to-iteration-3.html class csgNode(object): """ class Node Holds a node in a BSP tree. A BSP tree is built from a collection of polygons by picking a polygon to split along. That polygon (and all other coplanar polygons) are added directly to that node and the other polygons are added to the front and/or back subtrees. This is not a leafy BSP tree since there is no distinction between internal and leaf nodes. """ def __init__(self, polygons=None, name="root", level=0, parent=None, report=1): # Modified for PPyCSG self.name = name # Added for PPyCSG self.level = level # Added for PPyCSG self.parent = parent self.plane = None self.front = None self.back = None self.polygons = [] if polygons: self.build(polygons, report=report) # Modified for PPyCSG def clone(self): node = Node() if self.plane: node.plane = self.plane.clone() if self.front: node.front = self.front.clone() if self.back: node.back = self.back.clone() node.polygons = map(lambda p: p.clone(), self.polygons) return node def invert(self): # Modified for PPyCSG: removal of recursion """ Convert solid space to empty space and empty space to solid space. """ work = [self] while work: self = work.pop() for poly in self.polygons: poly.flip() self.plane.flip() if self.front: work.append(self.front) if self.back: work.append(self.back) temp = self.front self.front = self.back self.back = temp def clipPolygons(self, polygons): # Needs conversion from recursive to iterative """ Recursively remove all polygons in `polygons` that are inside this BSP tree. """ if not self.plane: return polygons[:] front = [] back = [] for poly in polygons: self.plane.splitPolygon(poly, front, back, front, back) if self.front: front = self.front.clipPolygons(front) if self.back: back = self.back.clipPolygons(back) else: back = [] front.extend(back) return front def clipTo(self, bsp): # Needs conversion from recursive to iterative """ Remove all polygons in this BSP tree that are inside the other BSP tree `bsp`. """ self.polygons = bsp.clipPolygons(self.polygons) if self.front: self.front.clipTo(bsp) if self.back: self.back.clipTo(bsp) def allPolygons(self): """ Return a list of all polygons in this BSP tree. """ polygons = self.polygons[:] if self.front: polygons.extend(self.front.allPolygons()) if self.back: polygons.extend(self.back.allPolygons()) return polygons def build(self, polygons, report=0): # Modified for PPyCSG: removal of recursion """ http://www.renderosity.com/mod/forumpro/showthread.php?thread_id=2879036&page=1#message_4139758 Credit to Bagginsbill for the revision. """ reps = 0 front_check = -1 back_check = -1 work = [(self,polygons)] while work: self, polygons = work.pop() if not len(polygons): return if not self.plane: self.plane = polygons[0].plane.clone() front = [] back = [] for poly in polygons: self.plane.splitPolygon(poly, self.polygons, self.polygons, front, back) if len(front): if not self.front: self.front = csgNode(name="front",level=self.level+1, parent=self) work.append((self.front,front)) if len(back): if not self.back: self.back = csgNode(name="back",level=self.level+1, parent=self) work.append((self.back,back)) if (len(front) == 0) or (len(back) == 0): front_check = len(front) back_check = len(back) if (front_check != -1) and (back_check != -1): if (len(front) == front_check) and (len(back) == back_check): reps += 1 if reps >= 1000: print self.level, reps, front_check, back_check print "Intersections not detected. Script quitting." return if report: # Added for PPyCSG - tracking #CSG.print_info(self) theGUI.status_update("node: %s_%s, front: %s, back: %s" %(self.name,self.level,len(front),len(back))) def flattened_pre_order(self): # Added for PPyCSG # http://codereview.stackexchange.com/questions/33662/returning-a-list-of-the-values-from-the-binary-tree # You use recursion to visit the child nodes, which means that traversing a very deep tree will exceed # Python's maximum recursion depth: # The way to avoid this is to maintain your own stack, like this: """ Return a list of items in the tree rooted at this node, using pre-order traversal. >>> tree = BTNode(1, BTNode(2, None, BTNode(3)), BTNode(4)) >>> tree.flattened_pre_order() [1, 2, 3, 4] >>> node = BTNode(9999) >>> for i in range(9998, -1, -1): node = BTNode(i, node) >>> node.flattened_pre_order() == list(range(10000)) True """ result = [] stack = [self] node = None while stack: if node is None: node = stack.pop() if node is not None: result.append(node) stack.append(node.front) node = node.back return result #========================================================================================= # End pycsg #========================================================================================= #====================================================================================== # Geometry-merging functions #====================================================================================== def merge_act_geoms(actors,sep_mats=0,name="merged_prop"): merge_geom = mergeGeom() matnames = [] mat_suffix = "" for ai in range(len(actors)): act = actors[ai] geom = act.Geometry() sets = [i for i in geom.Sets()] polys = [[p.Start(),p.NumVertices()] for p in geom.Polygons()] verts = [[i.X(),i.Y(),i.Z()] for i in geom.WorldVertices()] if not geom.NumTexVertices(): tverts, tsets, tpolys = apply_box_map(geom) else: tsets = [i for i in geom.TexSets()] tpolys = [[p.Start(),p.NumTexVertices()] for p in geom.TexPolygons()if p.NumTexVertices()] # Poser will add dummy listings to tpolys on a created geometry with split texverts tverts = [[i.U(),i.V()] for i in geom.TexVertices()] if sep_mats: mat_suffix = "_%s" %(ai) for m in geom.Materials(): if (len(geom.Materials()) > 1) and (m.Name() == "Preview"): continue if not ("%s%s" %(m.Name(),mat_suffix) in matnames): matnames.append("%s%s" %(m.Name(),mat_suffix)) poly_mats = ["%s%s" %(p.MaterialName(),mat_suffix) for p in geom.Polygons()] merge_geomdata(merge_geom,verts,polys,sets,tverts,tpolys,tsets,poly_mats=poly_mats,group="%s"%(ai)) name = unique_actname(name) newprop = make_prop(merge_geom,name=name) poly_mats = [] set_mats(newprop,matnames,merge_geom.mats) return newprop def merge_geomdata(geom,verts,polys,sets,tverts,tpolys,tsets,poly_mats=[],group="",): geom.merge_polys(polys,groups=[group],polymats=poly_mats) geom.merge_sets(sets) geom.merge_tpolys(tpolys) geom.merge_tsets(tsets) geom.merge_verts(verts) geom.merge_tverts(tverts) def make_prop(geom,name="merged_prop"): verts = Numeric.array(geom.verts,Numeric.Float) polys = Numeric.array(geom.polys,Numeric.Int) sets = Numeric.array(geom.sets,Numeric.Int) tverts = Numeric.array(geom.tverts,Numeric.Float) tpolys = Numeric.array(geom.tpolys,Numeric.Int) tsets = Numeric.array(geom.tsets,Numeric.Int) newgeom = poser.NewGeometry() newgeom.AddGeneralMesh(polys,sets,verts,tpolys,tsets,tverts) newprop = scene.CreatePropFromGeom(newgeom,name) return newprop def set_mats(newprop,matnames,poly_mats): newgeom = newprop.Geometry() if len(matnames) > 1: for mi in range(len(matnames)): name = matnames[mi] newgeom.AddMaterialName(name) for pi in range(len(poly_mats)): newpoly = newgeom.Polygon(pi) newpoly.SetMaterialName(poly_mats[pi]) newprop.SetGeometry(newgeom) class mergeGeom(object): def __init__(self): self.verts = [] self.polys = [] self.sets = [] self.tverts = [] self.tpolys = [] self.tsets = [] self.groups = [] self.mats = [] self.linklen = 0.0 self.centers = [] self.vertcount = 0 self.tvertcount = 0 self.setcount = 0 self.tsetcount = 0 def merge_verts(self,newverts): self.vertcount += len(newverts) for vi in range(len(newverts)): self.verts.append([i for i in newverts[vi]]) def merge_polys(self,newpolys,groups=[],mat="Preview",polymats=[]): count = self.setcount for pi in range(len(newpolys)): poly = newpolys[pi] self.polys.append([poly[0]+count,poly[1]]) self.groups.append([i for i in groups]) self.mats.append(polymats[pi]) def merge_sets(self,newsets): self.setcount += len(newsets) count = self.vertcount for si in range(len(newsets)): self.sets.append(newsets[si]+count) def merge_tverts(self,newtverts): self.tvertcount += len(newtverts) for vi in range(len(newtverts)): self.tverts.append([i for i in newtverts[vi]]) def merge_tpolys(self,newtpolys): count = self.tsetcount for pi in range(len(newtpolys)): poly = newtpolys[pi] self.tpolys.append([poly[0]+count,poly[1]]) def merge_tsets(self,newtsets): self.tsetcount += len(newtsets) count = self.tvertcount for si in range(len(newtsets)): self.tsets.append(newtsets[si]+count) def unique_actname(name): """ Generate a unique actor name to prevent Poser from re-naming the new prop. """ names = [a.Name() for a in scene.Actors()] if name in names: number = 1 while "%s_%s" %(name,number) in names: number += 1 name = "%s_%s" %(name,number) return name def apply_box_map(geom): sets = [i for i in geom.Sets()] polys = [[p.Start(),p.NumVertices()] for p in geom.Polygons()] verts = Numeric.array([[i.X(),i.Y(),i.Z()] for i in geom.Vertices()],Numeric.Float) tpolys = [[p.Start(),p.NumVertices()] for p in geom.Polygons()] tverts,tsets = box_map(verts,polys,sets,inbounds=0) return tverts, tsets, tpolys def get_axis(xdir): """Get the major axis along which a vector points""" absz = [abs(i) for i in xdir] index = absz.index(max(absz)) sign = mySign(xdir[index]) axis = [0.0,0.0,0.0] axis[index] = 1.0 * sign return axis def mySign(num): """Get the sign of a float or int""" if abs(num) == num: return 1 return -1 def box_map(verts,polys,sets,inbounds=0): """ Apply box-mapped UVs. Adapted from UV-mapping plugin for AC3D. http://www.supercoldmilk.com/ac3dplug/uvmap.html """ bounds = b_box_ock(verts) nx,mx,ny,my,nz,mz = bounds szx = mx - nx szy = my - ny szz = mz - nz tverts = [] tsets = [] for pi in range(len(polys)): poly = polys[pi] psets = sets[poly[0]:poly[0]+poly[1]] edge1 = verts[psets[1]] - verts[psets[0]] edge2 = verts[psets[0]] - verts[psets[len(psets)-1]] norm = vector_crossproduct(edge1,edge2) norm = vector_normalize(norm) axis = get_axis(norm) for vi in psets: vert = verts[vi] if axis[0]: u = (vert[2] - nz)/szz v = (vert[1] - ny)/szy u = -u if norm[0] > 0.0: u = -u elif axis[1]: u = (vert[0] - nx)/szx v = (vert[2] - nz)/szz u = -u v = -v if norm[1] < 0.0: u = -u else: u = (vert[0] - nx)/szx v = (vert[1] - ny)/szy u = -u if norm[2] < 0.0: u = -u if inbounds: u *= 0.5 v *= 0.5 u += 0.5 v += 0.5 tverts.append([u,v]) tsets.append(len(tverts)-1) return tverts,tsets #================================================================================================= # class myMesh # A container class for geometry data # Functions developed for TDMT project, 2007 # Some functions written or modified by Spanki #================================================================================================= class myMesh(object): def __init__(self,act,verts,geom=None,name=""): if act: self.act = act self.name = act.Name() self.geom = act.Geometry() else: self.act = None self.name = name self.geom = geom self.verts = verts self.polyverts(self.geom,self.geom.Polygons()) self.polyedges(self.pverts) if self.geom.TexVertices(): self.uvs = Numeric.array([[i.U(),i.V()] for i in self.geom.TexVertices()],Numeric.Float) self.polyverts(self.geom,self.geom.TexPolygons(),UV=1) self.polyedges(self.puvs,UV=1) if self.geom.NumSets() != self.geom.NumTexSets(): print "Un-mapped polygons!" tverts,tsets,tpolys = apply_box_map(self.geom) self.uvs = [[j for j in i] for i in tverts] self.puvs = [tsets[i[0]:i[0]+i[1]] for i in tpolys] else: #print "%s has no UV data. Texvertices not found. Skipping...." %(self.name) tverts,tsets,tpolys = apply_box_map(self.geom) self.uvs = [[j for j in i] for i in tverts] self.puvs = [tsets[i[0]:i[0]+i[1]] for i in tpolys] self.polynorms(self.pverts) self.vertpolys(self.geom,self.pverts) self.vertnorms_Geom() self.polymats() self.polyneighbors(self.pverts,no=0) def clear(self): self.act = None self.geom = None self.verts = [] self.polyverts = [] self.pverts = [] self.uvs = [] self.puvs = [] self.euvs = [] self.edges = [] self.pnorms = [] self.vpolys = [] self.vnorms = [] self.center = [] self.startDists = [] self.vertneighbors = [] def polyverts(self,geom,polys,UV=0): """ vertices by polygon - this is the Sets() list broken down by polygon index. Returns a list of Numeric arrays """ if UV: self.puvs = [] gset = geom.TexSets() else: self.pverts = [] gset = geom.Sets() for p in polys: if UV: l = p.NumTexVertices() else: l = p.NumVertices() s = p.Start() e = s + l vl = [v for v in gset[s:e]] if UV: self.puvs.append(vl) else: self.pverts.append(vl) def polymats(self): self.mats = [] geom = self.geom for p in geom.Polygons(): #self.mats.append(p.MaterialIndex()) self.mats.append(p.MaterialName()) def polyedges(self,pgons,UV=0): """ edges of polygons as [start vert,end vert] """ if UV: self.euvs = [[[0,0] for j in range(len(i))] for i in pgons] edges = self.euvs else: self.edges = [[[0,0] for j in range(len(i))] for i in pgons] edges = self.edges for p in range(len(pgons)): for v in range(len(pgons[p])): if v != len(pgons[p])-1: edges[p][v] = [pgons[p][v],pgons[p][v+1]] else: edges[p][v] = [pgons[p][v],pgons[p][0]] def vertpolys(self,geom,pgons,tex=0): """ polygons by vertex - each vert will have multiple listings returns a list of Numeric arrays """ if tex: self.vpolys = [[] for i in range(geom.NumTexVertices())] else: self.vpolys = [[] for i in range(geom.NumVertices())] for pvi in range(len(pgons)): for v in pgons[pvi]: if not pvi in self.vpolys[v]: self.vpolys[v].append(pvi) self.vpolys = [Numeric.array(i,Numeric.Int) for i in self.vpolys] def vertneighbors(self,pgons): """ neighbor verts of verts pgons is self.pverts or self.tverts """ self.vneighbors = [[] for i in range(len(self.vpolys))] for vi in range(len(self.vpolys)): buflist = [vi] # Need to exclude the vert itself! for ps in self.vpolys[vi]: for v in pgons[ps]: if not v in buflist: self.vneighbors[vi].append(v) buflist.append(v) def polynorms(self,pgons,verts=[]): """ [Spanki] compute normal for each polygon (don't rely on Poser supplied Normals - they're broken) returns a Numeric array """ self.pnorms = [[0.0,0.0,0.0] for i in range(len(pgons))] self.pnorms = Numeric.array(self.pnorms,Numeric.Float) if verts == []: verts = self.verts for p_i in range(len(pgons)): p = pgons[p_i] v0 = verts[p[0]] v1 = verts[p[1]] v2 = verts[p[2]] # Create 2 edge vectors to define a plane e1 = v1 - v0 e2 = v2 - v0 # Cross-product is normal to that plane vN = vector_normalize(vector_crossproduct(e1, e2)) self.pnorms[p_i] = vN def trinorms(self,pgons,verts=[]): """ [Spanki] compute normal for each polygon (don't rely on Poser supplied Normals - they're broken) returns a Numeric array """ self.tnorms = [[0.0,0.0,0.0] for i in range(len(pgons))] self.tnorms = Numeric.array(self.tnorms,Numeric.Float) if verts == []: verts = self.verts for p_i in range(len(pgons)): p = pgons[p_i] v0 = verts[p[0]] v1 = verts[p[1]] v2 = verts[p[2]] # Create 2 edge vectors to define a plane e1 = v1 - v0 e2 = v2 - v0 # Cross-product is normal to that plane vN = vector_normalize(vector_crossproduct(e1, e2)) self.tnorms[p_i] = vN def vertnorms_Geom(self,norms2=0): """ [Spanki] normals of vertices, computed as the average of the face normals of the polygons which use each vertex returns a Numeric array """ if norms2: self.raynorms = [[0.0,0.0,0.0] for i in range(len(self.vpolys))] self.raynorms = Numeric.array(self.vnorms,Numeric.Float) else: self.vnorms = [[0.0,0.0,0.0] for i in range(len(self.vpolys))] self.vnorms = Numeric.array(self.vnorms,Numeric.Float) for vi in range(len(self.vpolys)): polys = self.vpolys[vi] nx = 0; ny = 0; nz = 0 l = len(polys) if l == 0: # In some unusual geometries, there may be vertices with no polygon associations - we try to skip these. 1/26/08 #print "Stray vertex: %s, %s" %(vi,l) continue # If we don't skip these, we get a divide by zero crash error due to a bad mesh. for p in polys: nx += self.pnorms[p][0] ny += self.pnorms[p][1] nz += self.pnorms[p][2] n = [nx/l,ny/l,nz/l] n = vector_normalize(n) # Shouldn't need to do this, but it can't hurt if norms2: self.raynorms[vi] = n else: self.vnorms[vi] = n def polycenters(self,pgons,verts=[]): """ centers of polygons """ self.pcenters = [] #if not verts: if verts == []: verts = self.verts pcents = [] for pvi in range(len(pgons)): pv = pgons[pvi] v_avg = Numeric.array([verts[vi] for vi in pv],Numeric.Float) #v_avg = Numeric.average(v_avg) v_avg = oldnumeric.average(v_avg) pcents.append([i for i in v_avg]) for vi in range(self.geom.NumVertices()): v_avg = Numeric.array([pcents[pi] for pi in self.vpolys[vi]],Numeric.Float) #v_avg = Numeric.average(v_avg) v_avg = oldnumeric.average(v_avg) self.pcenters.append([i for i in v_avg]) self.pcenters = Numeric.array(self.pcenters,Numeric.Float) def vcenters(self,pgons,verts=[]): """ """ self.vcenters = [] if not verts: verts = self.verts for vi in range(self.geom.NumVertices()): pvts = pgons[vi] lng = len(pvts) for pvi in range(lng): pass def triverts_simple(self,polys): """ polygons broken into triangles, using 'fanning' method. returns: self.tverts = list of new tri-polys as Numeric array polys is a list of Geometry().Polygons() """ self.tverts = [] tv = 0 # Keep count of new tri indices for p_i in range(len(self.pverts)): p = self.pverts[p_i] for t_i in range(1,len(p)-1): # Number of tris per poly will be number of verts in poly, minus 2 self.tverts.append([p[0],p[t_i],p[t_i+1]]) # Add the new triangles tv += 1 self.tverts = Numeric.array(self.tverts,Numeric.Int) def triverts(self,pverts,matlist=[],puv=[],tracking=0): """ polygons broken into triangles. returns: self.tverts = list of new tri-polys as Numeric array matlist is a materials selection list, returned by app (or a blank list) self.tmats is materials indices by tri indices puv is self.puvverts (or a blank list) self.tuvverts is texvertices by tri """ self.tverts = [] if matlist != []: self.tmats = [] if puv != []: self.tuvverts = [] if tracking: self.p2t = [[] for i in range(len(self.pverts))] tv = 0 # Keep count of new tri indices for p_i in range(len(self.pverts)): p = pverts[p_i] for t_i in range(1,len(p)-1): # Number of tris per poly will be number of verts in poly, minus 2 self.tverts.append([p[0],p[t_i],p[t_i+1]]) # Add the new triangles if tracking: self.p2t[p_i].append(tv) if matlist != []: self.tmats.append(matlist[p_i]) if puv: self.tuvverts.append([puv[p_i][0],puv[p_i][t_i],puv[p_i][t_i+1]]) tv += 1 self.tverts = Numeric.array(self.tverts,Numeric.Int) if tracking: self.t2p = [0 for i in range(len(self.tverts))] for i in range(len(self.p2t)): for j in self.p2t[i]: self.t2p[j] = i def verttris(self,geom,pgons): """ polygons by vertex - each vert will have multiple listings returns a list of Numeric arrays """ self.vtris = [[] for i in range(geom.NumVertices())] for pvi in range(len(pgons)): for v in pgons[pvi]: if not pvi in self.vtris[v]: self.vtris[v].append(pvi) self.vtris = [Numeric.array(i,Numeric.Int) for i in self.vtris] def polyplanes(self,pgons): """ planes of polygons tries to use a single vertex to calculate the plane, unless two axes of the first vertex are at 0.0, in which case it will use the average of all three vertices to calculate the plane. returns a Numeric array [Spanki] - I flipped the order of these, so that the average is used as preference (it's less likely to have any 0.0 coordinate) """ self.pplanes = [] for pvi in range(len(pgons)): px = 0; py = 0; pz = 0 n = self.pnorms[pvi] pv = pgons[pvi] for vert in pv: vx = self.verts[vert] px += vx[0] py += vx[1] pz += vx[2] if (abs(px) < FLT_EPSILON and abs(py) < FLT_EPSILON) or\ (abs(px) < FLT_EPSILON and abs(pz) < FLT_EPSILON) or\ (abs(py) < FLT_EPSILON and abs(pz) < FLT_EPSILON): vx = self.verts[self.tverts[pvi][0]] #D = Numeric.dot(n,vx) D = n[0] * vx[0] + n[1] * vx[1] + n[2] * vx[2] else: v_avg = [px/3,py/3,pz/3] D = 0 for i in range(3): D += n[i]*v_avg[i] self.pplanes.append(D) self.pplanes = Numeric.array(self.pplanes,Numeric.Float) def triplanes(self,pgons): """ planes of polygons tries to use a single vertex to calculate the plane, unless two axes of the first vertex are at 0.0, in which case it will use the average of all three vertices to calculate the plane. returns a Numeric array [Spanki] - I flipped the order of these, so that the average is used as preference (it's less likely to have any 0.0 coordinate) """ self.tplanes = [] for pvi in range(len(pgons)): px = 0; py = 0; pz = 0 n = self.tnorms[pvi] pv = pgons[pvi] for vert in pv: vx = self.verts[vert] px += vx[0] py += vx[1] pz += vx[2] if (abs(px) < FLT_EPSILON and abs(py) < FLT_EPSILON) or\ (abs(px) < FLT_EPSILON and abs(pz) < FLT_EPSILON) or\ (abs(py) < FLT_EPSILON and abs(pz) < FLT_EPSILON): vx = self.verts[self.tverts[pvi][0]] #D = Numeric.dot(n,vx) D = n[0] * vx[0] + n[1] * vx[1] + n[2] * vx[2] else: v_avg = [px/3,py/3,pz/3] D = 0 for i in range(3): D += n[i]*v_avg[i] self.tplanes.append(D) self.tplanes = Numeric.array(self.tplanes,Numeric.Float) def polyneighbors(self,pgons,no=0,anyv=0): # Don't need tris unless we do Numeric conversions """ neighbor polys of polys tris is a boolean flag, indicating whether or not we're using triangles """ self.pneighbors = [[] for i in range(len(pgons))] if no: self.noNeighbors = [[-1 for j in i] for i in self.edges] for pi in range(len(pgons)): for e1,e2 in self.edges[pi]: for p in self.vpolys[e1]: if p != pi: for edge in range(len(self.edges[p])): e3,e4 = self.edges[p][edge] if anyv: if (e1 == e3) or (e1 == e4) or (e2 == e3) or (e2 == e4): if not p in self.pneighbors[pi]: self.pneighbors[pi].append(p) else: if (e1,e2) == (e4,e3): self.pneighbors[pi].append(p) if no: self.noNeighbors[p][edge] = 1 def vertuvs(self,geom,pgons,uvgons): """ Returns a Numeric array of vert indices to texvert indices for the geom pgons is pverts or tverts uvgons is puvverts or tuvverts Do not mix use of tris and polys! """ self.vuvverts = [[] for i in range(geom.NumVertices())] screen = [[] for i in range(geom.NumVertices())] for i in range(len(pgons)): for j in range(len(pgons[i])): if not uvgons[i][j] in screen[pgons[i][j]]: self.vuvverts[pgons[i][j]].append([uvgons[i][j],[i]]) screen[pgons[i][j]].append(uvgons[i][j]) def geom_polys(self,geom,pgons,uvgons=[]): """ Organize existing lists into Numeric array format used by PoserPython to add a new geometry UV handling requires that self.vuvverts be defined. This will not end up copying over groups Materials groups will have to be added after the prop is generated pgons is pverts or tverts uvgons is puvverts or tuvverts """ self.gpolys = [] pos = 0 for p in pgons: self.gpolys.append([pos,len(p)]) pos += len(p) self.gsets = [] for pv in pgons: for v in pv: self.gsets.append(v) # self.verts should be used for gverts self.gpolys = Numeric.array(self.gpolys,Numeric.Int) self.gsets = Numeric.array(self.gsets,Numeric.Int) if uvgons != []: self.gtpolys = [] pos = 0 for uv in uvgons: self.gtpolys.append([pos,len(uv)]) pos += len(uv) self.gtsets = [] for p in uvgons: for uv in p: self.gtsets.append(uv) self.gtverts = [[0,0] for i in range(geom.NumTexVertices())] # The PoserPython manual is incorrect: the gtverts array is UV coords, not vertex coords. # See geom.bucky.py for the only example of which I know. self.gtpolys = Numeric.array(self.gtpolys,Numeric.Int) self.gtsets = Numeric.array(self.gtsets,Numeric.Int) self.gtverts = Numeric.array(self.gtverts,Numeric.Float) def vertexPos(geom,worldspace=1,multiple=1.0): """ Pre-fetch vertex coordinates for faster access. Returns a Numeric array of vert coords by vert indices Send worldspace=0 keyword when calling function, to use localspace From TDMT Classic source code. """ numVerts = geom.NumVertices() verts = [[0.0,0.0,0.0] for i in range(numVerts)] for i in range(numVerts): if worldspace: v = geom.WorldVertex(i) else: v = geom.Vertex(i) verts[i] = [v.X(),v.Y(),v.Z()] verts = Numeric.array(verts,Numeric.Float) return verts #================================================================================================= # --- Bounding boxes, testing boxes --- # Functions developed for TDMT project, 2007 # Some functions written or modified by Spanki #================================================================================================= def in_box(bbox,verts): """Simple bounding box check""" inbox = [0 for i in range(len(verts))] for vi in range(len(verts)): vert = verts[vi] if vert.x > bbox[0] and vert.x < bbox[1]: if vert.y > bbox[2] and vert.y < bbox[3]: if vert.z > bbox[4] and vert.z < bbox[5]: inbox[vi] = 1 return inbox def b_box_ock(vertpos): """ create bounding boxes """ minX = 1000000.0; maxX = -1000000.0 minY = 1000000.0; maxY = -1000000.0 minZ = 1000000.0; maxZ = -1000000.0 for i in range(len(vertpos)): tx = vertpos[i][0] ty = vertpos[i][1] tz = vertpos[i][2] if tx < minX: minX = tx if tx > maxX: maxX = tx if ty < minY: minY = ty if ty > maxY: maxY = ty if tz < minZ: minZ = tz if tz > maxZ: maxZ = tz return [minX,maxX,minY,maxY,minZ,maxZ] def box_center(box): """ find the center of the box To use results as a cylinder: use box_center x,z with vertex y Game gems: can also do (min + max)/2 for each axis. """ minX,maxX,minY,maxY,minZ,maxZ = box cx = (abs(minX-maxX)/2)+minX cy = (abs(minY-maxY)/2)+minY cz = (abs(minZ-maxZ)/2)+minZ return vector_copy((cx,cy,cz)) def increase_box(box,increase): """ increase the bounding box size """ for i in range(6): if i%1 == 0: increase = -increase box[i] = box[i] + increase return box def testing_boxes(point,sz,name,disp,merge): """ build the testing boxes """ b = [point[0]-sz, point[0]+sz, point[1]-sz, point[1]+sz, point[2]-sz, point[2]+sz] build_box(b,name,disp,merge) def return_box_geom(b1,as_mesh=0): """ """ verts = Numeric.array([ [b1[0],b1[2],b1[4]], [b1[0],b1[3],b1[4]], [b1[0],b1[3],b1[5]], [b1[0],b1[2],b1[5]],[b1[1],b1[2],b1[4]],[b1[1],b1[3],b1[4]], [b1[1],b1[3],b1[5]],[b1[1],b1[2],b1[5]] ], Numeric.Float) if as_mesh: pverts = [[3,2,1,0],[4,5,6,7],[5,4,0,1],[6,5,1,2],[7,6,2,3],[4,7,3,0]] return verts, pverts polys = Numeric.array([[0,4],[4,4],[8,4],[12,4],[16,4],[20,4]],Numeric.Int) sets = Numeric.array([3,2,1,0,4,5,6,7,5,4,0,1,6,5,1,2,7,6,2,3,4,7,3,0],Numeric.Int) return verts, polys, sets def build_box(b1,name,display,merge,vertarray=None): """ Create a box using the Poser geometry API b1 format is [minX,maxX,minY,maxY,minZ,maxZ] if merge == 1, prop will be merged with any pre-existing prop of the same name. """ addmesh = 0 if merge: props = [a for a in scene.Actors() if a.Name() == name and a.IsProp()] if len(props) >= 1: addmesh = 1 if addmesh: newbox = props[0] bbox = newbox.Geometry() else: bbox = poser.NewGeometry() if vertarray == None: Verts = Numeric.array([ [b1[0],b1[2],b1[4]], [b1[0],b1[3],b1[4]], [b1[0],b1[3],b1[5]], [b1[0],b1[2],b1[5]],[b1[1],b1[2],b1[4]],[b1[1],b1[3],b1[4]], [b1[1],b1[3],b1[5]],[b1[1],b1[2],b1[5]] ], Numeric.Float) else: # Use vertarray Verts = vertarray Polys = Numeric.array([[0,4],[4,4],[8,4],[12,4],[16,4],[20,4]],Numeric.Int) Sets = Numeric.array([3,2,1,0,4,5,6,7,5,4,0,1,6,5,1,2,7,6,2,3,4,7,3,0],Numeric.Int) bbox.AddGeneralMesh(Polys,Sets,Verts) if addmesh: newbox.SetGeometry(bbox) newbox.MarkGeomChanged() else: newbox = scene.CreatePropFromGeom(bbox,name) if display == 1: newbox.SetDisplayStyle(poser.kDisplayCodeEDGESONLY) if display == 2: newbox.SetDisplayStyle(poser.kDisplayCodeWIREFRAME) scene.DrawAll() return newbox #================================================================================================= # Vector and matrix functions from Blender2Cal3D.py for Blender. # http://www-users.cs.umn.edu/~mein/blender/plugins/python/import_export/blend2cal3d/blender2cal3d.py # (Other versions of this script can be found online, with Google) # Poser matrices are compatible with Cal3D matrices, but not with Blender matrices. # Some functions have been altered from the original. # # (Xscale, 0.0, 0.0, 0.0), # (0.0, yScale, 0.0, 0.0), # (0.0, 0.0, zScale, 0.0), # (xTran, yTran, zTran, ) #================================================================================================= def matrix_multiplyOLD(b, a): return [ [ a[0][0] * b[0][0] + a[0][1] * b[1][0] + a[0][2] * b[2][0], a[0][0] * b[0][1] + a[0][1] * b[1][1] + a[0][2] * b[2][1], a[0][0] * b[0][2] + a[0][1] * b[1][2] + a[0][2] * b[2][2], 0.0, ], [ a[1][0] * b[0][0] + a[1][1] * b[1][0] + a[1][2] * b[2][0], a[1][0] * b[0][1] + a[1][1] * b[1][1] + a[1][2] * b[2][1], a[1][0] * b[0][2] + a[1][1] * b[1][2] + a[1][2] * b[2][2], 0.0, ], [ a[2][0] * b[0][0] + a[2][1] * b[1][0] + a[2][2] * b[2][0], a[2][0] * b[0][1] + a[2][1] * b[1][1] + a[2][2] * b[2][1], a[2][0] * b[0][2] + a[2][1] * b[1][2] + a[2][2] * b[2][2], 0.0, ], [ a[3][0] * b[0][0] + a[3][1] * b[1][0] + a[3][2] * b[2][0] + b[3][0], a[3][0] * b[0][1] + a[3][1] * b[1][1] + a[3][2] * b[2][1] + b[3][1], a[3][0] * b[0][2] + a[3][1] * b[1][2] + a[3][2] * b[2][2] + b[3][2], 1.0, ] ] def matrix_multiply(b,a): """Huge speed gains from this. Cage thought it was broken, along with LinearAlgebra.py.""" return Numeric.matrixmultiply(a,b) def matrix_invert(m): det = (m[0][0] * (m[1][1] * m[2][2] - m[2][1] * m[1][2]) - m[1][0] * (m[0][1] * m[2][2] - m[2][1] * m[0][2]) + m[2][0] * (m[0][1] * m[1][2] - m[1][1] * m[0][2])) if det == 0.0: return None det = 1.0 / det r = [ [ det * (m[1][1] * m[2][2] - m[2][1] * m[1][2]), - det * (m[0][1] * m[2][2] - m[2][1] * m[0][2]), det * (m[0][1] * m[1][2] - m[1][1] * m[0][2]), 0.0, ], [ - det * (m[1][0] * m[2][2] - m[2][0] * m[1][2]), det * (m[0][0] * m[2][2] - m[2][0] * m[0][2]), - det * (m[0][0] * m[1][2] - m[1][0] * m[0][2]), 0.0 ], [ det * (m[1][0] * m[2][1] - m[2][0] * m[1][1]), - det * (m[0][0] * m[2][1] - m[2][0] * m[0][1]), det * (m[0][0] * m[1][1] - m[1][0] * m[0][1]), 0.0, ] ] r.append([ -(m[3][0] * r[0][0] + m[3][1] * r[1][0] + m[3][2] * r[2][0]), -(m[3][0] * r[0][1] + m[3][1] * r[1][1] + m[3][2] * r[2][1]), -(m[3][0] * r[0][2] + m[3][1] * r[1][2] + m[3][2] * r[2][2]), 1.0, ]) return r def matrix_rotate(axis, angle): vx = axis[0] vy = axis[1] vz = axis[2] vx2 = vx * vx vy2 = vy * vy vz2 = vz * vz cos = math.cos(angle) sin = math.sin(angle) co1 = 1.0 - cos return [ [vx2 * co1 + cos, vx * vy * co1 + vz * sin, vz * vx * co1 - vy * sin, 0.0], [vx * vy * co1 - vz * sin, vy2 * co1 + cos, vy * vz * co1 + vx * sin, 0.0], [vz * vx * co1 + vy * sin, vy * vz * co1 - vx * sin, vz2 * co1 + cos, 0.0], [0.0, 0.0, 0.0, 1.0], ] def matrix_rotate_x(angle): cos = math.cos(angle) sin = math.sin(angle) return [ [1.0, 0.0, 0.0, 0.0], [0.0, cos, sin, 0.0], [0.0, -sin, cos, 0.0], [0.0, 0.0, 0.0, 1.0], ] def matrix_rotate_y(angle): cos = math.cos(angle) sin = math.sin(angle) return [ [cos, 0.0, -sin, 0.0], [0.0, 1.0, 0.0, 0.0], [sin, 0.0, cos, 0.0], [0.0, 0.0, 0.0, 1.0], ] def matrix_rotate_z(angle): cos = math.cos(angle) sin = math.sin(angle) return [ [ cos, sin, 0.0, 0.0], [-sin, cos, 0.0, 0.0], [ 0.0, 0.0, 1.0, 0.0], [ 0.0, 0.0, 0.0, 1.0], ] def point_by_matrixOLD(p, m): return [p[0] * m[0][0] + p[1] * m[1][0] + p[2] * m[2][0] + m[3][0], p[0] * m[0][1] + p[1] * m[1][1] + p[2] * m[2][1] + m[3][1], p[0] * m[0][2] + p[1] * m[1][2] + p[2] * m[2][2] + m[3][2]] def point_by_matrix(p,m): p = Numeric.array([p[0],p[1],p[2],1.0],Numeric.Float) m = Numeric.array([[j for j in i] for i in m],Numeric.Float) p = Numeric.matrixmultiply(p,m) return [p[0],p[1],p[2]] def matrix_translate(m, v): m[3][0] += v[0] m[3][1] += v[1] m[3][2] += v[2] return m def matrix_scale(fx, fy, fz): return [ [ fx, 0.0, 0.0, 0.0], [0.0, fy, 0.0, 0.0], [0.0, 0.0, fz, 0.0], [0.0, 0.0, 0.0, 1.0], ] def r2d(r): """Could also use math.degrees(r)""" #return round(r*180.0/math.pi,4) d = round(r*180.0/math.pi,4) if d > 360.0: d = d % 360 return d def d2r(d): """Could also use math.radians(d)""" return (d*math.pi)/180.0 def vector_normalize(v): l = Numeric.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) if l == 0.0: return Numeric.array([v[0], v[1], v[2]],Numeric.Float) # Spanki - avoid divide-by-zero (test added) return Numeric.array([v[0] / l, v[1] / l, v[2] / l],Numeric.Float) def vector_normalize2d(v): l = Numeric.sqrt(v[0] * v[0] + v[1] * v[1]) if l == 0.0: return Numeric.array([v[0], v[1]],Numeric.Float) # Spanki - avoid divide-by-zero (test added) return Numeric.array([v[0] / l, v[1] / l],Numeric.Float) def vector_crossproduct(v1, v2): return [ v1[1] * v2[2] - v1[2] * v2[1], v1[2] * v2[0] - v1[0] * v2[2], v1[0] * v2[1] - v1[1] * v2[0], ] def distance2d(p1,p2): p1 = Numeric.array(p1,Numeric.Float) p2 = Numeric.array(p2,Numeric.Float) dist = p2 - p1 dist = Numeric.sqrt( pow(dist[0],2) + pow(dist[1],2) ) return dist def distance3d(p1,p2): p1 = Numeric.array(p1,Numeric.Float) p2 = Numeric.array(p2,Numeric.Float) dist = p2 - p1 dist = Numeric.sqrt( pow(dist[0],2) + pow(dist[1],2) + pow(dist[2],2) ) return dist def vector_angle(v1, v2, r2d=0, oldtype=0): s = vector_length(v1) * vector_length(v2) f = vector_dotproduct(v1, v2) / s if s == 0: return 0.0 if f >= 1.0: return 0.0 if f <= -1.0: return math.pi / 2.0 if oldtype: if r2d: return round(math.degrees(math.acos(f))) return math.acos(f) else: if r2d: return r2d(math.atan(-f / math.sqrt(1.0 - f * f)) + math.pi / 2.0) return math.atan(-f / math.sqrt(1.0 - f * f)) + math.pi / 2.0 def vector_length(v): if len(v) == 2: return Numeric.sqrt( pow(v[0],2) + pow(v[1],2) ) elif len(v) == 3: return Numeric.sqrt( pow(v[0],2) + pow(v[1],2) + pow(v[2],2) ) def vector_dotproduct(v1,v2): if len(v1) == 2: v1 = [v1[0],v1[1],0.0] if len(v2) == 2: v2 = [v2[0],v2[1],0.0] return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2] #================================================================================================= # GUI stuff #================================================================================================= if version >= 8.0: class wxGUI(wx.ScrolledWindow): """ Python wx GUI for Poser 8+ The Tk method requires use of scene.ProcessSomeEvents(), but the parameter dials don't respond properly to this in current P8+ (it's a reported bug). """ def __init__(self, parent): wx.ScrolledWindow.__init__(self,parent=parent,id=wx.NewId(),pos=wx.Point(-100,250)) self.parent = parent self.status = [] self.cutBy_acts = [] self.toCut_acts = [] self.weldlabel = wx.StaticText(self, id=-1, label="Weld handling:", pos=(320,230)) self.weldrb_1 = wx.RadioButton(self, id=-1, label="No welding", pos=(320,250), style=wx.RB_GROUP) self.weldrb_2 = wx.RadioButton(self, id=-1, label="Split by source", pos=(320,270)) self.weldrb_3 = wx.RadioButton(self, id=-1, label="Weld all polygons", pos=(320,290)) self.weldrb_2.SetValue(1) self.boollabel = wx.StaticText(self, id=-1, label="Boolean function:", pos=(320,320)) self.boolrb_1 = wx.RadioButton(self, id=-1, label="Subtraction", pos=(320,340), style=wx.RB_GROUP) self.boolrb_2 = wx.RadioButton(self, id=-1, label="Union", pos=(320,360)) self.boolrb_3 = wx.RadioButton(self, id=-1, label="Intersection", pos=(320,380)) self.boolrb_1.SetValue(1) self.toCutLabel = wx.StaticText(self, id=-1, label="Object to be cut:", pos=(0,0)) self.toCutList = wx.ListBox(self, id=-1, pos=(0,20), size=(220,200), style=wx.LB_NEEDED_SB | wx.LB_SINGLE) self.Bind(wx.EVT_LISTBOX, self.fill_cutBy, self.toCutList) self.cutByLabel = wx.StaticText(self, id=-1, label="Object(s) by which to cut:", pos=(225,0)) self.cutByList = wx.ListBox(self, id=-1, pos=(225,20), size=(220,200), style=wx.LB_NEEDED_SB | wx.LB_MULTIPLE) self.runbutton = wx.Button(self, id=-1, label="Run Script", pos=(50,225), size=(100,20)) self.Bind(wx.EVT_BUTTON, self.runScript, self.runbutton) self.quitbutton = wx.Button(self, id=-1, label="End script", pos=(150,225), size=(100,20)) self.Bind(wx.EVT_BUTTON, self.endScript, self.quitbutton) self.statusList = wx.ListBox(self, id=-1, pos=(0,250), size=(300,145), style=wx.LB_NEEDED_SB) self.fill_lists(self.toCut_acts) self.fill_boxes(self.toCutList,self.toCut_acts) def status_update(self,line): self.status.append(line) self.statusList.Append(line) self.statusList.SetSelection(len(self.status)-1) self.Update() def fill_cutBy(self,event): sel = self.toCutList.GetSelections() if not sel: self.status_update("Could not get toCut selection.") return self.cutByList.Clear() self.cutBy_acts = [] name,fig = self.toCut_acts[sel[0]].replace(">","").split(" <") if fig != "PropActor": intname = scene.Figure(fig).Actor(name).InternalName() else: intname = scene.Actor(name).InternalName() self.fill_lists(self.cutBy_acts,notme=intname) self.fill_boxes(self.cutByList,self.cutBy_acts) def fill_lists(self,theList,notme=""): """ Fills the internal actor lists """ for act in scene.Actors(): if act.IsLight() or act.IsCamera() or act.IsDeformer() or act.IsBase() or\ act.IsZone() or (not act.Geometry()) or act.Name() == 'GROUND' or\ act.Name() == 'FocusDistanceControl' or 'CenterOfMass' in act.Name(): continue if not act.OnOff(): continue if act.InternalName() == notme: continue line = [act.Name()] if act.ItsFigure(): line.append(act.ItsFigure().Name()) else: line.append("PropActor") theList.append("%s <%s>" %(line[0],line[1])) def fill_boxes(self,listBox,actList): """ Adds the figures and props to the first set of listboxes """ listBox.Clear() for item in actList: listBox.Append(item) self.Update() def endScript(self,event): """End the script""" poser.WxAuiManager().DetachPane(self) # This is how you work a "close" button with Poser wxPython self.Close(True) def runScript(self,event): """Run button.""" if self.weldrb_1.GetValue(): weld = 0 if self.weldrb_2.GetValue(): weld = 1 if self.weldrb_3.GetValue(): weld = 2 if self.boolrb_1.GetValue(): bool_type = "subtract" if self.boolrb_2.GetValue(): bool_type = "union" if self.boolrb_3.GetValue(): bool_type = "intersect" sel = self.toCutList.GetSelections() if not sel: self.status_update("Could not get toCut selection.") return toCut = self.toCut_acts[sel[0]] sel = self.cutByList.GetSelections() if not sel: self.status_update("Could not get cutBy selection.") return cutBy = [self.cutBy_acts[i] for i in sel] self.status_update("Starting Boolean processing..") self.status_update("Please wait...") StartTime = time.time() run(toCut=toCut,cutBy=cutBy,bool_type=bool_type,weld=weld) EndTime = (time.time() - StartTime)/60 minutes = int(EndTime) seconds = int((EndTime - float(minutes))*60) self.status_update("Done in %s minutes, %s seconds." %(minutes,seconds)) else: class TkGUI(object): def __init__(self,master): self.master = master master.title("PPyCSG (Boolean)") self.weldVar = IntVar() self.weldVar.set(1) self.boolVar = IntVar() self.boolVar.set(0) # ===== Frames ===== self.masterFrame = Frame(self.master) self.masterFrame.grid(row=1,column=0) self.listMixFrame = Frame(self.masterFrame) self.listMixFrame.grid(row=0,column=0) self.listFrame = Frame(self.listMixFrame,borderwidth=2,relief=RIDGE) self.listFrame.grid(row=0,column=0) self.groupFrame = Frame(self.listFrame) self.groupFrame.grid(row=0,column=0) self.cutByFrame = Frame(self.listFrame) self.cutByFrame.grid(row=0,column=1) self.buttonFrame = Frame(self.listMixFrame,borderwidth=2,relief=RIDGE) self.buttonFrame.grid(row=1,column=0) self.previewFrame = Frame(self.buttonFrame) self.previewFrame.grid(row=0,column=0) self.statusFrame = Frame(self.buttonFrame) self.statusFrame.grid(row=2,column=0) self.runFrame = Frame(self.statusFrame) self.runFrame.grid(row=0,column=0) self.radioFrame = Frame(self.statusFrame) self.radioFrame.grid(row=1,column=1) # ===== Radio buttons ==== self.weldlabel = Label(self.radioFrame, text="Weld handling:") self.weldlabel.grid(row=0, column=0, sticky=W) self.weldrb_1 = Radiobutton(self.radioFrame, text="No welding", variable=self.weldVar, value=0) self.weldrb_1.grid(row=1, column=0, sticky=W) self.weldrb_2 = Radiobutton(self.radioFrame, text="Split by source", variable=self.weldVar, value=1) self.weldrb_2.grid(row=2, column=0, sticky=W) self.weldrb_3 = Radiobutton(self.radioFrame, text="Weld all polygons", variable=self.weldVar, value=2) self.weldrb_3.grid(row=3, column=0, sticky=W) self.boollabel = Label(self.radioFrame, text="Boolean function:") self.boollabel.grid(row=4, column=0, sticky=W) self.boolrb_1 = Radiobutton(self.radioFrame, text="Subtraction", variable=self.boolVar, value=0) self.boolrb_1.grid(row=5, column=0, sticky=W) self.boolrb_2 = Radiobutton(self.radioFrame, text="Union", variable=self.boolVar, value=1) self.boolrb_2.grid(row=6, column=0, sticky=W) self.boolrb_3 = Radiobutton(self.radioFrame, text="Intersection", variable=self.boolVar, value=2) self.boolrb_3.grid(row=7, column=0, sticky=W) # ===== Listboxes ===== self.toCutLabel = Label(self.groupFrame, text="Select toCut object" , anchor=N, justify=LEFT) self.toCutLabel.grid(row=1, column=1) self.toCutScroll = Scrollbar(self.groupFrame, orient=VERTICAL) self.toCutScroll.grid( row=2, column=0,sticky=N+S+E) self.toCutList = Listbox(self.groupFrame, height=17, width=35, selectmode=SINGLE,exportselection=0, yscrollcommand=self.toCutScroll.set) self.toCutList.grid( row=2, column=1) self.toCutScroll["command"] = self.toCutList.yview self.cutByLabel = Label(self.cutByFrame, text="Select cutBy object(s)" , anchor=N, justify=LEFT) self.cutByLabel.grid(row=1, column=1) self.cutByScroll = Scrollbar(self.cutByFrame, orient=VERTICAL) self.cutByScroll.grid( row=2, column=0,sticky=N+S+E) self.cutByList = Listbox(self.cutByFrame, height=17, width=35, selectmode=MULTIPLE,exportselection=0, yscrollcommand=self.cutByScroll.set) self.cutByList.grid( row=2, column=1) self.cutByScroll["command"] = self.cutByList.yview # ---- Mouse-button binding ---- self.toCutList.bind('',lambda e, s=self: s.fill_cutBy(e.y)) # ===== Run buttons ===== self.runButton = Button(self.runFrame,text="Run script",command=self.runScript,padx=25) self.runButton.grid(row=0,column=1) self.quitButton = Button(self.runFrame,text="End script",command=self.endScript,padx=25) self.quitButton.grid(row=0,column=2) # ===== Status box ===== self.statusList = Listbox(self.statusFrame, height=13, width=54, selectmode=SINGLE, exportselection=0) # Status bar self.statusList.grid(row=1, column=0) self.fill_boxes(self.toCutList) def status_update(self,line): """""" self.statusList.insert(END,line) self.statusList.update() self.statusList.see(END) def fill_cutBy(self,event): """ """ name,fig = self.toCutList.get(self.toCutList.curselection()[0]).replace(">","").split(" <") self.cutByList.delete(0,END) if fig != "PropActor": intname = scene.Figure(fig).Actor(name).InternalName() else: intname = scene.Actor(name).InternalName() self.fill_boxes(self.cutByList,notme=intname) def fill_boxes(self,theList,notme=""): """ Adds the figures and props to the first set of listboxes """ for act in scene.Actors(): if act.IsLight() or act.IsCamera() or act.IsDeformer() or act.IsBase() or\ act.IsZone() or (not act.Geometry()) or act.Name() == 'GROUND' or\ act.Name() == 'FocusDistanceControl' or 'CenterOfMass' in act.Name(): continue if not act.OnOff(): continue if act.InternalName() == notme: continue line = [act.Name()] if act.ItsFigure(): line.append(act.ItsFigure().Name()) else: line.append("PropActor") theList.insert(END,"%s <%s>" %(line[0],line[1])) def endScript(self): """End the script""" root.destroy() root.quit() def runScript(self): """Run button.""" weld = self.weldVar.get() booltype = self.boolVar.get() if booltype == 0: bool_type = "subtract" elif booltype == 1: bool_type = "union" elif booltype == 2: bool_type = "intersect" toCut = self.toCutList.get(self.toCutList.curselection()[0]) sel = self.cutByList.curselection() cutBy = [self.cutByList.get(i) for i in sel] self.status_update("Starting Boolean processing..") self.status_update("Please wait...") StartTime = time.time() run(toCut=toCut,cutBy=cutBy,bool_type=bool_type,weld=weld) EndTime = (time.time() - StartTime)/60 minutes = int(EndTime) seconds = int((EndTime - float(minutes))*60) self.status_update("Done in %s minutes, %s seconds." %(minutes,seconds)) if version < 8.0: root = Tk() theGUI = TkGUI(root) root.mainloop() else: theGUI = wxGUI(root) man.AddPane(theGUI, wx.aui.AuiPaneInfo(). Caption("PPyCSG (Boolean)").CaptionVisible(). Float().Resizable().DestroyOnClose().Dockable(False). FloatingSize(wx.Size(455, 430)).CloseButton(True)) info = man.GetPane(theGUI) info.Show() man.Update()