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