Module kawa_scripts.instantiator

Expand source code
# Kawashirov's Scripts (c) 2021 by Sergey V. Kawashirov
#
# Kawashirov's Scripts is licensed under a
# Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
#
# You should have received a copy of the license along with this
# work.  If not, see <http://creativecommons.org/licenses/by-nc-sa/3.0/>.
#
#
from collections import deque as _deque

import bpy as _bpy

from . import commons as _commons
from . import modifiers as _modifiers
from .reporter import LambdaReporter as _LambdaReporter
from ._internals import log as _log

import typing as _typing

if _typing.TYPE_CHECKING:
        from typing import *
        from bpy.types import *
        
        Collection = _bpy.types.Collection


class BaseInstantiator:
        ORIGINAL_NAME = '__KawaInstantiator_OriginalName'
        
        # Создаёт рабочию копию оригинальных объектов (для запекания):
        # - Копирует объекты и их меши
        # - Превращает инстансы коллекций в объекты
        # - Заменяет OBJECT текстуры на DATA
        # - Применяет все модификаторы
        # -
        # - TODO переименования
        #
        # Как использовать:
        # - Задать сцены: .original_scene и .working_scene
        # - Положить оригиналы в .originals
        # - Запустить .run()
        # - Копии будут лежать в copy2original и original2copy
        # - Все новые объекты сохранятся в .copies
        # Не стоит запускать run() повторно или изменять что-либо после run()
        
        def __init__(self):
                self.original_scene = None  # type: Scene
                self.working_scene = None  # type: Scene
                self.instantiate_collections = True
                self.instantiate_material_slots = True
                self.apply_modifiers = True
                self.apply_scales = False
                self.report_time = 5
                
                self.originals = set()  # type: Set[Object]
                self.copies = set()  # type: Set[Object]
                
                self._original_names = set()  # type: Set[Object]
        
        def rename_copy(self, obj: 'Object', original_name: 'str', ) -> 'str':
                return NotImplemented
        
        def rename_object_from_collection(self,
                        parent_obj: 'Object', parent_obj_orig_name: 'str',
                        inst_obj: 'Object', inst_obj_orig_name: 'str',
                        collection: 'Collection'
        ) -> 'str':
                return NotImplemented
        
        def _check_originals(self):
                wrong_scene = set()
                for original in self.originals:
                        if self.original_scene not in original.users_scene:
                                wrong_scene.add(original)
                if len(wrong_scene) > 0:
                        wrong_scene_str = ', '.join(repr(x.name) for x in wrong_scene)
                        msg = '{0} of {1} original objects does not belong to original_scene={2}: {3}'.format(
                                len(wrong_scene), len(self.originals), repr(self.original_scene.name), wrong_scene_str)
                        _log.error(msg)
                        raise RuntimeError(msg, wrong_scene)

        def _register_original_names(self):
                original_names_q = _deque()
                original_names_q.extend(self.originals)
                self._original_names.clear()
                while len(original_names_q) > 0:
                        obj = original_names_q.pop()  # type: Object
                        self._original_names.add(obj)
                        if obj.instance_type == 'COLLECTION' and obj.instance_collection is not None:
                                original_names_q.extend(obj.instance_collection.objects)
                for obj in self._original_names:
                        obj[self.ORIGINAL_NAME] = obj.name
                        
        def _put_originals_on_working(self):
                _commons.ensure_deselect_all_objects()
                for original in self.originals:
                        if original.name not in self.working_scene.collection.objects:
                                self.working_scene.collection.objects.link(original)
                        _commons.activate_object(original)
        
        def _duplicate(self):
                _commons.ensure_op_finished(_bpy.ops.object.duplicate(linked=False), name='bpy.ops.object.duplicate')
                self.copies.update(_bpy.context.selected_objects)
                _log.info('Basic copies created: {0}'.format(len(self.copies)))
                _commons.ensure_deselect_all_objects()
                
        def _unlink_originals_from_working(self):
                for original in self.originals:
                        if original.name in self.working_scene.collection.objects:
                                self.working_scene.collection.objects.unlink(original)
        
        def _rename_copies(self):
                _log.info('Renaming copies...')
                for copy in self.copies:
                        original_name = copy.get(self.ORIGINAL_NAME)
                        if original_name is not None:
                                new_name = None
                                try:
                                        new_name = self.rename_copy(copy, original_name)
                                except Exception as exc:
                                        # TODO
                                        raise RuntimeError('rename', copy, original_name, new_name) from exc
                                if isinstance(new_name, str):
                                        copy.name = new_name

        def _instantiate_collections(self):
                _log.info('Instantiating collections...')
                
                created, obj_i, inst_i = 0, 0, 0
                reporter = _LambdaReporter(self.report_time)
                reporter.func = lambda r, t: _log.info(
                        "Instantiating collections: Objects={0}/{1}, Instantiated={2}, Created={3}, Time={4:.1f} sec...".format(
                                obj_i, len(self.copies), inst_i, created, t))
                
                queue = _deque()
                queue.extend(self.copies)
                while len(queue) > 0:
                        obj = queue.pop()  # type: Object
                        obj_i += 1
                        if obj.type != 'EMPTY' or obj.instance_type != 'COLLECTION' or obj.instance_collection is None:
                                continue
                        inst_i += 1
                        _commons.ensure_deselect_all_objects()
                        _commons.activate_object(obj)
                        collection = obj.instance_collection
                        _commons.ensure_op_finished(_bpy.ops.object.duplicates_make_real(
                                use_base_parent=True, use_hierarchy=True
                        ), name='bpy.ops.object.duplicates_make_real')
                        self.copies.update(_bpy.context.selected_objects)
                        queue.extend(_bpy.context.selected_objects)
                        created += len(_bpy.context.selected_objects)
                        obj_orignal_name = obj.get(self.ORIGINAL_NAME)
                        for inst_obj in list(_bpy.context.selected_objects):
                                inst_obj_orignal_name = inst_obj.get(self.ORIGINAL_NAME)
                                new_name = self.rename_object_from_collection(obj, obj_orignal_name, inst_obj, inst_obj_orignal_name, collection)
                                if isinstance(new_name, str):
                                        inst_obj.name = new_name
                                elif isinstance(inst_obj_orignal_name, str):
                                        inst_obj.name = obj.name + '-' + inst_obj_orignal_name
                        reporter.ask_report(False)
                        
                _commons.ensure_deselect_all_objects()
                reporter.ask_report(True)
        
        def _convert_curves_to_meshes(self):
                _log.info('Converting curves to meshes...')
                curves = list(obj for obj in self.copies if isinstance(obj.data, _bpy.types.Curve))
                if len(curves) < 1:
                        return
                _commons.ensure_deselect_all_objects()
                _commons.activate_objects(curves)
                self.copies.difference_update(curves)
                _commons.ensure_op_finished(_bpy.ops.object.convert(target='MESH'), name='bpy.ops.object.convert')
                self.copies.update(_bpy.context.selected_objects)
                _commons.ensure_deselect_all_objects()
                _log.info('Converted {0} curves to meshes.'.format(len(curves)))
        
        def _make_single_user(self):
                _log.info('Making data blocks single-users...')
                _commons.ensure_deselect_all_objects()
                _commons.select_set_all(self.copies, True)
                before = len(set(obj.data for obj in self.copies if obj.data is not None))
                _commons.ensure_op_finished(_bpy.ops.object.make_single_user(
                        object=False, obdata=True, material=False, animation=False,
                ), name='bpy.ops.object.make_single_user')
                after = len(set(obj.data for obj in self.copies if obj.data is not None))
                self.copies.update(_bpy.context.selected_objects)
                _commons.ensure_deselect_all_objects()
                _log.info('make_single_user, data blocks: {0} +{1} -> {2}'.format(before, (after - before), after))
        
        def _instantiate_material_slots(self):
                obj_i, slot_i = 0, 0
                
                reporter = _LambdaReporter(self.report_time)
                reporter.func = lambda r, t: _log.info(
                        "Instantiating material slots: Objects={0}/{1}, Slots={2}, Time={3:.1f} sec, ETA={4:.1f} sec...".format(
                                obj_i, len(self.copies), slot_i, t, r.get_eta(1.0 * obj_i / len(self.copies))))
                
                _log.info('Instantiating material slots...')
                for copy in self.copies:
                        if not isinstance(copy.data, _bpy.types.Mesh):
                                continue
                        for slot in copy.material_slots:
                                if slot.material is None or slot.link == 'DATA':
                                        continue  # Пропуск пустых или DATA материалов
                                mat = slot.material
                                # log.info("Object='%s': Switching Material='%s' from OBJECT to DATA...", copy, mat)
                                slot.link = 'DATA'
                                slot.material = mat
                                slot_i += 1
                        obj_i += 1
                        reporter.ask_report(False)
                reporter.ask_report(True)
        
        def _apply_modifiers(self):
                obj_n, obj_i, mod_i = len(self.copies), 0, 0
                
                reporter = _LambdaReporter(self.report_time)
                reporter.func = lambda r, t: _log.info(
                        "Applying modifiers: Objects={0}/{1}, Modifiers={2}, Time={3:.1f} sec, ETA={4:.1f} sec...".format(
                                obj_i, obj_n, mod_i, t, r.get_eta(1.0 * obj_i / obj_n)))
                
                _log.info('Applying modifiers...')
                for copy in self.copies:
                        mod_i += _modifiers.apply_all_modifiers(copy)
                        obj_i += 1
                        reporter.ask_report(False)
                reporter.ask_report(True)

        def _clean_original_names(self):
                for original in self._original_names:
                        if self.ORIGINAL_NAME in original:
                                del original[self.ORIGINAL_NAME]

        def run(self) -> 'None':
                if self.original_scene is None:
                        raise RuntimeError("original_scene is not set")
                if self.working_scene is None:
                        raise RuntimeError("working_scene is not set")
                
                self._check_originals()
                _log.info('Instantiating {0} objects from scene {1} to {2}... '.format(
                        len(self.originals), repr(self.original_scene.name), repr(self.working_scene.name)))
                self._register_original_names()
                _bpy.context.window.scene = self.working_scene
                self._put_originals_on_working()
                self._duplicate()
                self._unlink_originals_from_working()
                self._rename_copies()
                
                if self.instantiate_collections:
                        self._instantiate_collections()
                        
                self._make_single_user()
                self._convert_curves_to_meshes()
                
                if self.instantiate_material_slots:
                        self._instantiate_material_slots()

                if self.apply_modifiers:
                        self._apply_modifiers()
                
                if self.apply_scales:
                        _log.info('Applying scales...')
                        _commons.select_set_all(self.copies, True)
                        _bpy.ops.object.transform_apply(location=False, rotation=False, scale=self.apply_scales, properties=False)
                        _commons.select_set_all(self.copies, False)
                        _log.info('Applied scales.')
                
                invalids = list(obj for obj in self.copies if obj.name not in self.working_scene.collection.objects)
                if len(invalids) > 0:
                        _log.info("Discarding {0} invalid objects...".format(len(invalids)))
                        for invalid in invalids:
                                self.copies.discard(invalid)

                self._clean_original_names()
                
                _log.info("Instantiation done: {0} original -> {1} copies.".format(len(self.originals), len(self.copies)))

