Dev:Py/Scripts/Cookbook/Code snippets/Multi-File packages

提供: wiki
移動先: 案内検索

Multi-file packages

Packages are a way of structuring Python's module namespace by using "dotted module names". For example, the module name A.B designates a submodule named B in a package named A. Just like the use of modules saves the authors of different modules from having to worry about each other's global variable names, the use of dotted module names saves the authors of multi-module packages from having to worry about each other's module names. For more information about Python packages, please refer to the Python documentation at Packages

Every package must contain file called __init__.py. This file is required to make Python treat the directory as containing a package; this is done to prevent directories with a common name, such as string, from unintentionally hiding valid modules that occur later on the module search path. In the simplest case, __init__.py can just be an empty file, but it can also execute initialization code for the package. In Blender, __init__.py often contains the user interface and the add-ons information, whereas the real work is done in other files.

Unlike the other scripts in this book, a multi-file package can not be executed from the text editor. It must be copied to a location which is included in Blender's search path, e.g. the addons or addons-contrib, cf. section Blender add-ons. Fortunately, you don't have to restart Blender to reload the files after each modification. Hitting F8 on the keyboard reloads all activated add-ons into Blender.

A simple example

This package is spread on four files. Three of these create meshes; a cube, a cylinder, and a sphere, respectively. These files are stand-alone scripts which can be executed in the text editor window for debug purposes. The condition (__name__ == "__main__") is true when the file is run in stand-alone mode.

mycube.py

#----------------------------------------------------------
# File mycube.py
#----------------------------------------------------------
import bpy

def makeMesh(z):
    bpy.ops.mesh.primitive_cube_add(location=(0,0,z))
    return bpy.context.object

if __name__ == "__main__":
    ob = makeMesh(1)
    print(ob, "created")

mycylinder.py

#----------------------------------------------------------
# File mycylinder.py
#----------------------------------------------------------
import bpy

def makeMesh(z):
    bpy.ops.mesh.primitive_cylinder_add(location=(0,0,z))
    return bpy.context.object

if __name__ == "__main__":
    ob = makeMesh(5)
    print(ob, "created")

mysphere.py

#----------------------------------------------------------
# File mysphere.py
#----------------------------------------------------------
import bpy

def makeMesh(z):
    bpy.ops.mesh.primitive_ico_sphere_add(location=(0,0,z))
    return bpy.context.object

if __name__ == "__main__":
    ob = makeMesh(3)
    print(ob, "created")

__init__.py

The fourth file contains the bl_info dictionary and registration needed for add-ons, and the user interface. It also contains the following piece of code to import the other files in the package.

Blender3D FreeTip.png
Note about imports
The examples here rely on using the imp python library, that should be phased out in the future. For the new alternative, please consult this page.
# To support reload properly, try to access a package var, 
# if it's there, reload everything
if "bpy" in locals():
    import imp
    imp.reload(mycube)
    imp.reload(mysphere)
    imp.reload(mycylinder)
    print("Reloaded multifiles")
else:
    from . import mycube, mysphere, mycylinder
    print("Imported multifiles")

This code works as follows.

  • If __init__.py() is run for the first time, i.e. on starting Blender with the addon enabled in your default.blend, "bpy" in locals() is False. The other files in the package are imported, and Imported multifiles is printed in the terminal.
  • If __init__.py() is run for the first time after starting Blender with the addon disabled in your default.blend, and you click enable addon, "bpy" in locals() is False. The other files in the package are imported, and Imported multifiles is printed in the terminal.
  • Once the add-on is enabled, anytime you press F8 to reload add-ons, "bpy" in locals() is True. The other files in the package are reloaded, and Reloaded multifiles is printed in the terminal.
#----------------------------------------------------------
# File __init__.py
#----------------------------------------------------------

#    Addon info
bl_info = {
    'name': 'Multifile',
    'author': 'Thomas Larsson',
    'location': 'View3D > UI panel > Add meshes',
    'category': '3D View'
    }

