Многофайловые пакеты

Пакеты — это способ структурирования пространства имен модулей Питона, используя "точечную нотацию имен модулей". Например, имя модуля

A.B
определяет подмодуль с именем
B
в пакете с именем
A
. Точно так же как использование модулей спасает авторов различных модулей от необходимости беспокоиться о существовании совпадающих глобальных имен переменных, использование точечной нотации имен модулей спасает авторов многомодульных пакетов от необходимости волноваться о совпадающих именах модулей. За дополнительной информацией о пакетах Питона, пожалуйста, обратитесь к документации на пакеты Питона

Каждый пакет должен содержать файл

__init__.py
. Этот файл необходим, чтобы заставить Питон относиться к каталогу, как к содержащему пакет, это сделано для предотвращения у каталогов с частоиспользуемым названием, например, string, непреднамеренного сокрытия действительного модуля, которое происходит в дальнейшем пути поиска модулей. В простейшем случае,
__init__.py
может быть просто пустым файлом, но он также может выполнять код инициализации пакета. В Блендере
__init__.py
часто содержит пользовательский интерфейс и информацию аддона, в то время как реальная работа делается в других файлах.

В отличие от других скриптов в этой книге, многофайловый пакет не может быть выполнен из текстового редактора. Он должен быть скопирован в место, которое входит в путь поиска в Блендере, например,

addons
или
addons-contrib
, см. раздел аддоны Блендера. К счастью, вам не нужно перезагружать весь Блендер для перезагрузки файлов после каждой модификации. Нажатие F8 на клавиатуре перезагружает все активированные аддоны в Блендере.


Простой пример

Этот пакет разнесён на четыре файла. Три из них создают меши: куб, цилиндр и сферу, соответственно. Это файлы автономных скриптов, которые можно выполнять в окне текстового редактора для отладочных целей. Условие

(__name__ == "__main__")
истинно, если файл был запущен в автономном режиме.

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

Четвертый файл содержит словарь

bl_info
и код регистрации, необходимый для аддона и пользовательского интерфейса. В нем также содержится следующий фрагмент кода для импорта других файлов пакета.


# Для поддержки правильной перезагрузки, пробуем обратиться 

# к переменной пакета, если она есть, перезагрузить всё 

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


Этот код работает следующим образом.

• Если

__init__.py()
запускается в первый раз, т.е. при запуске Блендера с включенным аддоном в вашем файле default.blend,
"bpy" in locals()
возвращает Ложь. Другие файлы в пакете импортируются и в терминале выводится "Imported multifiles".

• Если

__init__.py()
запускается в первый раз после запуска Блендера с выключенным аддоном в вашем файле default.blend, и вы нажали включение аддона,
"bpy" in locals()
возвращает Ложь. Другие файлы в пакете импортируются и в терминале выводится "Imported multifiles".

• После того, как дополнения включены, в любое время вы нажимаете F8, чтобы перезагрузить аддоны,

"bpy" in locals()
возвращает Истину. Другие файлы в пакете перезагружаются, а в терминал выводится "Reloaded multifiles".


#---------------------------------------------------------- 

# File __init__.py 

#---------------------------------------------------------- 


# Addon info

bl_info = {

   "name": "Multifile",

   'author': 'Thomas Larsson',

   "location": "View3D > UI panel > Add meshes",

   "category": "3D View"

   }  


# Для поддержки правильной перезагрузки, пробуем обратиться 

# к переменной пакета, если она есть, перезагрузить всё 

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'} 


#

# Регистрация 

#  


def register():

   bpy.utils.register_module(__name__)  


def unregister():

   bpy.utils.unregister_module(__name__) 


if __name__ == "__main__":

   register()


Простой импортёр и экспортёр obj-файлов

Формат OBJ часто используется для обмена данными меша между различными приложениями. Первоначально изобретеный для Wavefront Maya, он стал отраслевым стандартом. Это простой ASCII-формат, который содержит строки следующего вида:

v x y z

Координаты вершин как

(x, y, z)

vt u v

Текстурные координаты как

(u, v)

f v1 v2 ... vn

Грань с n углами, в вершинах

v1, v2, ... vn
. Для мешей без координат UV.

f v1/vt1 v2/vt2 ... vn/vtn

Грани с n углами. Углы — это вершины

v1, v2, ... vn
в 3D-пространстве и
vt1, vt2, ... vtn
в текстурном пространстве.


Больше конструкций, например, для настройки материала или групп граней, имеются в полноценном экспортёре-импортёре OBJ-формата.

Есть две вещи, которые надо принять во внимание. Во-первых, большинство приложений (насколько мне известно, все, кроме Блендера) используют соглашение, что ось

Y
указывает вверх, в то время как Блендер использует ось
Z
для направления вверх. Во-вторых, Майя начинает подсчет вершин с
1
, тогда как Блендер начинает отсчет от
0
. Это означает, что углы граней на самом деле расположены в вершинах
v1-1, v2-1, ... vn-1
в 3D-пространстве и в
vt1-1, vt2-1, ... vtn-1
в пространстве текстур.

Простой экспортёр-импортёр OBJ-файлов — это пакет Питона, который состоит из трех файлов: два файла, которые фактически выполняют работу экспорта/импорта, и

__init__.py
, который делает каталог пакетом.


Простой экспорт OBJ-файлов

Этот скрипт экспортирует выбранный меш как OBJ-файл.


#---------------------------------------------------------- 

# File export_simple_obj.py 

# Простой OBJ-экспортёр, который записывает только вершины, грани и текстурные вершины 

#---------------------------------------------------------- 

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


Простой импорт OBJ-файлов

Этот скрипт импорта — компаньон предыдущего. Он, конечно, также может использоваться для импорта OBJ-файлов из других приложений.


#---------------------------------------------------------- 

# File import_simple_obj.py 

# Простой OBJ-импортёр, который читает только вершины, грани и текстурные вершины 

#---------------------------------------------------------- 

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

Этот файл содержит пользовательский интерфейс, то есть два класса, которые создают пункты меню для экспортёра и импортёра. Простой экспортёр вызывается из меню File » Export. Есть две опции: логический выбор, чтобы повернуть меш на 90 градусов (для преобразования между осями Y и Z для направления вверх), и масштаб. Простой импортёр вызывается из меню File » Import. Есть две опции: логический выбор, чтобы повернуть меш на 90 градусов (чтобы ось Z указывала вверх), и масштаб.

__init__.py
также содержит словарь
bl_info
, который преобразует пакет в аддон Блендера, код регистрации, и код для импорта/перезагрузки двух других файлов.


#---------------------------------------------------------- 

# File __init__.py 

#---------------------------------------------------------- 


# Информация аддона 

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"}  


# Для поддержки правильной перезагрузки, пробуем обратиться 

# к переменной пакета, если она есть, перезагрузить всё 

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 

#  

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 

#  


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"  


   # Из ExportHelper. Фильтрация имён файлов.

   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'} 


#

# Регистрация 

#  


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()




Загрузка...