Classes

class BaseInstantiator
Expand source code
class BaseInstantiator:
        ORIGINAL_NAME = '__KawaInstantiator_OriginalName'
        
        # Создаёт рабочию копию оригинальных объектов (для запекания):
        # - Копирует объекты и их меши
        # - Превращает инстансы коллекций в объекты
        # - Заменяет OBJECT текстуры на DATA
        # - Применяет все модификаторы
        # -
        # - TODO переименования
        #
        # Как использовать:
        # - Задать сцены: .original_scene и .working_scene
        # - Положить оригиналы в .originals
        # - Запустить .run()
        # - Копии будут лежать в copy2original и original2copy
        # - Все новые объекты сохранятся в .copies
        # Не стоит запускать run() повторно или изменять что-либо после run()
        
        def __init__(self):
                self.original_scene = None  # type: Scene
                self.working_scene = None  # type: Scene
                self.instantiate_collections = True
                self.instantiate_material_slots = True
                self.apply_modifiers = True
                self.apply_scales = False
                self.report_time = 5
                
                self.originals = set()  # type: Set[Object]
                self.copies = set()  # type: Set[Object]
                
                self._original_names = set()  # type: Set[Object]
        
        def rename_copy(self, obj: 'Object', original_name: 'str', ) -> 'str':
                return NotImplemented
        
        def rename_object_from_collection(self,
                        parent_obj: 'Object', parent_obj_orig_name: 'str',
                        inst_obj: 'Object', inst_obj_orig_name: 'str',
                        collection: 'Collection'
        ) -> 'str':
                return NotImplemented
        
        def _check_originals(self):
                wrong_scene = set()
                for original in self.originals:
                        if self.original_scene not in original.users_scene:
                                wrong_scene.add(original)
                if len(wrong_scene) > 0:
                        wrong_scene_str = ', '.join(repr(x.name) for x in wrong_scene)
                        msg = '{0} of {1} original objects does not belong to original_scene={2}: {3}'.format(
                                len(wrong_scene), len(self.originals), repr(self.original_scene.name), wrong_scene_str)
                        _log.error(msg)
                        raise RuntimeError(msg, wrong_scene)

        def _register_original_names(self):
                original_names_q = _deque()
                original_names_q.extend(self.originals)
                self._original_names.clear()
                while len(original_names_q) > 0:
                        obj = original_names_q.pop()  # type: Object
                        self._original_names.add(obj)
                        if obj.instance_type == 'COLLECTION' and obj.instance_collection is not None:
                                original_names_q.extend(obj.instance_collection.objects)
                for obj in self._original_names:
                        obj[self.ORIGINAL_NAME] = obj.name
                        
        def _put_originals_on_working(self):
                _commons.ensure_deselect_all_objects()
                for original in self.originals:
                        if original.name not in self.working_scene.collection.objects:
                                self.working_scene.collection.objects.link(original)
                        _commons.activate_object(original)
        
        def _duplicate(self):
                _commons.ensure_op_finished(_bpy.ops.object.duplicate(linked=False), name='bpy.ops.object.duplicate')
                self.copies.update(_bpy.context.selected_objects)
                _log.info('Basic copies created: {0}'.format(len(self.copies)))
                _commons.ensure_deselect_all_objects()
                
        def _unlink_originals_from_working(self):
                for original in self.originals:
                        if original.name in self.working_scene.collection.objects:
                                self.working_scene.collection.objects.unlink(original)
        
        def _rename_copies(self):
                _log.info('Renaming copies...')
                for copy in self.copies:
                        original_name = copy.get(self.ORIGINAL_NAME)
                        if original_name is not None:
                                new_name = None
                                try:
                                        new_name = self.rename_copy(copy, original_name)
                                except Exception as exc:
                                        # TODO
                                        raise RuntimeError('rename', copy, original_name, new_name) from exc
                                if isinstance(new_name, str):
                                        copy.name = new_name

        def _instantiate_collections(self):
                _log.info('Instantiating collections...')
                
                created, obj_i, inst_i = 0, 0, 0
                reporter = _LambdaReporter(self.report_time)
                reporter.func = lambda r, t: _log.info(
                        "Instantiating collections: Objects={0}/{1}, Instantiated={2}, Created={3}, Time={4:.1f} sec...".format(
                                obj_i, len(self.copies), inst_i, created, t))
                
                queue = _deque()
                queue.extend(self.copies)
                while len(queue) > 0:
                        obj = queue.pop()  # type: Object
                        obj_i += 1
                        if obj.type != 'EMPTY' or obj.instance_type != 'COLLECTION' or obj.instance_collection is None:
                                continue
                        inst_i += 1
                        _commons.ensure_deselect_all_objects()
                        _commons.activate_object(obj)
                        collection = obj.instance_collection
                        _commons.ensure_op_finished(_bpy.ops.object.duplicates_make_real(
                                use_base_parent=True, use_hierarchy=True
                        ), name='bpy.ops.object.duplicates_make_real')
                        self.copies.update(_bpy.context.selected_objects)
                        queue.extend(_bpy.context.selected_objects)
                        created += len(_bpy.context.selected_objects)
                        obj_orignal_name = obj.get(self.ORIGINAL_NAME)
                        for inst_obj in list(_bpy.context.selected_objects):
                                inst_obj_orignal_name = inst_obj.get(self.ORIGINAL_NAME)
                                new_name = self.rename_object_from_collection(obj, obj_orignal_name, inst_obj, inst_obj_orignal_name, collection)
                                if isinstance(new_name, str):
                                        inst_obj.name = new_name
                                elif isinstance(inst_obj_orignal_name, str):
                                        inst_obj.name = obj.name + '-' + inst_obj_orignal_name
                        reporter.ask_report(False)
                        
                _commons.ensure_deselect_all_objects()
                reporter.ask_report(True)
        
        def _convert_curves_to_meshes(self):
                _log.info('Converting curves to meshes...')
                curves = list(obj for obj in self.copies if isinstance(obj.data, _bpy.types.Curve))
                if len(curves) < 1:
                        return
                _commons.ensure_deselect_all_objects()
                _commons.activate_objects(curves)
                self.copies.difference_update(curves)
                _commons.ensure_op_finished(_bpy.ops.object.convert(target='MESH'), name='bpy.ops.object.convert')
                self.copies.update(_bpy.context.selected_objects)
                _commons.ensure_deselect_all_objects()
                _log.info('Converted {0} curves to meshes.'.format(len(curves)))
        
        def _make_single_user(self):
                _log.info('Making data blocks single-users...')
                _commons.ensure_deselect_all_objects()
                _commons.select_set_all(self.copies, True)
                before = len(set(obj.data for obj in self.copies if obj.data is not None))
                _commons.ensure_op_finished(_bpy.ops.object.make_single_user(
                        object=False, obdata=True, material=False, animation=False,
                ), name='bpy.ops.object.make_single_user')
                after = len(set(obj.data for obj in self.copies if obj.data is not None))
                self.copies.update(_bpy.context.selected_objects)
                _commons.ensure_deselect_all_objects()
                _log.info('make_single_user, data blocks: {0} +{1} -> {2}'.format(before, (after - before), after))
        
        def _instantiate_material_slots(self):
                obj_i, slot_i = 0, 0
                
                reporter = _LambdaReporter(self.report_time)
                reporter.func = lambda r, t: _log.info(
                        "Instantiating material slots: Objects={0}/{1}, Slots={2}, Time={3:.1f} sec, ETA={4:.1f} sec...".format(
                                obj_i, len(self.copies), slot_i, t, r.get_eta(1.0 * obj_i / len(self.copies))))
                
                _log.info('Instantiating material slots...')
                for copy in self.copies:
                        if not isinstance(copy.data, _bpy.types.Mesh):
                                continue
                        for slot in copy.material_slots:
                                if slot.material is None or slot.link == 'DATA':
                                        continue  # Пропуск пустых или DATA материалов
                                mat = slot.material
                                # log.info("Object='%s': Switching Material='%s' from OBJECT to DATA...", copy, mat)
                                slot.link = 'DATA'
                                slot.material = mat
                                slot_i += 1
                        obj_i += 1
                        reporter.ask_report(False)
                reporter.ask_report(True)
        
        def _apply_modifiers(self):
                obj_n, obj_i, mod_i = len(self.copies), 0, 0
                
                reporter = _LambdaReporter(self.report_time)
                reporter.func = lambda r, t: _log.info(
                        "Applying modifiers: Objects={0}/{1}, Modifiers={2}, Time={3:.1f} sec, ETA={4:.1f} sec...".format(
                                obj_i, obj_n, mod_i, t, r.get_eta(1.0 * obj_i / obj_n)))
                
                _log.info('Applying modifiers...')
                for copy in self.copies:
                        mod_i += _modifiers.apply_all_modifiers(copy)
                        obj_i += 1
                        reporter.ask_report(False)
                reporter.ask_report(True)

        def _clean_original_names(self):
                for original in self._original_names:
                        if self.ORIGINAL_NAME in original:
                                del original[self.ORIGINAL_NAME]

        def run(self) -> 'None':
                if self.original_scene is None:
                        raise RuntimeError("original_scene is not set")
                if self.working_scene is None:
                        raise RuntimeError("working_scene is not set")
                
                self._check_originals()
                _log.info('Instantiating {0} objects from scene {1} to {2}... '.format(
                        len(self.originals), repr(self.original_scene.name), repr(self.working_scene.name)))
                self._register_original_names()
                _bpy.context.window.scene = self.working_scene
                self._put_originals_on_working()
                self._duplicate()
                self._unlink_originals_from_working()
                self._rename_copies()
                
                if self.instantiate_collections:
                        self._instantiate_collections()
                        
                self._make_single_user()
                self._convert_curves_to_meshes()
                
                if self.instantiate_material_slots:
                        self._instantiate_material_slots()

                if self.apply_modifiers:
                        self._apply_modifiers()
                
                if self.apply_scales:
                        _log.info('Applying scales...')
                        _commons.select_set_all(self.copies, True)
                        _bpy.ops.object.transform_apply(location=False, rotation=False, scale=self.apply_scales, properties=False)
                        _commons.select_set_all(self.copies, False)
                        _log.info('Applied scales.')
                
                invalids = list(obj for obj in self.copies if obj.name not in self.working_scene.collection.objects)
                if len(invalids) > 0:
                        _log.info("Discarding {0} invalid objects...".format(len(invalids)))
                        for invalid in invalids:
                                self.copies.discard(invalid)

                self._clean_original_names()
                
                _log.info("Instantiation done: {0} original -> {1} copies.".format(len(self.originals), len(self.copies)))