# To support reload properly, try to access a package var, 
# if it's there, reload everything
if "bpy" in locals():
    import imp
    imp.reload(mycube)
    imp.reload(mysphere)
    imp.reload(mycylinder)
    print("Reloaded multifiles")
else:
    from . import mycube, mysphere, mycylinder
    print("Imported multifiles")

import bpy
from bpy.props import *

#
#   class AddMeshPanel(bpy.types.Panel):
#
class AddMeshPanel(bpy.types.Panel):
    bl_label = "Add meshes"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"

    def draw(self, context):
        self.layout.operator("multifile.add", 
            text="Add cube").mesh = "cube"
        self.layout.operator("multifile.add", 
            text="Add cylinder").mesh = "cylinder"
        self.layout.operator("multifile.add", 
            text="Add sphere").mesh = "sphere"

#
#   class OBJECT_OT_AddButton(bpy.types.Operator):
#
class OBJECT_OT_AddButton(bpy.types.Operator):
    bl_idname = "multifile.add"
    bl_label = "Add"
    mesh = bpy.props.StringProperty()

    def execute(self, context):
        if self.mesh == "cube":
            mycube.makeMesh(-8)
        elif self.mesh == "cylinder":
            mycylinder.makeMesh(-5)
        elif self.mesh == "sphere":
            mysphere.makeMesh(-2)
        return{'FINISHED'}    

#
#    Registration
#

def register():
    bpy.utils.register_module(__name__)

def unregister():
    bpy.utils.unregister_module(__name__)

if __name__ == "__main__":
    register()

Simple obj importer and exporter

The obj format is commonly used for exchanging mesh data between different applications. Originally invented for Wavefront Maya, it has become the industry standard. It is a simple ascii format which contain lines of the following form:

  • v x y z
    Vertex coordinates are (x, y, z)
  • vt u v
    Texture vertex coordinates are (u, v)
  • f v1 v2 ... vn
    Face with n corners, at vertex v1, v2, ... vn. For meshes without UVs.
  • f v1/vt1 v2/vt2 ... vn/vtn
    Face with n corners. The corners are at vertex v1, v2, ... vn in 3D space and at vt1, vt2, ... vtn in texture space.

More constructs, e.g. for material settings and face groups, exist in full-fledged obj exporter and importers.

There are two things to be aware of. First, most applications (to my knowledge, all except Blender) use a convention were the Y axis points up, whereas Blender uses the Z up convention. Second, Maya start counting vertices from 1, whereas Blender starts counting from 0. This means that the face corners are really located at v1-1, v2-1, ... vn-1 in 3D space and at vt1-1, vt2-1, ... vtn-1 in texture space.

The simple obj exporter-importer is a Python package which consists of three files: two files that does the actual export/import work, and __init__.py which makes the directory into a package.

Simple obj exporter

This script exports the selected mesh as an obj file.

#----------------------------------------------------------
# File export_simple_obj.py
# Simple obj exporter which writes only verts, faces, and texture verts
#----------------------------------------------------------
import bpy, os

