Blend Shape Anim (python)

from functools import partial

import Atoms
import AtomsCore
import os
from Atoms.ui.utils.qthandler import QtWidgets, QtCore, QtGui

from Atoms.images import get_image_path
from Atoms.ui.utils.qt import build_layout, ORIENTATION
from Atoms.ui.components.collapsiblebox import CollapsibleBox
from Atoms.ui.components.modules import BaseModuleWidget
from Atoms.ui.factory import MODULE_WIDGETS_FACTORY

ADD_ICON = QtGui.QIcon(get_image_path('add.png'))
CLEAR_ICON = QtGui.QIcon(get_image_path('clear.png'))
REMOVE_ICON = QtGui.QIcon(get_image_path('remove.png'))


class BlendShapesAnimBoxWidget(CollapsibleBox):
    def __init__(self, host_bridge, module_name, parent):
        super(BlendShapesAnimBoxWidget, self, ).__init__(parent)

        self._module_name = module_name
        self._host_bridge = host_bridge

        self._layout = build_layout(ORIENTATION.VERTICAL, margins=4,
                                    alignment=QtCore.Qt.AlignTop)
        self.set_content_layout(self._layout)
        self.set_collapsed(True)

        self._build_content_widgets()
        self._build_content_signals()

        self._update_tree()

    def _build_content_widgets(self):
        h_layout = build_layout(ORIENTATION.HORIZONTAL, margins=0)
        self._add_button = QtWidgets.QPushButton(ADD_ICON, "Add", self)
        self._clear_button = QtWidgets.QPushButton(CLEAR_ICON, "Clear", self)
        self._clear_button.setFixedWidth(65)
        h_layout.addWidget(self._add_button)
        h_layout.addWidget(self._clear_button)

        self._layout.addLayout(h_layout)

        self._tree = QtWidgets.QTreeWidget()
        style = """QTreeView::item {height:20; border: 1px solid #2B2B2B;
                        border-right-color:transparent;
                        border-left: 0px solid #2B2B2B;}"""
        self._tree.setStyleSheet(style)
        self._tree.setUniformRowHeights(True)
        self._tree.setIndentation(0)
        self._tree.setColumnCount(7)
        self._tree.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
        self._tree.setFocusPolicy(QtCore.Qt.NoFocus)
        #self._tree.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self._tree.setHeaderLabels(["","Agent Type", "Geo", "Metadata",
                                    "Anim file", "Anim type",""])

        header = self._tree.header()
        header.setStretchLastSection(False)
        header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
        header.resizeSection(0, 20)
        header.resizeSection(6, 20)
        self._layout.addWidget(self._tree)

    def _build_content_signals(self):
        self._add_button.clicked.connect(self._add_button_clicked)
        self._clear_button.clicked.connect(self._clear_button_clicked)

    def _add_button_clicked(self):
        count = self._tree.topLevelItemCount()
        index = 0
        if count > 0:
            index = int(self._tree.topLevelItem(count - 1).text(0)) + 1

        self._host_bridge.add_index_to_array_metadata(self._module_name,
                                                      "animData", index, "")
        self._add_item(index)

    def _add_item(self, index, values=[]):
        twi = QtWidgets.QTreeWidgetItem(self._tree)
        twi.setText(0, str(index))
        self._tree.addTopLevelItem(twi)

        for i in range(5):
            vn = QtWidgets.QLineEdit(self._tree)
            if i < len(values):
                vn.setText(values[i])
            vn.editingFinished.connect(partial(self._value_changed, twi))
            self._tree.setItemWidget(twi, i + 1, vn)
        
        b = QtWidgets.QPushButton(REMOVE_ICON, '', self._tree)
        b.clicked.connect(partial(self._remove_item, twi))
        b.setFixedSize(QtCore.QSize(20, 20))
        self._tree.setItemWidget(twi, 6, b)

        return twi

    def _value_changed(self, item):
        host_index = int(item.text(0))
        value = []
        for i in range(5):
            value.append(str(self._tree.itemWidget(item, i + 1).text()))
        type_str = AtomsCore.StringMetadata.staticTypeStr()
        value_str = ','.join(value)
        self._host_bridge.set_metadata_value(self._module_name, "animData",
                                             type_str, value_str,
                                             index=host_index)

    def _clear_button_clicked(self):
        self._clear_button.setFocus()
        self._tree.clear()
        indices = self._host_bridge.get_array_metadata_indices(
            self._module_name, "animData")
        for index in indices:
            self._host_bridge.remove_metadata_at_index(self._module_name,
                                                       "animData", index)

    def _remove_item(self, item):
        mindex = self._tree.indexFromItem(item)
        host_index = int(item.text(0))

        for i in range(1, 4):
            w = self._tree.itemWidget(item, i)
            w.setParent(None)

        self._tree.takeTopLevelItem(mindex.row())
        self._host_bridge.remove_metadata_at_index(self._module_name,
                                                   "animData", host_index)

    def _update_tree(self):
        self._tree.clear()

        type_str = AtomsCore.StringMetadata.staticTypeStr()
        type_double = AtomsCore.DoubleMetadata.staticTypeStr()

        indices = self._host_bridge.get_array_metadata_indices(
                                    self._module_name, "animData")
        for index in indices:
            ln = self._host_bridge.get_metadata_value(self._module_name,
                                                      "animData", type_str,
                                                      index=index)
            self._add_item(index, values=ln.split(','))


