BMeshing с контекстными менеджерами

Стоимость использования Python всегда ищет более простые способы сделать что-то. Это относится и к Blender. Недавно я обнаружил, что копирую и вставляю из кода тот же самый старый шаблон BMesh и спрашиваю себя, не существует ли способа сделать это менее повторяющимся. Есть конечно!
Картинки по запросу блендер 3д 2,8

Всегда пишите код, как будто парень, который в конечном итоге будет поддерживать ваш код, будет жестоким психопатом, который знает, где вы живете.
Джон Ф. Вудс
Моей первой идеей было создание функции, которая принимает другую функцию (функция высокого порядка в функциональном жаргоне). Эта функция создаст объект bmesh, запустит заданную ему функцию, а затем отправит данные BMesh в меш и освободит его. Но это было своего рода ограничение. Вы можете запустить только одну вещь, или вам придется сгруппировать их в другую функцию. Другая идея заключалась в том, чтобы создать класс-обертку, но мне все равно пришлось бы вручную его освобождать. Кроме того, я не большой поклонник классов.
Открытие файла дало мне лучшую идею: контекстные менеджеры.
Менеджеры контекста - это специальные объекты в Python, которые позволяют вам определять специальные контексты времени выполнения. Обычно для управления ресурсами. Вы создаете / открываете ресурсы, когда заходите в контекст, используете его, и когда вы покидаете ресурс, он автоматически закрывается / освобождается.
Рассмотрим openфункцию / менеджер контекста, например.
    # This snippet...
    with open('some/file', 'r') as the_file:
        do_something(the_file)

    # ...is doing this behind the scenes
    the_file = open('some/file', 'r')
    do_something(the_file)
    close(the_file)
Немного сахара для мирской задачи открытия файлов. Но он автоматически заботится о закрытии файлов (чтобы мы не пропускали дескрипторы) и аккуратно группирует весь код на новом уровне отступов. А как же БМеш? Объект BMesh не слишком отличается от ресурса Python. Мы создаем его, передаем и, наконец, отправляем в меш и освобождаем (или позволяем, если выпадают из области видимости).
Менеджеры контекста определяются как классы с тремя специфическими методами:
__init____enter__и __exit__Вы можете добавить свой, конечно, но это минимум, необходимый. Давайте посмотрим на первую реализацию.
class Bmesh_from_obj():

    def __init__(self, obj):
        # Register parameters as class properties
        self.obj = obj

    def __enter__(self):
        # Create and return bmesh object
        self.bm = bmesh.new()
        self.bm.from_object(self.obj)

        return self.bm

    def __exit__(self, *args):
        # Clean up: send bmesh to mesh and free()
        self.bm.to_mesh(obj.data)
        self.bm.free()
При этом мы уже можем использовать bmesh_objв качестве менеджера контекста (в объектном режиме). Обратите внимание, что возвращать значение в __enter__необязательно, вы можете создать  with блок, который не привязывает переменную. Также, если мы нажмем исключение __init__ или __enter__никогда не попадем внутрь блока with. С другой стороны, если мы идем внутрь, __exit__метод всегда вызывается, и мы можем использовать его для обработки исключений, подобных этому:
    def __exit__(self, exc_type, exc_value, exc_traceback):
        self.bm.to_mesh(obj.data)
        self.bm.free()

        if exc_type:
            print('Oh no')
            print(f'{exc_value}. Trace: {exc_traceback}')
Я оставлю обработку исключений для вас, хотя. Каждый проект индивидуален, и вы можете захотеть обработать исключения раньше или позже (позволяя им подняться). Кроме исключений, мы можем сделать еще две вещи, чтобы улучшить это: упростить код и поддерживать режим редактирования. Давайте сначала упростим.
Мы можем использовать декоратор для функции вместо написания целого класса. Сначала нам нужно импортировать @contextmanagerиз contextlib. Модуль contextlib полностью посвящен менеджерам контекста, и его документация - это то место, куда можно обратиться, если вы хотите углубиться в них. Как бы тогда выглядела эта функция?
from contextlib import contextmanager


@contextmanager
def bmesh_from_obj(obj):
    # Create bmesh object and yield it

    yield bm
        # Code inside the with block gets executed here

    # Clean up (we're leaving the with block)
Заметьте , что мы получали Bm вместо того , чтобы вернуться , как мы делали в в  __enter__методе раньше. Декоратору нужна функция для возврата одного итератора генератора значений, который затем становится целью оператора with (часть «as»). Вот и все для инициализации. Код внутри withблока выполняется сразу после yield, и как только мы достигаем конца, он выполняет остальную часть функции.
Давайте добавим параметр mode и уточним это:
@contextmanager
def bmesh_from_obj(obj, mode):
    """Context manager to auto-manage BMesh."""

    if mode == 'EDIT_MESH':
        bm = bmesh.from_edit_mesh(obj.data)
    else:
        bm = bmesh.new()
        bm.from_mesh(obj.data)

    yield bm

    # Send to mesh and clean up
    bm.normal_update()

    if mode == 'EDIT_MESH':
        bmesh.update_edit_mesh(obj.data)
    else:
        bm.to_mesh(obj.data)

    bm.free()