def export_simple_obj(filepath, ob, rot90, scale):
    name = os.path.basename(filepath)
    realpath = os.path.realpath(os.path.expanduser(filepath))
    fp = open(realpath, 'w')    
    print('Exporting %s' % realpath)

    if not ob or ob.type != 'MESH':
        raise NameError('Cannot export: active object %s is not a mesh.' % ob)
    me = ob.data

    for v in me.vertices:
        x = scale*v.co
        if rot90:
            fp.write("v %.5f %.5f %.5f\n" % (x[0], x[2], -x[1]))
        else:
            fp.write("v %.5f %.5f %.5f\n" % (x[0], x[1], x[2]))

    if len(me.uv_textures) > 0:
        uvtex = me.uv_textures[0]
        for f in me.faces:
            data = uvtex.data[f.index]
            fp.write("vt %.5f %.5f\n" % (data.uv1[0], data.uv1[1]))
            fp.write("vt %.5f %.5f\n" % (data.uv2[0], data.uv2[1]))
            fp.write("vt %.5f %.5f\n" % (data.uv3[0], data.uv3[1]))
            if len(f.vertices) == 4:
                fp.write("vt %.5f %.5f\n" % (data.uv4[0], data.uv4[1]))

        vt = 1
        for f in me.faces:
            vs = f.vertices
            fp.write("f %d/%d %d/%d %d/%d" % (vs[0]+1, vt, vs[1]+1, vt+1, vs[2]+1, vt+2))
            vt += 3
            if len(f.vertices) == 4:
                fp.write(" %d/%d\n" % (vs[3]+1, vt))
                vt += 1        
            else:
                fp.write("\n")
    else:
        for f in me.faces:
            vs = f.vertices
            fp.write("f %d %d %d" % (vs[0]+1, vs[1]+1, vs[2]+1))
            if len(f.vertices) == 4:
                fp.write(" %d\n" % (vs[3]+1))
            else:
                fp.write("\n")
    
    print('%s successfully exported' % realpath)
    fp.close()
    return

Simple obj import

This script is the import companion of the previous one. It can of course also used to import obj files from other applications.

#----------------------------------------------------------
# File import_simple_obj.py
# Simple obj importer which reads only verts, faces, and texture verts
#----------------------------------------------------------
import bpy, os

def import_simple_obj(filepath, rot90, scale):
    name = os.path.basename(filepath)
    realpath = os.path.realpath(os.path.expanduser(filepath))
    fp = open(realpath, 'rU')    # Universal read
    print('Importing %s' % realpath)

    verts = []
    faces = []
    texverts = []
    texfaces = []

    for line in fp:
        words = line.split()
        if len(words) == 0:
            pass
        elif words[0] == 'v':
            (x,y,z) = (float(words[1]), float(words[2]), float(words[3]))
            if rot90:
                verts.append( (scale*x, -scale*z, scale*y) )
            else:
                verts.append( (scale*x, scale*y, scale*z) )
        elif words[0] == 'vt':
            texverts.append( (float(words[1]), float(words[2])) )
        elif words[0] == 'f':
            (f,tf) = parseFace(words)
            faces.append(f)
            if tf:
                texfaces.append(tf)
        else:
            pass
    print('%s successfully imported' % realpath)
    fp.close()

    me = bpy.data.meshes.new(name)
    me.from_pydata(verts, [], faces)
    me.update()

    if texverts:
        uvtex = me.uv_textures.new()
        uvtex.name = name
        data = uvtex.data
        for n in range(len(texfaces)):
            tf = texfaces[n]
            data[n].uv1 = texverts[tf[0]]
            data[n].uv2 = texverts[tf[1]]
            data[n].uv3 = texverts[tf[2]]
            if len(tf) == 4:
                data[n].uv4 = texverts[tf[3]]

    scn = bpy.context.scene
    ob = bpy.data.objects.new(name, me)
    scn.objects.link(ob)
    scn.objects.active = ob

    return
    
def parseFace(words):
    face = []
    texface = []
    for n in range(1, len(words)):
        li = words[n].split('/')
        face.append( int(li[0])-1 )
        try:
            texface.append( int(li[1])-1 )
        except:
            pass
    return (face, texface)

__init__.py

This file contains the user interface, i.e. the two classes that create menu items for the exporter and importer. The simple exporter is invoked from the File » Export menu. There are two options: a boolean choice to rotate the mesh 90 degrees (to convert between Y up and Z up), and a scale. The simple importer is invoked from the File » Import menu. There are two options: a boolean choice to rotate the mesh 90 degrees (to make Z up), and a scale.

__init__.py also contains the bl_info dictionary which makes the package into a Blender add-on, the registration code, and the code to import/reload the two other files.

