#!BPY """ Name: 'Quantities Bill' Blender: 242 Group: 'Object' Tooltip: 'Creates a quantities bill for the selected objects' """ __author__ = ["GreyBeard, Cambo, Yorik van Havre"] __url__ = ("blender", "blenderartists", "http://yorik.orgfree.com") __version__ = "0" __bpydoc__ = """\ Script made by Yorik with pieces of code by Cambo and GreyBeard Bugfixes by Hiower (correct scaling of meshes and support for no material) This scripts creates a quantities bill from the selected objects. It separates the objects by material, then computes for each object either the volume, the total area or the total length and creates a new text in the text editor, formatted in CSV format, which you can save and open with any spreadsheet application. For this script to be efficient, you need to be very careful when modelling, build proper meshes, according to the rules below. It is a bit tedious but it is the right way to work with Blender. This is mostly made for small architecture projects, where you have linear elements, areas and volumes, to obtain a base to make a quick costs estimation. Rules for calculs: - NON-MANIFOLD MESHES (Meshes that contain edges shared by more than 2 faces) are not computed (can be changed below) - WIRE MESHES (Meshes with no faces, only edges) will have the sum of their edges lengths computed and appear under LENGTH - OPEN MESHES (Meshes that contain edges that are only shared by one face), will have the sume of the areas of their faces computed and appear under AREA - CLOSED MESHES (Meshes where all edges are shared by exactly two faces) will have their volume computed an appear under VOLUME Important notes: - Only meshes will be calculated - Each mesh must have only one material, multi-material meshes won't be calculated - Meshes made of several separated parts won't have their volume calculated correctly - Apply size and rotations to all objects before using (CTRL+A) - Some options can be changed in the first lines of the script """ # ***** BEGIN GPL LICENSE BLOCK ***** # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # ***** END GPL LICENCE BLOCK ***** import Blender from Blender import NMesh, Text, Mathutils # -------------------------------------------------------------------- # CONSTANTS THAT CAN BE CHANGED UNITS = ["m","meters"] # the units you work with, ie the value of your blender unit SCALEFACTOR = 1 # if you want to multiply all valors (for ex, if you scaled down your model) COMPUTEOPENMESHES = 0 # if 0: only calculate area of open meshes, if 1: calculate volume of open meshes (warning, strange results) COMPUTEMANIFOLD = 0 # if 0: non-manifold meshes are discarded # -------------------------------------------------------------------- def triangulateNMesh(nm): # function copied from the Blending into Python Wikibook - see mediawiki.blender.org ''' Converts the meshes faces to tris, modifies the mesh in place. ''' #============================================================================# # Returns a new face that has the same properties as the origional face # # but with no verts # #============================================================================# def copyFace(face): newFace = NMesh.Face() # Copy some generic properties newFace.mode = face.mode if face.image != None: newFace.image = face.image newFace.flag = face.flag newFace.mat = face.mat newFace.smooth = face.smooth return newFace # 2 List comprehensions are a lot faster then 1 for loop. tris = [f for f in nm.faces if len(f) == 3] quads = [f for f in nm.faces if len(f) == 4] if quads: # Mesh may have no quads. has_uv = quads[0].uv has_vcol = quads[0].col for quadFace in quads: # Triangulate along the shortest edge if (quadFace.v[0].co - quadFace.v[2].co).length < (quadFace.v[1].co - quadFace.v[3].co).length: # Method 1 triA = 0,1,2 triB = 0,2,3 else: # Method 2 triA = 0,1,3 triB = 1,2,3 for tri1, tri2, tri3 in (triA, triB): newFace = copyFace(quadFace) newFace.v = [quadFace.v[tri1], quadFace.v[tri2], quadFace.v[tri3]] if has_uv: newFace.uv = [quadFace.uv[tri1], quadFace.uv[tri2], quadFace.uv[tri3]] if has_vcol: newFace.col = [quadFace.col[tri1], quadFace.col[tri2], quadFace.col[tri3]] nm.addEdge(quadFace.v[tri1], quadFace.v[tri3]) # Add an edge where the 2 tris are devided. tris.append(newFace) nm.faces = tris # -------------------------------------------------------------------- def calculateVolume(mymesh): # volume calculation triangulateNMesh(mymesh) vtot = 0 for f in mymesh.faces: fzn = f.normal[2] x1 = f.v[0].co[0] y1 = f.v[0].co[1] z1 = f.v[0].co[2] x2 = f.v[1].co[0] y2 = f.v[1].co[1] z2 = f.v[1].co[2] x3 = f.v[2].co[0] y3 = f.v[2].co[1] z3 = f.v[2].co[2] pa = 0.5*abs((x1*(y3-y2))+(x2*(y1-y3))+(x3*(y2-y1))) volume = ((z1+z2+z3)/3.0)*pa if fzn < 0: fzn = -1 elif fzn > 0: fzn = 1 else: fzn = 0 vtot = vtot + (fzn * volume) return vtot # -------------------------------------------------------------------- def calculateArea(mymesh): # area calculation triangulateNMesh(mymesh) atot = 0 for f in mymesh.faces: a = Mathutils.TriangleArea(f.v[0].co, f.v[1].co, f.v[2].co) atot = atot + a return atot # -------------------------------------------------------------------- def calculateLength(mymesh): # length calculation ltot = 0 for e in mymesh.edges: ltot = ltot + (e.v1.co-e.v2.co).length return ltot # -------------------------------------------------------------------- def manifoldType(mymesh): for e in mymesh.edges: shared=0 for f in mymesh.faces: for vf1 in f.v: if vf1 == e.v1: for vf2 in f.v: if vf2 == e.v2: shared = shared+1 if (shared > 2): print "non-manifold" return "non-manifold" if (shared < 2): print "open" return "open" print "closed" return "closed" # -------------------------------------------------------------------- def displayLine(name,value,typeValue): # display results if (typeValue == "length"): resultForm.write(name) resultForm.write(" ") resultForm.write("%.2f" % value) resultForm.write(UNITS[0]) resultForm.write(" (length)\n") elif (typeValue == "area"): resultForm.write(name) resultForm.write(" ") resultForm.write("%.2f" % value) resultForm.write(UNITS[0]) resultForm.write("² (area)\n") elif (typeValue == "volume"): resultForm.write(name) resultForm.write(" ") resultForm.write("%.2f" % value) resultForm.write(UNITS[0]) resultForm.write("³ (volume)\n") elif (typeValue == "note"): resultForm.write("\n\n--------------------------------------------------------------------\n") resultForm.write("DISCARDED MESHES:\n") for i in value: resultForm.write (i) resultForm.write ("\n") elif (typeValue == "material"): resultForm.write("\n\n") resultForm.write (value) resultForm.write("\n--------------------------------------------------------------------\n\n") # -------------------------------------------------------------------- def mtlSort(selection): # sorts objects by material sortedList={} for obj in selection: materials = obj.getData().getMaterials() if materials != []: mtl = obj.getData().getMaterials()[0].getName() else: mtl = 'No texture' found = 0 for i in sortedList: if (i == mtl): found = 1 if (found == 0): sortedList[mtl] = [] sortedList[mtl].append(obj) return sortedList # -------------------------------------------------------------------- # main selection = Blender.Object.GetSelected() discarded = [] if len(selection) == 0: print 'Error: No Objects Selected' else: print print 'Quantity Bill Script' print '--------------------' resultForm = Text.New("Quantity Bill.csv") resultForm.write("Extracted from file ") resultForm.write(Blender.Get("filename")) resultForm.write("\nNOTE: All quantities are in ") resultForm.write(UNITS[1]) resultForm.write( "\n\n") resultForm.write("ELEMENT LENGTH AREA VOLUME\n") resultForm.write("--------------------------------------------------------------------\n\n") sortedList = mtlSort(selection) for matselection in sortedList: displayLine("MATERIAL",matselection,"material") for obj in sortedList[matselection]: obtype = obj.getType() if obj.getType() != 'Mesh': # discard objects reason = 'Object ' + obj.name + ' is not a mesh, not computed.' discarded.append(reason) else: print "Processing ",obj.name, "..." mymesh = NMesh.GetRawFromObject(obj.name) # Scale the mesh (including normals) so that it can be compared to other world objects. wmatrix = obj.getMatrix() mymesh.transform(wmatrix, 1) mymeshType = manifoldType(mymesh) if (len(mymesh.getMaterials()) > 1): reason = 'Object ' + obj.name + ' has more than one material, not computed.' discarded.append(reason) elif (mymeshType == "non-manifold"): if (COMPUTEMANIFOLD == 0): reason = 'Object ' + obj.name + ' is non-manifold (some edges are shared by more than 2 faces), not computed.' discarded.append(reason) else: # compute object if len(mymesh.faces) == 0: displayLine(obj.name,calculateLength(mymesh),"length") elif (mymeshType == "closed"): displayLine(obj.name,calculateVolume(mymesh),"volume") else: if (COMPUTEOPENMESHES): displayLine(obj.name,calculateVolume(mymesh),"volume") else: displayLine(obj.name,calculateArea(mymesh),"area") if (discarded != []): displayLine("NOT COMPUTED MESHES",discarded,"note")