Теперь мы можем передать переменную режима из контекста и использовать BMesh в любом режиме. Вот пример из руководства по экструзии.
with bmesh_from_obj(obj, bpy.context.mode) as bm:
    # Get geometry to extrude
    bm.faces.ensure_lookup_table()
    faces = [bm.faces[0]]  # For a plane
    faces = [bm.faces[5]]  # For the top face of the cube# Extrude

    extruded = bmesh.ops.extrude_face_region(bm, geom=faces)

    # Move extruded geometry
    translate_verts = [v for v in extruded['geom'] if isinstance(v, BMVert)]

    up = Vector((0, 0, 1))
    bmesh.ops.translate(bm, vec=up, verts=translate_verts)


    bmesh.ops.delete(bm, geom=faces, context=DEL_FACES)

    # Remove doubles
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
Что если мы хотим отправить данные BMesh новому объекту? Мы можем добавить небольшую служебную функцию, чтобы создать новый пустой объект и передать его менеджеру контекста.
def new_obj(obj_name):
    """Add a new object to the scene."""

    # Make new object when leaving context manager
    mesh = bpy.data.meshes.new(obj_name)
    obj = bpy.data.objects.new(obj_name, mesh)
    bpy.context.scene.objects.link(obj)

    return obj


# (Later)
with bmesh_obj(new_obj('myobj'), bpy.context.mode):
    pass

Финальный код

Я сделал пример немного более интересным для окончательного примера. Попробуйте! Этот код создает простую скрученную форму. Он начинается с плоскости, несколько раз выдавливает ее, вращает и масштабирует некоторые края. Как вы можете видеть в  extrudeфункции, мы также можем абстрагировать некоторые общие операции bmesh в функции, чтобы повторно использовать код или сделать его более выразительным. Все может пойти в withблоке.
import bpy
import bmesh
from bpy.types import Object

from bmesh.types import BMFace, BMVert

from mathutils import Vector
from contextlib import contextmanager

from math import radians
from mathutils import Matrix

# ------------------------------------------------------------------------------
# BMesh Context manager
# ------------------------------------------------------------------------------

@contextmanager
def bmesh_from_obj(obj, mode):
    """Context manager to auto-manage bmesh regardless of mode."""

    if mode == 'EDIT_MESH':
        bm = bmesh.from_edit_mesh(obj.data)
    else:
        bm = bmesh.new()
        bm.from_mesh(obj.data)

    yield bm

    bm.normal_update()

    if mode == 'EDIT_MESH':
        bmesh.update_edit_mesh(obj.data)
    else:
        bm.to_mesh(obj.data)

    bm.free()



# ------------------------------------------------------------------------------
# Bmesh / Utils functions
# ------------------------------------------------------------------------------
def new_obj(obj_name):
    """Add a new object to the scene."""

    # Make new object when leaving context manager
    mesh = bpy.data.meshes.new(obj_name)
    obj = bpy.data.objects.new(obj_name, mesh)
    bpy.context.scene.objects.link(obj)

    return obj


def extrude(bm, faces, direction, remove=True):
    """Extrude a set of faces in a direction"""

    # Extrude
    extruded = bmesh.ops.extrude_face_region(bm, geom=faces)
    translate_verts = [v for v in extruded['geom'] if isinstance(v, BMVert)]

    bmesh.ops.translate(bm, vec=Vector(direction), verts=translate_verts)

    if remove:
        bmesh.ops.delete(bm, geom=faces, context=5)

    return [f for f in extruded['geom'] if isinstance(f, BMFace)]


# ------------------------------------------------------------------------------
# Testing
# ------------------------------------------------------------------------------

# Add a new (empty) object
obj = new_obj('Bmesh test')

# We could also pass an existing object
# obj = bpy.context.object

with bmesh_from_obj(obj, bpy.context.mode) as bm:

    # A grid with segments of 1 is a plane
    bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1)

    # We need to call this since we are accesing faces by index
    bm.faces.ensure_lookup_table()

    faces = [bm.faces[0]]
    new_faces = extrude(bm, faces, (0, 0, 1), False)

    # Keep a copy of these verts for later
    middle_verts = new_faces[0].verts[:]

    # Another extrusion because why not
    top_faces = extrude(bm, new_faces, (0, 0, 3))

    # Give it a thin waist
    bmesh.ops.scale(bm, vec=Vector((0.25, 0.25, 1)), verts=middle_verts)

    # Add a small rotation at the top
    bmesh.ops.rotate(bm, verts=top_faces[0].verts, cent=(0, 0, 0),
                     matrix=Matrix.Rotation(radians(15), 3, 'Z'))

    # Unnecesary but for demo purposes...
    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
Посмотрите мою серию уроков по созданию сетки, если вы ищете больше идей.
Если вы заинтересованы в получении дополнительной информации о менеджерах контекста, я рекомендую обратиться к документации модуля contextlib. Также проверьте оригинальное предложение PEP для этой функции: PEP343 .

Комментарии