#----------------------------------------------------------
# File __init__.py
#----------------------------------------------------------

#    Addon info
bl_info = {
    'name': 'Simple OBJ format',
    'author': 'Thomas Larsson',
    'location': 'File > Import-Export',
    'description': 'Simple Wavefront obj import/export. Does meshes and UV coordinates',
    'category': 'Import-Export'}

# To support reload properly, try to access a package var, 
# if it's there, reload everything
if "bpy" in locals():
    import imp
    if 'simple_obj_import' in locals():
        imp.reload(simple_obj_import)
    if 'simple_obj_export' in locals():
        imp.reload(simple_obj_export)

import bpy
from bpy.props import *
from io_utils import ExportHelper, ImportHelper

#
#    Import menu
#

class IMPORT_OT_simple_obj(bpy.types.Operator, ImportHelper):
    bl_idname = "io_import_scene.simple_obj"
    bl_description = 'Import from simple OBJ file format (.obj)'
    bl_label = "Import simple OBJ"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"

    filename_ext = ".obj"
    filter_glob = StringProperty(default="*.obj;*.mtl", options={'HIDDEN'})

    filepath = bpy.props.StringProperty(
        name="File Path", 
        description="File path used for importing the simple OBJ file", 
        maxlen= 1024, default= "")

    rot90 = bpy.props.BoolProperty(
        name = "Rotate 90 degrees",
        description="Rotate mesh to Z up",
        default = True)

    scale = bpy.props.FloatProperty(
        name = "Scale", 
        description="Scale mesh", 
        default = 0.1, min = 0.001, max = 1000.0)

    def execute(self, context):
        from . import simple_obj_import
        print("Load", self.properties.filepath)
        simple_obj_import.import_simple_obj(
            self.properties.filepath, 
            self.rot90, 
            self.scale)
        return {'FINISHED'}

    def invoke(self, context, event):
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}

#
#    Export menu
#

class EXPORT_OT_simple_obj(bpy.types.Operator, ExportHelper):
    bl_idname = "io_export_scene.simple_obj"
    bl_description = 'Export from simple OBJ file format (.obj)'
    bl_label = "Export simple OBJ"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"

    # From ExportHelper. Filter filenames.
    filename_ext = ".obj"
    filter_glob = StringProperty(default="*.obj", options={'HIDDEN'})

    filepath = bpy.props.StringProperty(
        name="File Path", 
        description="File path used for exporting the simple OBJ file", 
        maxlen= 1024, default= "")

    rot90 = bpy.props.BoolProperty(
        name = "Rotate 90 degrees",
        description="Rotate mesh to Y up",
        default = True)

    scale = bpy.props.FloatProperty(
        name = "Scale", 
        description="Scale mesh", 
        default = 0.1, min = 0.001, max = 1000.0)

    def execute(self, context):
        print("Load", self.properties.filepath)
        from . import simple_obj_export
        simple_obj_export.export_simple_obj(
            self.properties.filepath, 
            context.object, 
            self.rot90, 
            1.0/self.scale)
        return {'FINISHED'}

    def invoke(self, context, event):
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}

#
#    Registration
#

def menu_func_import(self, context):
    self.layout.operator(IMPORT_OT_simple_obj.bl_idname, text="Simple OBJ (.obj)...")

def menu_func_export(self, context):
    self.layout.operator(EXPORT_OT_simple_obj.bl_idname, text="Simple OBJ (.obj)...")

def register():
    bpy.utils.register_module(__name__)
    bpy.types.INFO_MT_file_import.append(menu_func_import)
    bpy.types.INFO_MT_file_export.append(menu_func_export)

def unregister():
    bpy.utils.unregister_module(__name__)
    bpy.types.INFO_MT_file_import.remove(menu_func_import)
    bpy.types.INFO_MT_file_export.remove(menu_func_export)

if __name__ == "__main__":
    register()