Class variables

var ORIGINAL_NAME

Methods

def rename_copy(self, obj: Object, original_name: str) ‑> str
Expand source code
def rename_copy(self, obj: 'Object', original_name: 'str', ) -> 'str':
        return NotImplemented
def rename_object_from_collection(self, parent_obj: Object, parent_obj_orig_name: str, inst_obj: Object, inst_obj_orig_name: str, collection: Collection) ‑> str
Expand source code
def rename_object_from_collection(self,
                parent_obj: 'Object', parent_obj_orig_name: 'str',
                inst_obj: 'Object', inst_obj_orig_name: 'str',
                collection: 'Collection'
) -> 'str':
        return NotImplemented
def run(self) ‑> None
Expand source code
def run(self) -> 'None':
        if self.original_scene is None:
                raise RuntimeError("original_scene is not set")
        if self.working_scene is None:
                raise RuntimeError("working_scene is not set")
        
        self._check_originals()
        _log.info('Instantiating {0} objects from scene {1} to {2}... '.format(
                len(self.originals), repr(self.original_scene.name), repr(self.working_scene.name)))
        self._register_original_names()
        _bpy.context.window.scene = self.working_scene
        self._put_originals_on_working()
        self._duplicate()
        self._unlink_originals_from_working()
        self._rename_copies()
        
        if self.instantiate_collections:
                self._instantiate_collections()
                
        self._make_single_user()
        self._convert_curves_to_meshes()
        
        if self.instantiate_material_slots:
                self._instantiate_material_slots()

        if self.apply_modifiers:
                self._apply_modifiers()
        
        if self.apply_scales:
                _log.info('Applying scales...')
                _commons.select_set_all(self.copies, True)
                _bpy.ops.object.transform_apply(location=False, rotation=False, scale=self.apply_scales, properties=False)
                _commons.select_set_all(self.copies, False)
                _log.info('Applied scales.')
        
        invalids = list(obj for obj in self.copies if obj.name not in self.working_scene.collection.objects)
        if len(invalids) > 0:
                _log.info("Discarding {0} invalid objects...".format(len(invalids)))
                for invalid in invalids:
                        self.copies.discard(invalid)

        self._clean_original_names()
        
        _log.info("Instantiation done: {0} original -> {1} copies.".format(len(self.originals), len(self.copies)))