class BlendShapesAnimModuleWidget(BaseModuleWidget):
    def __init__(self, host_bridge, module_name, attributes_map, parent):
        super(BlendShapesAnimModuleWidget, self).__init__(host_bridge,
                                                          module_name,
                                                          attributes_map,
                                                          parent)

        self._anim_widgets = BlendShapesAnimBoxWidget(host_bridge, module_name,
                                                      self)
        self.add_widget("anim data", self._anim_widgets)

    @staticmethod
    def excluded_metadatas_from_automatic_build():
        return ["animData"]


class BlendShapesAnimModule(Atoms.BehaviourModule):
    def __init__(self):
        self.anim_table = {}
        Atoms.BehaviourModule.__init__(self)
        self.addAttribute("enable", AtomsCore.BoolMetadata(True), True)
        self.addAttribute("animData", AtomsCore.StringArrayMetadata())

    def initSimulation(self, agentGroup):
        #Called at the first frame of the simulation
        animData = self.attributes()["animData"].value()

        self.anim_table.clear()
        for clip in animData:
            data = clip.replace(" ","").split(',')
            if len(data) < 4:
                continue

            agent_type = data[0]
            mesh_name = data[1]
            metadata_trigger = data[2]
            anim_file = data[3]
            property = "loop"
            if len(data) > 4:
                property = data[4]

            #load the anim clip
            if not os.path.exists(anim_file):
                continue

            ark = AtomsCore.Archive()
            if not ark.readFromFile(anim_file):
                continue

            if not agent_type in self.anim_table:
                self.anim_table[agent_type] = {}

            if not mesh_name in self.anim_table[agent_type]:
                self.anim_table[agent_type][mesh_name] = {}

            curves_data = AtomsCore.MapMetadata()
            if not curves_data.deserialise(ark):
                continue

            weights = []
            for i, target in enumerate(curves_data["targets"].value()):
                weights.append(curves_data["weights"][target].value())
            data_dict = {"property": property,
                         "metadata": metadata_trigger,
                         "range": (curves_data["startFrame"].value(),
                                   curves_data["endFrame"].value()),
                         "weights": weights}

            self.anim_table[agent_type][mesh_name] = data_dict

    def initFrame(self, agents, agentGroup):
        #Called at the begin of each frame after agentsCreated
        is_enabled = self.attributes()["enable"].value()
        enable_override = self.attributes()["enable_override"]
        for agent in agents:
            enabled = is_enabled
            agent_id = agent.metadata()["groupId"].value()
            a_id_str = str(agent_id)
            if a_id_str in enable_override:
                enabled = self.attributes()["enable_override"][a_id_str].value()

            if not enabled:
                continue

            agent_type_name = agent.agentType().name()
            if not agent_type_name in self.anim_table:
                continue

            for mesh in self.anim_table[agent_type_name]:
                mesh_table = self.anim_table[agent_type_name][mesh]
                trigger_meta_name = mesh_table["metadata"]
                if not trigger_meta_name in agent.metadata():
                    continue

                trigger_value = agent.metadata()[trigger_meta_name].value()

                mesh_anim = mesh + "_anim"
                if not mesh_anim in agent.metadata():
                    agent.metadata()[mesh_anim] = AtomsCore.IntMetadata(-1)

                anim_frame = agent.metadata()[mesh_anim].value()

                if trigger_value < 0.00001:
                    if anim_frame == -1:
                        # the animation is not started and the trigger is at 0
                        # so we can skip
                        continue
                    else:
                        # we need to reset the animation
                        agent.metadata()[mesh_anim].set(-1)
                        for i in range(len(mesh_table["weights"])):
                            weight_meta_name = "%s_%s_%d" % (agent_type_name,
                                                             mesh, i)
                            if weight_meta_name in agent.metadata():
                                agent.metadata()[weight_meta_name].set(0.0)
                else:
                    anim_frame += 1
                    ws = mesh_table["weights"]
                    for i in range(len(ws)):
                        weight_meta_name = "%s_%s_%d" % (agent_type_name, mesh,
                                                         i)
                        if not weight_meta_name in agent.metadata():
                            dm = AtomsCore.DoubleMetadata(0.0)
                            agent.metadata()[weight_meta_name] = dm
                        start = mesh_table["range"][0]
                        end = mesh_table["range"][1]
                        if mesh_table["property"] == "loop":
                            anim_frame = anim_frame % (end - start)
                        else:
                            if anim_frame > (end - start):
                                anim_frame = end - start
                        agent.metadata()[weight_meta_name].set(trigger_value *
                                                            ws[i][anim_frame])
                    agent.metadata()[mesh_anim].set(anim_frame)


def register():
    Atoms.BehaviourModules.instance().registerBehaviourModule(
                                                        "PyBlendShapesAnim",
                                                        BlendShapesAnimModule,
                                                        True)

    MODULE_WIDGETS_FACTORY.register('PyBlendShapesAnim',
                                    BlendShapesAnimModuleWidget)

Copyright © 2017, Toolchefs LTD.