Page MenuHome

ScapeGoat.py

ScapeGoat.py

'''
Created on May 7, 2012
@author: Rhett Jackson
'''
bl_info = {
"name":"ScapeGoat",
"author": "Rhett Jackson",
"version": (0,7,0),
"blender": (2,6,4),
"location": "Properties>Object",
"category": "Object",
"description": "Places linked duplicates of models at the locations of faces which are chosen by the user",
"wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Objects/ScapeGoat",
"tracker_url":""
}
import bpy
from bpy.utils import register_module, unregister_module
from mathutils import Vector, Matrix
from bpy.props import StringProperty, BoolProperty, IntProperty, FloatProperty, EnumProperty
import random
import math
bpy.types.Material.sgAlignBuildingNormals = BoolProperty(
name="sgAlignBuildingNormals",
description="Aligns building normals to the normal of the face from which they are spawned",
default=False)
bpy.types.Material.oneBuildingPerFace = BoolProperty(
name="oneBuildingPerFace",
description="Places a single building on each face in a mesh",
default=True)
bpy.types.Material.sgScaleType = EnumProperty(name="sg_scale_type",
items=[('NONE', "None", "Do not restrict object scale", 0),
('MANUAL', "Manual", "Set Scale Manually", 1),
('LOCAL', "Local", "Apply scale tolerance to object scale", 2),
('MESH', "Mesh", "Apply scale tolerance to the mesh's average face size", 3)],
default="MANUAL"
)
bpy.types.Material.sgGroupScale = FloatProperty(
name="sgGroupScale",
description="Manually set scale for placed objects",
default = 1.0,
soft_min = 0.5,
min = 0.1
)
bpy.types.Material.sgScaleConformType = EnumProperty(name="sg_scale_conform_type",
items=[('HIDE', "Hide", "Do not show an object which is outside the scale tolerance", 0),
('CONFORM', "Conform", "Conform an object's size to the nearest tolerance limit (only if tolerance is exceeded)", 1),
('DELETE', "Delete", "Delete an object if it will be outside scale tolerance", 2)]
)
bpy.types.Material.sgUseScale = BoolProperty(
name="sgUseScale",
description="Uses the relative scale of the mesh",
default=True)
bpy.types.Material.sgRandomSeed = IntProperty(
name="sgRandomSeed",
description="Random number seed",
default=1)
bpy.types.Object.sgScale = FloatProperty(
name="sgScale",
description="A unified scale for the object",
default = -1.0
)
#Each edge is "adjacent" to a half of a street. In this way when two faces share and edge, each face will account for half of a street.
bpy.types.Object.scapeGoatStreetSize = FloatProperty(
name="Street Size",
description="The spacing of objects between faces",
min = 0,
default = .1)
bpy.types.Object.sgGlobalScaleF = FloatProperty(
name="Global Scale Factor",
description="A ratio between building base area and a map-object's median face size (roughly)",
default = 1)
bpy.types.Material.sgAvgFaceArea = FloatProperty(
name="sgFaceArea",
description="Average face area for a material group",
default=1.0
)
bpy.types.Material.sgScaleTol=FloatProperty(
name="sgScaleTolerance",
description="Scale Tolerance",
default=0.05)
bpy.types.Material.sgScaleBasis=FloatProperty(
name="sgScalebasis",
description="Scale Basis",
default=1.0)
bpy.types.Material.cityGroupName=StringProperty(
name="sgcityGroupName",
description="Building Group Name referenced by a ScapeGoat material",
default="")
bpy.types.MaterialSlot.sgSlotCityGroupName=StringProperty(
name="sgSlotCityGroupName",
description="Building Group Name",
default="")
bpy.types.MaterialSlot.sgMedianFaceArea=FloatProperty(
name="sgMedianFaceArea",
description="Material Slot's Median face area after removing street size",
default=0)
class ClearBuildings (bpy.types.Operator):
bl_idname = "object.clearbuildings"
bl_label = "CityScape Clear"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Clears all objects created by ScapeGoat for the selected object."
sgMaterialName = StringProperty()
sgGroupParentName = StringProperty()
def execute(self, context):
print("self is named" + self.name)
mapObject = context.object
self.sgGroupParentName = 'sg' + mapObject.name + self.sgMaterialName + 'Parent'
if self.sgGroupParentName in bpy.data.objects:
clearSGBuildings(mapObject, self.sgGroupParentName)
return{'FINISHED'}
class ScapeGoat (bpy.types.Operator):
bl_idname = "object.scapegoat"
bl_label = "CityScape"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Places building objects to make a cityscape."
sgMaterialName = StringProperty()
sgGroupParentName = StringProperty()
whatChanged = StringProperty()
interactiveFlag = BoolProperty()
alignToNormals = BoolProperty()
onePerFace = BoolProperty()
randomSeed = IntProperty()
sgGroupScale = FloatProperty(
name="sgGroupScale",
description="Manually set scale for placed objects",
default = 1.0,
soft_min = 0.5,
min = 0.1
)
sgStreetSize = FloatProperty(
name="Street Size",
description="The spacing of objects between faces",
min = 0,
default = .1)
sgScaleType = EnumProperty(name="sg_scale_type",
items=[('NONE', "None", "Do not restrict object scale", 0),
('MANUAL', "Manual", "Set Scale Manually", 1),
('LOCAL', "Local", "Apply scale tolerance to object scale", 2),
('MESH', "Mesh", "Apply scale tolerance to the mesh's average face size", 3)]
)
sgScaleBasis = FloatProperty()
sgScaleTol = FloatProperty(
min = 0.0)
sgScaleConform = EnumProperty(name="sg_scale_conform",
items=[('HIDE', "Hide", "Do not show an object which is outside the scale tolerance", 0),
('CONFORM', "Conform", "Conform an object's size to the nearest tolerance limit (only if tolerance is exceeded)", 1),
('DELETE', "Delete", "Delete an object if it will be outside scale tolerance", 2)]
)
def draw(self, context):
theMaterial = bpy.data.materials[self.sgMaterialName]
layout = self.layout
row = layout.row()
row.prop(self, 'randomSeed', text="Seed")
box = layout.box()
row = box.row()
row.prop(self, 'alignToNormals', text = "Align to normals")
row = box.row()
row.prop(self, 'onePerFace', text = "One per face")
box = layout.box()
row = box.row()
row.label("Scale Type")
row.prop_menu_enum(self, 'sgScaleType', text=self.sgScaleType)
if self.sgScaleType == "MANUAL":
col = box.column()
col.prop(self, 'sgGroupScale', text="Scale")
elif self.sgScaleType == "LOCAL":
col = box.column()
subrow = col.row()
subrow.label(text="Conformity")
subrow.prop(self, 'sgScaleConform', expand=True)
col.prop(self, 'sgScaleBasis', text="Scale Basis")
col.prop(self, 'sgScaleTol', text="Scale Tolerance")
elif self.sgScaleType == "MESH":
col = box.column()
subrow = col.row()
subrow.label(text="Conformity")
subrow.prop(self, 'sgScaleConform', expand=True)
col.prop(self, 'sgScaleTol', text="Scale Tolerance")
box = layout.box()
box.prop(self,'sgStreetSize', text = "Street Size")
def buildCityGroup(self, obj, options):
#alignNormal = options[1]
materialName = options[2]
onePerFace = options[3]
sgStreetSize = options[5]
if bpy.context.mode != 'OBJECT':
#toggle into Object mode
#bpy.ops.object.editmode_toggle()
set_mode('OBJECT')
#Check if the created-buildings group exists
#Remove existing buildings and their parent.
if self.sgGroupParentName in bpy.data.objects:
clearSGBuildings(obj, self.sgGroupParentName)
#if not 'sg' + obj.name + groupName + 'Parent' in bpy.data.objects:
#add Building Group parent
bpy.ops.object.add(type="EMPTY", location=obj.location)
bpy.context.active_object.name=self.sgGroupParentName
print("***Start Scanning Faces***")
#Rewrite this so that ActiveMaterials are not added if there are no faces with the material applied
'''
activeMaterials = [] #A list of materials referenced by the ScapeGoat panel which are applied to at least one face
allMaterials = [] #A list of materials being referenced by the ScapeGoat panel
for aMaterialSlot in obj.material_slots: #Look at the object's material slots
if aMaterialSlot.material.cityGroupName != "": #As long as a cityGroupName is in place
allMaterials.append(aMaterialSlot.material) #Add the material to the list
'''
theMat = bpy.data.materials[materialName]
faceLists = [] #A list of matList objects. Each list is a group of faces which will receive building objects
faceGroup = getFacesWithMaterial(obj, theMat)
faceArea = sgMedianFaceArea(faceGroup) #faceArea can be the global average face size
#theMat.sgAvgFaceArea = faceArea - 2*obj.scapeGoatStreetSize*(math.sqrt(faceArea) - obj.scapeGoatStreetSize)-obj.scapeGoatStreetSize*obj.scapeGoatStreetSize
theMat.sgAvgFaceArea = areaSquareWithBorder(faceArea,sgStreetSize)
if theMat.sgAvgFaceArea < 0:
theMat.sgAvgFaceArea = 0
if theMat.oneBuildingPerFace:
sgGlobalScaleFTest = obj.sgGlobalScaleF = math.sqrt(faceArea)
#for face in obj.data.polygons:
for face in faceGroup:
cityMatGroup = obj.material_slots[face.material_index].material.cityGroupName
if cityMatGroup != "":
if onePerFace:
buildFace(obj,face, cityMatGroup, theMat, options)
else:
buildEdges(obj,face,cityMatGroup, options)
#Account for map-object rotation
groupParent = selectObject(bpy.data.objects[self.sgGroupParentName])
groupParent.rotation_euler = obj.rotation_euler
def execute(self, context):
#if some of the decision-making code from buildCity() were done here (like determining 1-per-face) the whole program could be made more modular and interactive-friendly
mapObject = context.object
random.seed(self.randomSeed)
options = [
self.whatChanged, #0 the property that has changed
self.alignToNormals, #1 Boolean. True aligns object's 'up' axis with the face's normal. False aligns it with Global Z
self.sgMaterialName, #2 String of the material's name
self.onePerFace, #3 Boolean. True places one object on each face. False walks the edges placing many
self.sgGroupScale, #4 Float. Manually-set scale for individual objects
self.sgStreetSize, #5 Float. Amount of empty space left between a placed object and its parent edge
self.sgScaleType, #6 String. Method for determining the size of placed objects
self.sgScaleBasis, #7 Float. Base scale factor for LOCAL scale restriction
self.sgScaleTol, #8 Float. Scale Tolerance
self.sgScaleConform #9 Enum. Behaviour for out-of-range scale values
]
#self.whatChanged = self.findWhatChagnged(options, self.prevOptions)
self.sgGroupParentName = 'sg' + mapObject.name + self.sgMaterialName + 'Parent'
if self.sgGroupParentName in bpy.data.objects:
sgParent = bpy.data.objects[self.sgGroupParentName]
sgBuildings = getAllChildren(sgParent)
if False:
#This would be for 1-per-face
placeEmpties()
populateEmpties()
scaleEmpties()
self.buildCityGroup(mapObject, options)
#buildCity(context.object)
#Restore selection of the map object
bpy.ops.object.select_all(action='DESELECT')
bpy.context.scene.objects.active=mapObject
mapObject.select=True
self.interactiveFlag = True
return {'FINISHED'}
def invoke (self, context, event):
obj = context.object
self.interactiveFlag = False
self.alignToNormals = bpy.data.materials[self.sgMaterialName].sgAlignBuildingNormals
self.onePerFace = bpy.data.materials[self.sgMaterialName].oneBuildingPerFace
self.whatChanged = "NULL"
self.randomSeed = bpy.data.materials[self.sgMaterialName].sgRandomSeed
self.sgGroupScale = bpy.data.materials[self.sgMaterialName].sgGroupScale
self.sgStreetSize = obj.scapeGoatStreetSize
self.sgScaleType = bpy.data.materials[self.sgMaterialName].sgScaleType
self.sgScaleBasis = bpy.data.materials[self.sgMaterialName].sgScaleBasis
self.sgScaleTol = bpy.data.materials[self.sgMaterialName].sgScaleTol
self.sgScaleConform = bpy.data.materials[self.sgMaterialName].sgScaleConformType
self.execute(context)
return{'FINISHED'}
def addRotation(obj1, obj2):
#Rotates obj1 by the amount obj2 is rotated
obj1.rotation_euler.x = obj1.rotation_euler.x + obj2.rotation_euler.x
obj1.rotation_euler.y = obj1.rotation_euler.y + obj2.rotation_euler.y
obj1.rotation_euler.z = obj1.rotation_euler.z + obj2.rotation_euler.z
def alignObjToFace(obj,face):
# print("Align to Normals")
tempRotMode = obj.rotation_mode
obj.rotation_mode = 'QUATERNION'
obj.rotation_quaternion = face.normal.to_track_quat('Z', 'Y')
obj.rotation_mode = tempRotMode
def placeEmpties():
#This function should create nulls, set their location and rotation and scale them
#the Function should have a "scale" flag so that the placement code can be skipped if only scale values have been altered.
return True
def populateEmpties():
#take a list of nulls and assign dupli_groups
return True
def scaleEmpties():
#take a list of empties and apply scaling rules
return True
def buildFace(obj,face, cityMatGroup, theMat, options):
alignNormal = options[1]
streetSize = options[5]
sgScaleType = options[6]
sgScaleBasis = options[7]
sgScaleTol = options[8]
sgScaleConform = options[9]
orientationMatrix = Matrix.Identity(3)
orVec0 = Vector((0,0,0))
orVec1 = Vector((0,0,0))
orVec2 = Vector((0,0,0))
debug = False
#print("********Build Face")
#get the face's center location in world coordinates
faceCenter = getFaceCenter(obj, face) + obj.location
theTemplate = chooseObjectFromGroup(bpy.data.groups[cityMatGroup],[])
bpy.ops.object.select_all(action='DESELECT')
#Add the new object's empty
theDupe = placeObject(theTemplate,faceCenter, obj.name+'Building', parentName='sg' + obj.name + theMat.name + 'Parent')
'''
#Align object to Face Normal
if alignNormal:
tempRotMode = theDupe.rotation_mode
theDupe.rotation_mode = 'QUATERNION'
theDupe.rotation_quaternion = face.normal.to_track_quat('Z', 'Y')
theDupe.rotation_mode = tempRotMode
'''
#Align object rotation to orientation of edges
vertList = []
vectorList = []
vertIndex = 0
for vert in face.vertices:
vertList.append(obj.data.vertices[vert])
vertIndex += 1
for vertIndex in range(0,len(vertList)):
vectorList.append(vectorFromVerts(vertList[vertIndex], vertList[(vertIndex+1)%len(vertList)],0))
vectorList[vertIndex].normalize()
if debug: print("Vector", vertIndex," =", vectorList[vertIndex])
#get an alignment vector for the building
alignVec = findAlignVector(vectorList)
orVec0 = alignVec.copy()
'''
#Make Y-Axis Vector
theVec = Vector((0,1,0))
#Rotate it to match the object's current Y-axis
tempRotMode = theDupe.rotation_mode
theDupe.rotation_mode = 'XYZ'
theVec.rotate(theDupe.rotation_euler)
if debug:
print("Dupe Rotation Mode", theDupe.rotation_mode)
print("Dupe Rotation", theDupe.rotation_euler)
print("Y-axis Vector", theVec)
theDupe.rotation_mode = tempRotMode
'''
#Cross it with the edge vector
#theCross = theVec.cross(alignVec)
theCross = face.normal.cross(alignVec)
orVec1 = theCross.copy()
#Dot the cross product with the face normal to get the direction of the angle
'''
if theCross.dot(face.normal) > 0:
if debug: print("Cross and normal are aligned")
alignAngle = theVec.angle(alignVec)
else:
if debug: print("Cross and normal are opposed")
alignAngle = -theVec.angle(alignVec)
if debug: print("Alignment Angle =", alignAngle)
'''
if alignNormal:
rotAxis=face.normal
else:
rotAxis=Vector((0,0,1))
orVec2 = rotAxis.copy()
orientationMatrix = getOrientationMatrix(orVec0, orVec1, orVec2, alignNormal)
orEuler = orientationMatrix.to_euler()
theDupe.rotation_euler = orEuler
#bpy.ops.transform.rotate(value=alignAngle, axis=rotAxis, constraint_orientation="LOCAL")
#Rotate the object for variety
bpy.ops.transform.rotate(value=(1.5708*random.randint(0,3)), axis=rotAxis, constraint_orientation="LOCAL")
#Choose a dupli_group
theDupe.dupli_type = "GROUP"
theDupe.dupli_group = theTemplate
#Scale object to face size
#Pick the smallest edge length of the face
smallestEdgeLength = getSmallestFaceEdge(obj, face)
groupDimensions = getGroupDimensions(bpy.data.groups[theDupe.dupli_group.name])
if groupDimensions[0] > groupDimensions[1]:
largeDimension = groupDimensions[0]
else:
largeDimension = groupDimensions[1]
#REsizeValue is a scale factor appropriate to the size of the face
#resizeValue = (smallestEdgeLength - obj.scapeGoatStreetSize)/largeDimension
upperLimit = resizeValue = (smallestEdgeLength - streetSize)/largeDimension #this is the ideal size
#scaleTol = obj.material_slots[face.material_index].material.sgScaleTol
#tolvalue is a scale factor appropriate to the mesh's median face area. It is a ratio of the root of a face's area to a building's base area.
#theFaceArea=obj.material_slots[face.material_index].material.sgAvgFaceArea
tolValue = math.sqrt(obj.material_slots[face.material_index].material.sgAvgFaceArea)/math.sqrt((groupDimensions[0]*groupDimensions[1]))
if debug:
print("resizeValue", resizeValue)
print("toleranceValue", tolValue)
#Calculate global scale value
#if obj.sgScale == -1.0:
obj.sgScale = tolValue
#Calculate some limits for scaling
maxTolB = sgScaleBasis * (1+sgScaleTol)
minTolB = sgScaleBasis * (1-sgScaleTol)
if sgScaleType == "LOCAL": #FIX THIS! DOES NOT WORK AS EXPECTED. CONFROMS TO LARGER SCALE, NOT FACE SIZE.
if sgScaleConform == 'HIDE':
if resizeValue > maxTolB:
theDupe.dupli_type="NONE"
elif resizeValue < minTolB:
theDupe.dupli_type="NONE"
elif sgScaleConform == 'CONFORM':
if sgScaleBasis < upperLimit:
if maxTolB > upperLimit:
resizeValue = upperLimit
else:
resizeValue = maxTolB
else:
resizeValue = upperLimit
else:
if maxTolB < upperLimit or minTolB > upperLimit:
bpy.ops.object.delete(use_global=False)
#return
elif sgScaleType == "MESH":
if sgScaleType != "NONE":
if resizeValue > tolValue*(1+sgScaleTol) or resizeValue < tolValue*(1-sgScaleTol):
print("Conform type is", sgScaleConform)
if sgScaleConform == 'HIDE':
theDupe.dupli_type="NONE"
elif sgScaleConform == 'CONFORM':
if resizeValue > tolValue*(1+sgScaleTol): #resize is larger so using the smaller value should be OK
resizeValue = tolValue*(1+sgScaleTol)
print("ResizeValue too large")
else: #resize is smaller so usit at the upper limit
resizeValue = tolValue*(1-sgScaleTol)
print("ResizeValue too small")
else:
bpy.ops.object.delete(use_global=False)
return
bpy.ops.transform.resize(value=(resizeValue,resizeValue,resizeValue))
def buildEdges(obj,face,cityMatGroup, options):
debug = False
print("***********************")
print("Convert after this")
theMaterial = obj.material_slots[face.material_index].material
alignToNormalFlag = options[1]
sgGroupScale = options[4]
sgScaleType = options[6]
#get a list of edges in the face
edgesList = edgesListFromFace(obj,face)
perimeterRemain = facePerimeter(obj,edgesList[0])
print("Starting Perimeter", perimeterRemain)
perimeterUsed = 0
#streetSize = obj.scapeGoatStreetSize
streetSize = options[5]
initialStreetSize = streetSize
edgeIndex = 0
edgeOffset = 0
firstEdgeOffset = 0
newLocation = [0,0,0]
firstRun = True
if sgScaleType == 'MESH':
#Get mesh-wide scale factor
if obj.sgScale == -1:
scaleF = 1
else:
scaleF = obj.sgScale
elif sgScaleType == 'MANUAL':
scaleF = sgGroupScale
else:
scaleF = 1
#While perimRemain > 0
#for each edge:
for edge in edgesList[0]:
debug = False
if debug: print("***Now adding edge #", edge.index)
edgeIndex = edgeIndex % len(edgesList[0])
reverseFlag = edgesList[1][edgeIndex]
#edge = edgesList[0][edgeIndex]
nextEdge = edgesList[0][(edgeIndex + 1) % len(edgesList[0])]
prevEdge = edgesList[0][(edgeIndex - 1) % len(edgesList[0])]
edgeLength = getEdgeLengthFromVerts(obj.data.vertices[edge.vertices[0]],obj.data.vertices[edge.vertices[1]])
edgeVectors = createEdgeVectors(obj, edge, edgesList, edgeIndex, face)
edgeVec = edgeVectors[0]
nextEdgeVec = edgeVectors[1]
prevEdgeVec = edgeVectors[2]
theCross = edgeVectors[3] #Perpendicular to the edge, pointing towards the face center
vecToFace = edgeVectors[4]
'''
lastEdgeVec = edgeVectors[0]
'''
#Create global alignment vectors
theXVec = Vector((1,0,0))
theYVec = Vector((0,1,0))
theZVec = Vector((0,0,1))
if firstRun:
#Subtract a full streetSize because a half width must be accounted for at each end of the edge.
edgeRemain = edgeLength - edgeOffset - streetSize
else:
#Subtract a half streetSize because one half width is accounted for by the offset
edgeRemain = edgeLength - edgeOffset - streetSize*0.5
if debug:
print("Edge length =", edgeLength)
print("Edge offset =", edgeOffset)
debug = False
#choose the template object
groupLength = len(bpy.data.groups[cityMatGroup].objects)
#get an edge vector and normalize it
#new Location = vert0/1.location
#edgeVec = vectorFromVerts(obj.data.vertices[edge.vertices[0]],obj.data.vertices[edge.vertices[1]],reverseFlag)
#edgeVec.normalize()
edgeVecFlat = vectorFromVerts(obj.data.vertices[edge.vertices[0]],obj.data.vertices[edge.vertices[1]],reverseFlag)
edgeVecFlat.normalize()
#nextEdgeVec = vectorFromVerts(obj.data.vertices[nextEdge.vertices[0]],obj.data.vertices[nextEdge.vertices[1]],edgesList[1][(edgeIndex+1) % len(edgesList[0])])
#nextEdgeVec.normalize()
#prevEdgeVec = vectorFromVerts(obj.data.vertices[prevEdge.vertices[0]],obj.data.vertices[prevEdge.vertices[1]],edgesList[1][(edgeIndex+1) % len(edgesList[0])])
#prevEdgeVec.normalize()
if firstRun:
initialStreetSize = streetSize*math.sin(prevEdgeVec.angle(edgeVec))*0.5
if reverseFlag:
newLocation = obj.location + obj.data.vertices[edge.vertices[1]].co + (edgeOffset + initialStreetSize)*edgeVec
#print("Reverse offset")
else:
newLocation = obj.location + obj.data.vertices[edge.vertices[0]].co + (edgeOffset + initialStreetSize)*edgeVec
#print("Forward offset")
initialStreetSize = 0
#vecToFace = vectorFromLocs(obj.data.vertices[edge.vertices[0]].co,getFaceCenter(obj,face))
#create temporary empty to get alignment angle
if alignToNormalFlag:
bpy.ops.object.add(type='EMPTY')
theTemp = bpy.context.active_object
alignObjToFace(theTemp,face)
#theXVec will line up with the Dupe's X-axis and theYVec with its Y-axis
theXVec.rotate(theTemp.rotation_euler)
theYVec.rotate(theTemp.rotation_euler)
bpy.ops.object.delete(use_global=False)
#The cross product of the edge and the face normal is perpendicular to the edge and in the face plane
'''
if alignToNormalFlag:
theCross = edgeVec.cross(face.normal)
else:
theCross = edgeVecFlat.cross(theZVec)
if theCross.dot(vecToFace) < 0:
#theCross points the wrong way
theCross = theCross * -1
'''
alignCross = theCross.copy()
if not alignToNormalFlag:
alignCross[2] = 0 #Flatten the vector so we get the right alignment angle.
if face.normal.dot(theCross.cross(theYVec)) > 0:
alignAngle = -theYVec.angle(alignCross)
else:
alignAngle = theYVec.angle(alignCross)
if alignToNormalFlag:
rotAxis=face.normal
else:
rotAxis=Vector((0,0,1))
#reset building counter for each edge
buildingCount = 0
if edgeIndex == (len(edgesList[0]) - 1):
edgeRemain -= firstEdgeOffset
#while edgeREmain > 0
#For each sub-edge:
while (edgeRemain > 0):
#Choose the edge's first building template
buildingNumber = random.randint(1, groupLength)-1
theTemplate = bpy.data.groups[cityMatGroup].objects[buildingNumber].dupli_group
#Get scaled building dimensions
bldgDim = getGroupDimensions(bpy.data.groups[theTemplate.name])*scaleF
#Building increment is half a building's width
bldgIncrement = buildingPlacementIncrement(bldgDim[0]*0.5, edgeVec, alignToNormalFlag)
#Can this be combined with the translation later on?
newLocation += bldgIncrement
if debug:
print("Ready to place a building")
print("building width", bldgDim[0])
print("firstEdgeOffset=", firstEdgeOffset)
print("Edge remaining=", edgeRemain)
#place & align building
bldgName = "%s edge %d-%d" % (obj.name, edge.index, buildingCount)
theDupe = placeObject(theTemplate,newLocation, bldgName, parentName='sg' + obj.name + theMaterial.name + 'Parent', scale=scaleF)
debug = False
if debug: print("created", theDupe.name, "at", newLocation)
if alignToNormalFlag:
alignObjToFace(theDupe,face)
#rotate object to align to the edge. Can these be combined with null's creation?
bpy.ops.transform.rotate(value=(alignAngle), axis=rotAxis, constraint_orientation="LOCAL")
#Translate the obj by half its depth so it lines up with the edge
#Could this be combined with the initial placement?
bpy.ops.transform.translate(value=(((bldgDim[1] + streetSize)/2)*theCross.normalized()))
#if debug: print("Moved", theDupe.name, "to", theDupe.location)
newLocation += bldgIncrement
#Delete depth of first building from the perimeter
if firstRun:
#get last edge vector
lastEdge = edgesList[0][len(edgesList[0])-1]
lastEdgeVec = vectorFromVerts(obj.data.vertices[lastEdge.vertices[0]],obj.data.vertices[lastEdge.vertices[1]],edgesList[1][len(edgesList[0])-1])
lastEdgeVec.normalize()
#find edge offset
A = bldgDim[1]*theCross.dot(-1*lastEdgeVec)
#if debug: print("A=",A)
firstEdgeOffset = math.fabs(A)
if debug: print("First Edge Offset",firstEdgeOffset)
perimeterRemain = perimeterRemain - firstEdgeOffset
#edgeRemain = edgeRemain - firstEdgeOffset
if debug:print("Perimeter after first building", perimeterRemain)
firstRun = False
edgeRemain -= 2*bldgIncrement.length
if debug:
print("Edge length =", edgeLength)
print("building width", bldgDim[0])
print("building depth", bldgDim[1])
print("Edge remaining after placement=", edgeRemain)
if edgeRemain < bldgDim[0]:
if debug:
print("Another", theTemplate.name, "will not fit")
print("it is", bldgDim[0], "wide and there is", edgeRemain, "left")
#Make the offset for the next edge
#The offset is
if reverseFlag:
joint = obj.data.vertices[edge.vertices[0]]
else:
joint = obj.data.vertices[edge.vertices[1]]
edgeOffset = newEdgeOffsetAmount(theDupe.location, joint.co, bldgDim[0], bldgDim[1], edge, reverseFlag, edgeRemain, edgeVec, nextEdgeVec, theCross, alignToNormalFlag)
#we're done with this edge so subtract its remainder from the remaining perimeter
print("End of edge #", edge.index)
#print("Perimeter Remaining", perimeterRemain)
#print("subtract edge Length", edgeLength)
perimeterRemain = perimeterRemain - getEdgeLengthFromVerts(obj.data.vertices[edge.vertices[0]],obj.data.vertices[edge.vertices[1]])
perimeterUsed = perimeterUsed +edgeRemain +edgeOffset
#print("Leaves perimeter", perimeterRemain)
#edgeRemain = 0
edgeRemain = 0
else:
edgeOffset = 0
buildingCount += 1
#print("Remaining Perimeter", perimeterRemain)
#if perimRemain < buildingWidth
if perimeterRemain < bldgDim[0]:
#perimRemain = 0
perimeterRemain = 0
edgeIndex += 1
print("But before this")
debug = False
def setParentObject(obj, parent):
obj.parent_type = 'OBJECT'
obj.parent = parent
obj.location = obj.location - parent.location
#Include rotation of child around parent?
def chooseObjectFromGroup(objGroup,used):
if not used:
theObj = bpy.data.groups[objGroup.name].objects[random.randint(1, len(objGroup.objects))-1].dupli_group
return theObj
def sgMeanFaceArea(obj):
totalArea = 0
for face in obj.data.polygons:
totalArea += face.area
averageArea = totalArea/len(obj.data.polygons)
return averageArea
def sgMedianFaceArea(faceList):
areaList = []
for face in faceList:
areaList.append(face.area)
medianArea = sorted(areaList)[int(len(faceList)/2)]
return medianArea
def areaSquareWithBorder(bigArea, borderWidth):
return bigArea - 2*borderWidth*math.sqrt(bigArea) - borderWidth*borderWidth
def flatVecScale(vector):
flat = vector.copy()
flat[2] = 0
return vector.length/flat.length
def newEdgeOffsetAmount(currentLoc, jointLoc, width, depth, edge, reverseFlag, edgeRemain, edgeVec, nextEdgeVec, theCross, align):
'''
Creates a vector (cornerVeec) from the vertex at the end of the current edge to
the corner of the last-placed object which is farthest from the start of the current edge.
The cosine of the angle between cornerVec and the next edges direction is multiplied
by the length of cornerVec to obtain the offset.
Note that one half of a streetSize is accounted for because currentLoc already includes it.
'''
debug = False
corner = currentLoc + 0.5*width*edgeVec.normalized() + 0.5*depth*theCross.normalized()
cornerVec = vectorFromLocs(jointLoc, corner)
if not align:
edgeVecFlat = edgeVec.copy()
edgeVecFlat[2] = 0
cornerVecFlat = cornerVec.copy()
cornerVecFlat[2] = 0
nextEdgeVecFlat = nextEdgeVec.copy()
nextEdgeVecFlat[2] = 0
#Make the offset for the next edge
if not align:
offset = cornerVecFlat.length*math.cos(cornerVecFlat.angle(nextEdgeVecFlat))
offset = offset*nextEdgeVec.length/nextEdgeVecFlat.length
else:
offset = cornerVec.length*math.cos(cornerVec.angle(nextEdgeVec))
if debug:
print("****")
print("CurrentLoc", currentLoc)
print("Corner Loc", corner)
print("Joint loc:", jointLoc)
print("offset", offset)
debug = False
return offset
def edgeOffsetAmount(amount, nextEdgeVec, align):
increment = amount
if not align:
adjustment = flatVecScale(nextEdgeVec)
increment = increment*adjustment
return increment
def buildingPlacementIncrement(width, edgeVec, align):
increment = width*edgeVec
if not align:
adjustment = flatVecScale(edgeVec)
increment = increment*adjustment
return increment
def clearSGBuildings(obj, parentName):
myob = bpy.context.active_object
bpy.ops.object.select_all(action='DESELECT')
#bpy.context.scene.objects.active=bpy.data.objects['sg' + obj.name + 'Parent']
#if parentName in bpy.data.objects:
bpy.data.objects[parentName].select=True
bpy.ops.object.select_hierarchy(direction='CHILD', extend=True)
bpy.ops.object.delete()
bpy.context.scene.objects.active=myob
obj.select=True
return True
def createEdgeVectors(obj, edge, edgesList, edgeIndex, face):
edgeVectors = []
nextEdge = edgesList[0][(edgeIndex + 1) % len(edgesList[0])]
prevEdge = edgesList[0][(edgeIndex - 1) % len(edgesList[0])]
reverseFlag = edgesList[1][edgeIndex]
edgeVec = vectorFromVerts(obj.data.vertices[edge.vertices[0]],obj.data.vertices[edge.vertices[1]],reverseFlag)
edgeVec.normalize()
nextEdgeVec = vectorFromVerts(obj.data.vertices[nextEdge.vertices[0]],obj.data.vertices[nextEdge.vertices[1]],edgesList[1][(edgeIndex+1) % len(edgesList[0])])
nextEdgeVec.normalize()
prevEdgeVec = vectorFromVerts(obj.data.vertices[prevEdge.vertices[0]],obj.data.vertices[prevEdge.vertices[1]],edgesList[1][(edgeIndex+1) % len(edgesList[0])])
prevEdgeVec.normalize()
vecToFace = vectorFromLocs(obj.data.vertices[edge.vertices[0]].co,getFaceCenter(obj,face))
theCross = edgeVec.cross(face.normal)
if theCross.dot(vecToFace) < 0:
#theCross points the wrong way
theCross = theCross * -1
edgeVectors.append(edgeVec) #0
edgeVectors.append(nextEdgeVec) #1
edgeVectors.append(prevEdgeVec) #2
edgeVectors.append(theCross) #3
edgeVectors.append(vecToFace) #4
return edgeVectors
def edgesListFromFace(obj,face):
#Returns a list of two lists. The first is a list of edges, the second is a list of reversal flags
edgesList = []
#find an edge in the mesh that uses two verts in face
for edge in obj.data.edges:
vert0 = edge.vertices[0]
vert1 = edge.vertices[1]
#if vert0 is in face
if vert0 in face.vertices and vert1 in face.vertices:
edgesList.append(edge)
#Check to see if we've found all the edges
if (len(edgesList) == len(face.vertices)):
break
#Organize edges
sortedList = []
sortedList.append([0]*len(edgesList))
sortedList.append([0]*len(edgesList))
currentVert = edgesList[0].vertices[0]
for i in range (0,len(edgesList)):
#get an edge with currentVert in it
for edge in edgesList:
if (currentVert in edge.vertices) and not (edge in sortedList[0]):
#add current edge to the new list
sortedList[0][i] =edge
if edge.vertices[0] == currentVert:
currentVert =edge.vertices[1]
sortedList[1][i] = False
else:
currentVert = edge.vertices[0]
#the current edge is reversed
sortedList[1][i] = True
break
return sortedList
def facePerimeter(obj,edgesList):
perimeter = 0
for edge in edgesList:
edgeLength = getEdgeLengthFromVerts(obj.data.vertices[edge.vertices[0]],obj.data.vertices[edge.vertices[1]])
perimeter += edgeLength
return perimeter
def findAlignVector(vectorList):
debug = False
vectorFound = False
for vectorIndex in range(0, len(vectorList)):
vec1 = vectorList[vectorIndex]
vec2 = vectorList[(vectorIndex+1)%len(vectorList)]
vec3 = vectorList[(vectorIndex+2)%len(vectorList)]
if math.fabs(vec1.dot(vec3)) > 0.99:
alignVec = vec1
vectorFound = True
if debug: print("Parallel alignVec =", alignVec)
break
elif math.fabs(vec1.dot(vec2)) < 0.1:
alignVec = vec1
vectorFound = True
if debug: print("Orthogonal alignVec =", alignVec)
elif not vectorFound:
alignVec = vec1
return alignVec
def getAllChildren(obj):
#Returns a list of all children of obj
children = []
bpy.ops.object.select_all(action='DESELECT')
obj.select=True
bpy.ops.object.select_hierarchy(direction='CHILD', extend=False)
for child in bpy.context.selected_objects:
children.append(child)
return children
def getEdgeCenter(obj, anEdge, segmentNum, totalSegments, reverseDir):
if reverseDir:
vert1=obj.data.vertices[anEdge.vertices[0]]
vert0=obj.data.vertices[anEdge.vertices[1]]
else:
vert0=obj.data.vertices[anEdge.vertices[0]]
vert1=obj.data.vertices[anEdge.vertices[1]]
segmentIncrement = (vert1.co-vert0.co)/totalSegments
point0 = vert0.co+(segmentNum-1)*segmentIncrement
point1 = vert0.co+(segmentNum)*segmentIncrement
return Vector(((point0[0]+point1[0])/2+obj.location.x,(point0[1]+point1[1])/2+obj.location.y,(point0[2]+point1[2])/2+obj.location.z))
def getEdgeLength(obj, edge):
vert1 = obj.data.vertices[edge.vertices[0]]
vert2 = obj.data.vertices[edge.vertices[1]]
return math.sqrt(pow(vert1.co.x-vert2.co.x,2) + pow(vert1.co.y-vert2.co.y,2) + pow(vert1.co.z-vert2.co.z,2))
def getEdgeLengthFromVerts(vert1, vert2):
return math.sqrt(pow(vert1.co.x-vert2.co.x,2) + pow(vert1.co.y-vert2.co.y,2) + pow(vert1.co.z-vert2.co.z,2))
def getFaceBoundingBox(obj, aFace):
tempBBox = [[0,0,0], #Min X Min Y Min Z
[0,0,0], #Min X Min Y Max Z
[0,0,0], #Min X Max Y Min Z
[0,0,0], #Min X Max Y Max Z
[0,0,0], #Max X Min Y Min Z
[0,0,0], #Max X Min Y Max Z
[0,0,0], #Max X Max Y Min Z
[0,0,0]] #Max X Max Y Max Z
firstRun = True
#For each object in the group
for vert in aFace.vertices:
#Get its most negative X
if obj.data.vertices[vert].co.x < tempBBox[0][0] or firstRun:
tempBBox[0][0] = tempBBox[1][0] =tempBBox[2][0] =tempBBox[3][0] = obj.data.vertices[vert].co.x
#Get its most negative Y
if obj.data.vertices[vert].co.y < tempBBox[0][1] or firstRun:
tempBBox[0][1] = tempBBox[1][1] =tempBBox[4][1] =tempBBox[5][1] = obj.data.vertices[vert].co.y
#Get its most negative Z
if obj.data.vertices[vert].co.z < tempBBox[0][2] or firstRun:
tempBBox[0][2] = tempBBox[2][2] =tempBBox[4][2] =tempBBox[6][2] = obj.data.vertices[vert].co.z
#Get its most positive X
if obj.data.vertices[vert].co.x > tempBBox[4][0] or firstRun:
tempBBox[4][0] = tempBBox[5][0] =tempBBox[6][0] =tempBBox[7][0] = obj.data.vertices[vert].co.x
#Get its most positive Y
if obj.data.vertices[vert].co.y > tempBBox[2][1] or firstRun:
tempBBox[2][1] = tempBBox[3][1] =tempBBox[6][1] =tempBBox[7][1] = obj.data.vertices[vert].co.y
#Get its most positive Z
if obj.data.vertices[vert].co.z > tempBBox[1][2] or firstRun:
tempBBox[1][2] = tempBBox[3][2] =tempBBox[5][2] =tempBBox[7][2] = obj.data.vertices[vert].co.z
firstRun = False
return tempBBox
def getFaceCenter(obj, face):
bBox = getFaceBoundingBox(obj, face)
faceCenter = Vector((0,0,0))
faceCenter[0] = (bBox[7][0] + bBox[0][0])/2
faceCenter[1] = (bBox[7][1] + bBox[0][1])/2
faceCenter[2] = (bBox[7][2] + bBox[0][2])/2
return faceCenter
def getFacesWithMaterial(obj, theMat):
matList = [] #A list of faces with theMat applied
for face in obj.data.polygons:
if obj.material_slots[face.material_index].material == theMat:
matList.append(face)
return matList
def getGroupBoundingBox(aGroup):
tempBBox = [[0,0,0],
[0,0,0],
[0,0,0],
[0,0,0],
[0,0,0],
[0,0,0],
[0,0,0],
[0,0,0]]
#For each object in the group
for obj in aGroup.objects:
#Get its most negative X
if obj.bound_box[0][0] + obj.location.x < tempBBox[0][0] or tempBBox[0][0] == 0:
tempBBox[0][0] = tempBBox[1][0] =tempBBox[2][0] =tempBBox[3][0] = obj.bound_box[0][0] + obj.location.x
#Get its most negative Y
if obj.bound_box[0][1] + obj.location.y < tempBBox[0][1] or tempBBox[0][1] == 0:
tempBBox[0][1] = tempBBox[1][1] =tempBBox[4][1] =tempBBox[5][1] = obj.bound_box[0][1] + obj.location.y
#Get its most negative Z
if obj.bound_box[0][2] + obj.location.z < tempBBox[0][2] or tempBBox[0][2] == 0:
tempBBox[0][2] = tempBBox[3][2] =tempBBox[4][2] =tempBBox[7][2] = obj.bound_box[0][2] + obj.location.z
#Get its most positive X
if obj.bound_box[4][0] + obj.location.x > tempBBox[4][0] or tempBBox[4][0] == 0:
tempBBox[4][0] = tempBBox[5][0] =tempBBox[6][0] =tempBBox[7][0] = obj.bound_box[4][0] + obj.location.x
#Get its most positive Y
if obj.bound_box[2][1] + obj.location.y > tempBBox[2][1] or tempBBox[2][1] == 0:
tempBBox[2][1] = tempBBox[3][1] =tempBBox[6][1] =tempBBox[7][1] = obj.bound_box[2][1] + obj.location.y
#Get its most positive Z
if obj.bound_box[1][2] + obj.location.z > tempBBox[1][2] or tempBBox[1][2] == 0:
tempBBox[1][2] = tempBBox[2][2] =tempBBox[5][2] =tempBBox[6][2] = obj.bound_box[1][2] + obj.location.z
return tempBBox
def getGroupDimensions(aGroup):
groupBox = getGroupBoundingBox(aGroup)
dimension = Vector((0,0,0))
dimension[0] = groupBox [4][0] - groupBox[0][0]
dimension[1] = groupBox [2][1] - groupBox[0][1]
dimension[2] = groupBox [1][2] - groupBox[0][2]
return dimension
def getNewGroupMember(newGroup, oldGroup):
#remove each old object from the new list until the dupe is found
#probably depricated
for theNew in newGroup:
for theOld in oldGroup:
if theOld.name == theNew.name:
oldMatchedNew = True
break
else:
oldMatchedNew = False
if not oldMatchedNew:
theDupe = theNew
break
return theDupe
def getOrientationMatrix(xvec, yvec, zvec, alignNormal):
if not alignNormal:
xvec[2] = 0
yvec[2] = 0
zvec = Vector((0,0,1))
mat = Matrix.Identity(3)
mat.col[0] = xvec.normalized()
mat.col[1] = yvec.normalized()
mat.col[2] = zvec.normalized()
return mat
def getSmallestFaceEdge(obj, face):
debug = False
smallest = 0
sideLength = []
edgeLength = [0]*len(face.vertices)
edgeVector = [[0,0,0]]*len(face.vertices)
if debug: print("Init edge vectors", edgeVector)
if debug: print("***************")
numberVerts = len(face.vertices)
for vertNum in range(0, numberVerts):
currentVert = face.vertices[vertNum]
nextVert = face.vertices[(vertNum+1) % (numberVerts)]
edgeLength[vertNum] = getEdgeLengthFromVerts(obj.data.vertices[currentVert], obj.data.vertices[nextVert])
edgeVector[vertNum] = vectorFromVerts(obj.data.vertices[currentVert], obj.data.vertices[nextVert],0).normalized()
if debug: print(edgeVector[vertNum])
if vertNum == 0:
#smallest = edgeLength[vertNum]
sideLength.append(edgeLength[vertNum])
elif edgeVector[vertNum].dot(edgeVector[vertNum-1]) > 0.6:
if debug:
print(edgeVector[vertNum],"dot", edgeVector[vertNum-1],"=", edgeVector[vertNum].dot(edgeVector[vertNum-1]))
print("Adding edges", edgeLength[vertNum], "and", edgeLength[vertNum-1])
sideLength[len(sideLength)-1] = sideLength[len(sideLength)-1] + edgeLength[vertNum]
edgeLength[vertNum] = edgeLength[vertNum] + edgeLength[vertNum-1]
else:
sideLength.append(edgeLength[vertNum])
vertNum += 1
sideNum = 0
for side in sideLength:
if sideNum == 0:
smallest = side
elif side < smallest:
smallest = side
sideNum += 1
if debug: print("The smallest edge is:", smallest)
return smallest
def placeObject(template,location, name, parentName="", scale=1.0):
bpy.ops.object.add(type='EMPTY', location=location)
theDupe = bpy.context.active_object
theDupe.dupli_type = "GROUP"
theDupe.dupli_group = template
theDupe.name = name
if parentName != "":
setParentObject(theDupe,bpy.data.objects[parentName])
theDupe.scale = Vector((scale,scale,scale))
#bpy.ops.object.parent_clear(type='CLEAR_INVERSE')
return theDupe
def selectObject(obj):
bpy.ops.object.select_all(action='DESELECT')
obj.select=True
bpy.context.scene.objects.active=obj
return obj
def set_mode(mode):
if bpy.context.mode != mode:
bpy.ops.object.mode_set(mode=mode)
def vectorFromVerts(vert0, vert1, reverse):
if reverse:
return Vector((vert0.co.x - vert1.co.x, vert0.co.y - vert1.co.y, vert0.co.z - vert1.co.z))
else:
return Vector((vert1.co.x - vert0.co.x, vert1.co.y - vert0.co.y, vert1.co.z - vert0.co.z))
def vectorFromLocs(loc0, loc1):
return Vector((loc1[0] - loc0[0], loc1[1] - loc0[1], loc1[2] - loc0[2]))
class ScapeGoatPanel(bpy.types.Panel):
bl_idname = "VIEW3D_PT_ScapeGoat"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_label = "ScapeGoat 0.7"
bpy.types.Material.cityGroupName = bpy.props.StringProperty(name="")
def draw(self, context):
obj = context.object
if obj.type =="MESH":
layout = self.layout
layout.label("Material and Object Groups")
for theMaterial in bpy.context.object.material_slots:
box = layout.box()
# print(theMaterial.name)
#col = layout.column()
box.prop_search(theMaterial.material, 'cityGroupName', bpy.data, "groups", text = theMaterial.name)
if theMaterial.material.cityGroupName:
split=box.split(0.5)
col=split.column()
col.prop(theMaterial.material, 'sgAlignBuildingNormals', text = "Align to normals")
col.prop(theMaterial.material, 'oneBuildingPerFace', text = "One Object per Face")
col.prop(theMaterial.material, 'sgRandomSeed', text = "Seed")
col=split.column()
col.label("Restrict Scale")
subrow = col.row(align=True)
#subrow.operator("sg_scale.button", text="None").theString="0 " + theMaterial.material.name
subrow.prop_menu_enum(theMaterial.material, 'sgScaleType', text=theMaterial.material.sgScaleType)
if theMaterial.material.sgScaleType != 'NONE':
if theMaterial.material.sgScaleType == 'MANUAL':
col.prop(theMaterial.material, 'sgGroupScale', text="Scale")
elif theMaterial.material.sgScaleType == 'LOCAL':
col.prop(theMaterial.material, 'sgScaleBasis', text="Scale Basis")
col.prop(theMaterial.material, 'sgScaleTol', text = "Tolerance")
if theMaterial.material.oneBuildingPerFace:
col.label("Tolerance Behavior")
subrow = col.row(align=True)
subrow.prop(theMaterial.material, 'sgScaleConformType', expand=True)
else:
col.prop(theMaterial.material, 'sgScaleTol', text = "Tolerance")
if theMaterial.material.oneBuildingPerFace:
col.label("Tolerance Behavior")
subrow = col.row(align=True)
subrow.prop(theMaterial.material, 'sgScaleConformType', expand=True)
subrow = box.row()
subrow.operator("object.scapegoat", text="Add Objects").sgMaterialName = theMaterial.name
subrow.operator("object.clearbuildings", text="Clear Objects").sgMaterialName = theMaterial.name
row = layout.row()
#row.prop(obj, 'scapeGoatStreetSize', text = "Street Size")
layout.prop(obj, 'scapeGoatStreetSize')
def register():
unregister_module(__name__)
register_module(__name__)
def unregister():
unregister_module(__name__)
if __name__ == '__main__':
register()

File Metadata

Mime Type
text/x-python
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
32/3c/ddf0ed5b450769d8470f9fe45949

Event Timeline