Create a gist now

Instantly share code, notes, and snippets.

@hdon /escher-tools.py Secret
Created Dec 5, 2014

Tools for working with "non-euclidean" game world maps in Blender
import bpy, bmesh
from mathutils import Color
def selectedFaces(bm):
return filter(lambda fa: fa.select, bm.faces)
def getEditMesh(cx):
if cx.mode == 'EDIT_MESH':
ob = cx.object
if ob and ob.type == 'MESH' and ob.select:
return bmesh.from_edit_mesh(ob.data)
return None
def getPortalMaterial(n):
matName = 'EscherPortalMaterial%03d' % n
if matName in bpy.data.materials:
return bpy.data.materials[matName]
mat = bpy.data.materials.new(matName)
c = n+1
mat.diffuse_color = Color((c&1, c&2, c&4))
mat.use_transparency = True
mat.alpha = 0.5
return mat
class OBJECT_OT_NewSpaceButton(bpy.types.Operator):
bl_idname = 'escher.create_space'
bl_label = 'New Space'
def execute(self, cx):
# Create a new mesh representing the Esher space
# Create a new PSO (Primary Space Object) containing the mesh
# Insert PSO into scene
# If an EMPTY representing an Escher Remote is active
# hide PSO
# create SSO (Secondary Space Object) and parent to the EMPTY
return {'FINISHED'}
def getMeshMaterialByMaterial(me, mat):
'''Gets the index of a material in a mesh, and returns it.
If it is not already a material of the mesh, it is added.'''
if mat.name in me.materials:
return me.materials.find(mat.name)
rval = len(me.materials)
me.materials.append(mat)
return rval
class ESCHER_OT_Portalize_Face(bpy.types.Operator):
'''Portalize selected faces'''
bl_idname = "escher.portalize_face"
bl_label = "Mesh Face: Portal++"
def execute(self, cx):
if cx.object.type != 'MESH':
raise TypeError("Selected object must be MESH")
bm = getEditMesh(cx)
if bm:
me = cx.object.data
matName = None
for fa in selectedFaces(bm):
mat = me.materials[fa.material_index]
if isPortalMaterialName(mat.name):
matName = mat.name
break
if matName:
numRemotes = 0
for ob in cx.object.children:
if objectIsRemote(ob):
numRemotes += 1
remoteIndex = (portalMaterialName2remoteIndex(matName) + 1) % numRemotes
else:
remoteIndex = 0
mat = getPortalMaterial(remoteIndex)
imat = getMeshMaterialByMaterial(me, mat)
for fa in selectedFaces(bm):
fa.material_index = imat
self.report({'INFO'}, 'Portalized face materials!')
return {'FINISHED'}
@classmethod
def poll(cls, cx):
me = getEditMesh(cx)
if me:
fa = list(selectedFaces(me))
if len(fa):
return True
return False
class ESCHER_OT_LinkRemote(bpy.types.Operator):
'''Links a remote space to this one'''
bl_idname = "escher.link_remote"
bl_label = "Link Remote"
@classmethod
def poll(cls, cx):
return cx.mode == 'OBJECT' and cx.object and cx.object.type == 'MESH'
def execute(self, cx):
pso = cx.object
# This empty object represents the remote space
eo = bpy.data.objects.new('EscherRemote', None)
cx.scene.objects.link(eo)
eo['escher_remote_space_name'] = '*none*'
eo['escher_portal_index'] = findUnusedPortalIndexInPSO(pso)
eo.parent = pso
bpy.ops.object.select_all(action='DESELECT')
eo.select = True
bpy.ops.transform.translate()
return bpy.ops.transform.translate('INVOKE_DEFAULT')
class ESCHER_OT_RealizeRemote(bpy.types.Operator):
'''Creates an object parented to the symbolic EMPTY object representing an Escher Remote.
The object created is referred to as an SSO, or "Secondary Space Object." This object is
disposable and probably won't get saved in the .blend file. The PSO, or "Primary Space
Object," is not expendable, as it has children which are important, and of course its
data is the mesh representing the geometry of the Escher Space.'''
bl_idname = 'escher.realize_remote'
bl_label = 'Realize Remote'
@classmethod
def poll(cls, cx):
return cx.mode == 'OBJECT' and cx.object and cx.object.type == 'EMPTY' and \
'escher_remote_space_name' in cx.object
def execute(self, cx):
eo = cx.object
for child in eo.children:
if child.name.startswith('EscherSSO_'):
showGraph(child)
return {'FINISHED'}
remoteSpaceName = eo['escher_remote_space_name']
sso = makeSSO(remoteSpaceName)
cx.scene.objects.link(sso)
sso.parent = eo
# TODO lock all transforms of the SSO
return {'FINISHED'}
class EscherSelectRemote(bpy.types.Operator):
"""Select a remote space to be linked"""
bl_idname = "escher.select_remote"
bl_label = "Select Remote"
bl_options = {'REGISTER', 'UNDO'}
bl_property = "enumprop"
def item_cb(self, context):
return [('*new*', 'New Space', '')] + [(c.name, c.name[10:], '') for c in self.choices]
# This has to be a bpy.props.CollectionProperty(), it can't be a Python List!!!
choices = bpy.props.CollectionProperty(type=bpy.types.PropertyGroup)
enumprop = bpy.props.EnumProperty(items=item_cb)
@classmethod
def poll(cls, cx):
return cx.mode == 'OBJECT' and cx.object and cx.object.type == 'EMPTY' and \
'escher_remote_space_name' in cx.object
def execute(self, cx):
if self.enumprop == '*new*':
self.report({'INFO'}, 'TODO: Implement NEW')
return {'FINISHED'}
remotePsoName = self.enumprop
remoteSpaceName = psoName2spaceName(remotePsoName)
removeSsosFromRemoteEmpty(cx.object, cx)
cx.object['escher_remote_space_name'] = remoteSpaceName
return {'FINISHED'}
def invoke(self, context, event):
self.choices.clear()
for ob in bpy.data.objects:
if ob.name.startswith('EscherPSO_'):
self.choices.add().name = ob.name
context.window_manager.invoke_search_popup(self)
return {'FINISHED'}
class EscherNewSpace(bpy.types.Operator):
'''Create a new Escher Space, and focus on its PSO'''
bl_idname = 'escher.new_space'
bl_label = 'New Space'
newSpaceName = bpy.props.StringProperty(name='Escher Space Name', description='Name your new space', default='Unnamed_Space')
@classmethod
def poll(cls, cx):
return cx.mode == 'OBJECT'
def execute(self, cx):
pso = makePSO(self.newSpaceName)
cx.scene.objects.link(pso)
self.report({'INFO'}, 'Created new Space, mesh, and PSO')
return {'FINISHED'}
def invoke(self, cx, ev):
return cx.window_manager.invoke_props_dialog(self)
def removeSsosFromRemoteEmpty(eo, cx):
for o in eo.children:
if isSsoName(o.name):
cx.scene.unlink(o)
def lockAllTransforms(ob):
for i in range(3):
ob.lock_location[i] = True
ob.lock_rotation[i] = True
ob.lock_scale[i] = True
def makePSO(spaceName, me=None):
if me is None:
bm = bmesh.new()
for x in -1, 1:
for y in -1, 1:
for z in -1, 1:
bm.verts.new().co = (x, y, z)
bm.verts.index_update()
bm.faces.new((bm.verts[0], bm.verts[2], bm.verts[3], bm.verts[1]))
bm.faces.new((bm.verts[4], bm.verts[5], bm.verts[7], bm.verts[6]))
bm.faces.new((bm.verts[0], bm.verts[4], bm.verts[6], bm.verts[2]))
bm.faces.new((bm.verts[1], bm.verts[3], bm.verts[7], bm.verts[5]))
bm.faces.new((bm.verts[0], bm.verts[1], bm.verts[5], bm.verts[4]))
bm.faces.new((bm.verts[2], bm.verts[6], bm.verts[7], bm.verts[3]))
me = bpy.data.meshes.new(spaceName2meshName(spaceName))
# Assign default material (makes life easier)
me.materials.append(bpy.data.materials['Material'])
bm.to_mesh(me)
pso = bpy.data.objects.new(spaceName2psoName(spaceName), me)
pso.show_transparent = True
lockAllTransforms(pso)
return pso
def cloneGraph(o, sc):
r = o.copy()
sc.objects.link(r)
for c in o.children:
d = cloneGraph(c, sc)
d.parent = r
return r
def graphWalk(o, fn):
fn(o)
for c in o.children:
graphWalk(c, fn)
def makeSso_copy(o, cx):
'''This function exists for makeSSO'''
# TODO
def makeSSO(spaceName):
psoName = spaceName2psoName(spaceName)
if psoName not in bpy.data.objects:
raise KeyError('Could not find "%s"' % psoName)
ssoName = spaceName2ssoName(spaceName)
sso = bpy.data.objects.new(ssoName, bpy.data.objects[psoName].data)
sso.show_transparent = True
return sso
def findUnusedPortalIndexInPSO(PSO):
'''Each Remote Object has a property assigning it to a particular
remote index, which corresponds to a particular EscherPortalMaterial.
This function is used to find which EscherPortalMaterial should be
assigned to a new Remote Object given the PSO it is intended for.'''
mats = list()
for ob in PSO.children:
if ob.type == 'EMPTY' and 'escher_remote_space_name' in ob:
if 'escher_portal_index' not in ob:
raise KeyError("Escher Remote Object should have an 'escher_portal_index' property!")
n = ob['escher_portal_index']
if type(n) is not int:
raise TypeError("Escher Remote Object property 'escher_portal_index' is not an int!")
if n in mats:
raise KeyError("Found duplicate 'escher_portal_index' property among PSO's Remote Objects!")
mats.append(n)
n = 0
mats.sort()
for mat in mats:
if n != mat:
return n
n += 1
return n
def portalMaterialName2remoteIndex(matName):
if not isPortalMaterialName(matName):
raise ValueError('Invalid Escher Portal Material name')
s = matName[20:]
if len(s):
return int(s)
return 0
def objectIsRemote(o):
return o.type == 'EMPTY' and o.name.startswith('EscherRemote')
def isPortalMaterialName(matName):
return matName.startswith('EscherPortalMaterial')
def isUnqualifiedSpaceName(spaceName):
return not (spaceName.startswith('EscherPSO_') or spaceName.startswith('EscherSSO_') or spaceName.startswith('EscherSM_'))
def toUnqualifiedSpaceName(spaceName):
if isUnqualifiedSpaceName(spaceName):
return spaceName
return spaceName[spaceName.find('_')+1:]
def spaceName2meshName(spaceName):
if isUnqualifiedSpaceName(spaceName):
return 'EscherSM_' + spaceName
raise ValueError('Invalid space name!')
def spaceName2psoName(spaceName):
if isUnqualifiedSpaceName(spaceName):
return 'EscherPSO_' + spaceName
raise ValueError('Invalid space name!')
def spaceName2ssoName(spaceName):
if isUnqualifiedSpaceName(spaceName):
return 'EscherSSO_' + spaceName
raise ValueError('Invalid space name!')
def psoName2spaceName(psoName):
if psoName.startswith('EscherPSO_'):
return psoName[10:]
else:
raise ValueError('Invalid Escher PSO name string')
class EscherFocusSpace(bpy.types.Operator):
'''Focus on a space by hiding all other spaces and displaying one space's PSO'''
bl_idname = 'escher.focus_space'
bl_label = 'Focus Space'
bl_property = "enumprop"
def item_cb(self, cx):
rval = [(c.name, c.name[10:], '') for c in self.choices]
if cx.object:
rval = [('*current*', 'Currently Selected Object', '')] + rval
return rval
# This has to be a bpy.props.CollectionProperty(), it can't be a Python List!!!
choices = bpy.props.CollectionProperty(type=bpy.types.PropertyGroup)
enumprop = bpy.props.EnumProperty(items=item_cb)
@classmethod
def poll(cls, cx):
return cx.mode == 'OBJECT'
def execute(self, cx):
if self.enumprop == '*current*':
psoName = spaceName2psoName(toUnqualifiedSpaceName(cx.object.name))
else:
psoName = self.enumprop
for ob in cx.scene.objects:
if ob.type == 'MESH':
hideGraph(ob)
pso = bpy.data.objects[psoName]
showGraph(pso)
return {'FINISHED'}
def invoke(self, context, event):
self.choices.clear()
for ob in bpy.data.objects:
if ob.name.startswith('EscherPSO_'):
self.choices.add().name = ob.name
context.window_manager.invoke_search_popup(self)
return {'FINISHED'}
def selectObject(o):
o.select = True
class EscherDeepCopy(bpy.types.Operator):
'''Copies a portion of the scene graph'''
bl_idname = 'escher.deep_copy'
bl_label = 'Deep Copy'
@classmethod
def poll(cls, cx):
return cx.mode == 'OBJECT' and cx.object
def execute(self, cx):
o = cloneGraph(cx.object, cx.scene)
bpy.ops.object.select_all(action='DESELECT')
graphWalk(o, selectObject)
return {'FINISHED'}
class EscherDeselectPortalFaces(bpy.types.Operator):
'''Deselects portal faces'''
bl_idname = 'escher.deselect_portals'
bl_label = 'Deslect Portal Faces'
@classmethod
def poll(cls, cx):
return cx.mode == 'EDIT_MESH' and cx.object.type == 'MESH'
def execute(self, cx):
me = cx.object.data
em = getEditMesh(cx)
for f in em.faces:
if isPortalMaterialName(me.materials[f.material_index].name):
f.select = False
return {'FINISHED'}
def showGraph(ob):
hideGraph(ob, False)
def hideGraph(ob, hide=True):
ob.hide = hide
for o in ob.children:
hideGraph(o, hide)
class NPanel(bpy.types.Panel):
bl_label = 'Escher Tools'
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
def draw(self, cx):
layout = self.layout
layout.operator('escher.portalize_face')
layout.operator('escher.link_remote')
layout.operator('escher.realize_remote')
layout.operator('mesh.flip_normals', text='Flip Normals')
layout.operator('escher.select_remote')
layout.operator('escher.new_space')
layout.operator('escher.focus_space')
layout.operator('escher.deep_copy')
layout.operator('escher.deselect_portals')
def register():
#bpy.types.Object.escher_space_name = bpy.props.EnumProperty(items=escher_space_names)
bpy.types.Object
bpy.utils.register_module(__name__)
def unregister():
#del bpy.types.Object.escher_space_name
bpy.utils.unregister_module(__name__)
if __name__ == '__main__':
register()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment