Skip to content

Commit

Permalink
✨ add support for publishing shapes
Browse files Browse the repository at this point in the history
  • Loading branch information
antirotor committed Jan 30, 2025
1 parent b33852c commit e137d3b
Show file tree
Hide file tree
Showing 10 changed files with 688 additions and 6 deletions.
4 changes: 2 additions & 2 deletions client/ayon_mocha/addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
import os
from typing import Any

from ayon_core.addon import AYONAddon, IHostAddon, IPluginPaths
from ayon_core.addon import AYONAddon, IHostAddon

from .version import __version__

MOCHA_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__))


class MochaAddon(AYONAddon, IHostAddon, IPluginPaths):
class MochaAddon(AYONAddon, IHostAddon):
"""BorisFX Mocha Pro addon for AYON."""

name = "mocha"
Expand Down
48 changes: 47 additions & 1 deletion client/ayon_mocha/api/lib.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
"""Library functions for the Ayon Mocha API."""
from __future__ import annotations

import dataclasses
import re
import subprocess
import sys
import tempfile
from hashlib import sha256
from pathlib import Path
from shutil import copyfile
from typing import TYPE_CHECKING, Any, Optional
from typing import TYPE_CHECKING, Any, Optional, Union

from mocha import get_mocha_exec_name, ui
from mocha.exporters import ShapeDataExporter, TrackingDataExporter
from mocha.project import Clip, Project
from qtpy.QtWidgets import QApplication

Expand All @@ -18,6 +22,35 @@
from qtpy import QtWidgets


EXTENSION_PATTERN = re.compile(r"(?P<name>.+)\(\*\.(?P<ext>\w+)\)")


"""
These dataclasses are here because they
cannot be defined directly in pyblish plugins.
There seems to be an issue (at least in python 3.7)
with dataclass checking for __module__ in class and
that one is missing in discovered pyblish
plugin classes.
"""
@dataclasses.dataclass
class ExporterInfo:
"""Exporter information."""
id: str
label: str
exporter: Union[TrackingDataExporter, ShapeDataExporter]


@dataclasses.dataclass
class ExporterProcessInfo:
"""Exporter process information."""
mocha_python_path: Path
mocha_exporter_path: Path
current_project_path: Path
staging_dir: Path
options: dict[str, bool]


def get_main_window() -> QtWidgets.QWidget:
"""Get the main window of the application.
Expand All @@ -26,6 +59,7 @@ def get_main_window() -> QtWidgets.QWidget:
"""
return ui.get_widgets()["MainWindow"]


def update_ui() -> None:
"""Update the UI."""
QApplication.processEvents()
Expand Down Expand Up @@ -131,3 +165,15 @@ def create_empty_project(
clip_path = copy_placeholder_clip(project_path.parent)
clip = Clip(clip_path.as_posix())
return Project(clip)


def get_shape_exporters() -> list[ExporterInfo]:
"""Return all registered shape exporters as a list."""
return [
ExporterInfo(
id=sha256(k.encode()).hexdigest(),
label=k,
exporter=v)
for k, v in sorted(
ShapeDataExporter.registered_exporters().items())
]
26 changes: 23 additions & 3 deletions client/ayon_mocha/api/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@

from ayon_mocha.api.lib import update_ui

from .lib import create_empy_project, get_main_window
from .lib import create_empty_project, get_main_window
from .workio import current_file, file_extensions, open_file, save_file

if TYPE_CHECKING:
from qtpy import QtWidgets

log = logging.getLogger("ayon_mocha")
HOST_DIR = Path(__file__).resolve().parent
HOST_DIR = Path(__file__).resolve().parent.parent
PLUGINS_DIR = HOST_DIR / "plugins"
PUBLISH_PATH = PLUGINS_DIR / "publish"
LOAD_PATH = PLUGINS_DIR / "load"
Expand All @@ -57,6 +57,26 @@
MOCHA_INSTANCES_KEY = "publish_instances"
MOCHA_CONTAINERS_KEY = "containers"

SHAPE_EXPORTERS_REPRESENTATION_NAME_MAPPING = {
"Adobe After Effects Mask Data (*.shape4ae)": "AfxMask",
"Adobe Premiere shape data (*.xml)": "PremiereShape",
"BlackMagic Fusion 19+ MultiPoly shapes (*.comp)": "FusionMultiPoly",
"BlackMagic Fusion shapes (*.comp)": "FusionShapes",
"Combustion GMask Script (*.gmask)": "CombustionGMask",
"Flame GMask Script (*.gmask)": "FlameGMask",
"Flame Tracer [Basic] (*.mask)": "FlameTracerBasic",
"Flame Tracer [Shape + Axis] (*.mask)": "FlameTracerShapeAxis",
"HitFilm [Transform & Shape] (*.hfcs)": "HitFilmTransformShape",
"Mocha shape data for Final Cut (*.xml)": "MochaShapeFinalCut",
"MochaBlend shape data (*.txt)": "MochaBlend",
"Nuke Roto [Basic] (*.nk)": "NuRotoBasic",
"Nuke RotoPaint [Basic] (*.nk)": "NukeRotoPaintBasic",
"Nuke SplineWarp (*.nk)": "NukeSplineWarp",
"Nuke v6.2+ Roto [Transform & Shape] (*.nk)": "NukeRotoTransformShape",
"Nuke v6.2+ RotoPaint [Transform & Shape] (*.nk)": "NukeRotoPaint",
"Shake Rotoshape (*.ssf)": "ShapeRotoshape",
"Silhouette shapes (*.fxs)": "SilhouetteShapes",
}

class AYONJSONEncoder(json.JSONEncoder):
"""Custom JSON encoder for dataclasses."""
Expand Down Expand Up @@ -424,7 +444,7 @@ def get_current_project(self) -> Project:
parent=get_main_window(),
)
self._uninitialized_project_warning_shown = True
return create_empy_project()
return create_empty_project()
return project


Expand Down
51 changes: 51 additions & 0 deletions client/ayon_mocha/plugins/create/create_shape_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Create shape data instance."""
from __future__ import annotations

from typing import TYPE_CHECKING

from ayon_core.lib import (
EnumDef,
UILabelDef,
UISeparatorDef,
)
from ayon_mocha.api.lib import get_shape_exporters
from ayon_mocha.api.plugin import MochaCreator

if TYPE_CHECKING:
from ayon_core.pipeline import CreatedInstance


class CreateShapeData(MochaCreator):
"""Create shape instance."""
identifier = "io.ayon.creators.mochapro.matteshapes"
label = "Shape Data"
description = __doc__
product_type = "matteshapes"
icon = "circle"

def get_attr_defs_for_instance(self, instance: CreatedInstance) -> list:
"""Get attribute definitions for instance."""
exporter_items = {ex.id: ex.label for ex in get_shape_exporters()}
layers = {
idx: layer.name
for idx, layer in enumerate(
self.create_context.host.get_current_project().layers)
} or {-1: "No layers"}

return [
EnumDef("layers",
label="Layers",
items=layers,
multiselection=True),
EnumDef("exporter",
label="Exporter format",
items=exporter_items, multiselection=True),
UISeparatorDef(),
UILabelDef(
"Exporter Options (not all are available in all exporters)"),
EnumDef("layer_mode", label="Layer mode",
items={
"selected": "Selected layers",
"all": "All layers"
}),
]
48 changes: 48 additions & 0 deletions client/ayon_mocha/plugins/publish/collect_instances.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Collect instances for publishing."""
from __future__ import annotations

from typing import TYPE_CHECKING, ClassVar

import pyblish.api

if TYPE_CHECKING:
from logging import Logger

class CollectInstances(pyblish.api.InstancePlugin):
"""Collect instances for publishing."""
label = "Collect Instances"
order = pyblish.api.CollectorOrder - 0.4
hosts: ClassVar[list[str]] = ["mochapro"]
log: Logger

def process(self, instance: pyblish.api.Instance) -> None:
"""Process the plugin."""
self.log.debug("Collecting data for %s", instance)

# Define nice instance label
instance_node = instance.data.get(
"transientData", {}).get("instance_node")
name = instance_node.label if instance_node else instance.name
label = f"{name} ({instance.data['folderPath']})"

# Set frame start handle and frame end handle if frame ranges are
# available
if "frameStart" in instance.data and "frameEnd" in instance.data:
# Enforce existence if handles
instance.data.setdefault("handleStart", 0)
instance.data.setdefault("handleEnd", 0)

# Compute frame start handle and end start handle
frame_start_handle = (
instance.data["frameStart"] - instance.data["handleStart"]
)
frame_end_handle = (
instance.data["frameEnd"] - instance.data["handleEnd"]
)
instance.data["frameStartHandle"] = frame_start_handle
instance.data["frameEndHandle"] = frame_end_handle

# Include frame range in label
label += f" [{int(frame_start_handle)}-{int(frame_end_handle)}]"

instance.data["label"] = label
59 changes: 59 additions & 0 deletions client/ayon_mocha/plugins/publish/collect_mocha_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Collect Mocha executable paths."""
from __future__ import annotations

import platform
from pathlib import Path
from typing import TYPE_CHECKING, ClassVar

import pyblish.api
from mocha import get_mocha_exec_name

if TYPE_CHECKING:
from logging import Logger

from mocha.project import Project

class CollectMochaPaths(pyblish.api.ContextPlugin):
"""Collect Mocha Pro project."""
order = pyblish.api.CollectorOrder - 0.45
label = "Collect Mocha Pro executables"
hosts: ClassVar[list[str]] = ["mochapro"]
log: Logger

def process(self, context: pyblish.api.Context) -> None:
"""Process the plugin."""
project: Project = context.data["project"]
self.log.info("Collected Mocha Pro project: %s", project)

mocha_executable_path = Path(get_mocha_exec_name("mochapro"))
context.data["mocha_executable_path"] = mocha_executable_path
mocha_install_dir = mocha_executable_path.parent.parent

if platform.system().lower() == "windows":
mocha_python_path = (
mocha_install_dir / "python" / "python.exe")
mocha_exporter_path = (
mocha_install_dir / "python" / "mochaexport.py")
elif platform.system().lower() == "darwin":
mocha_python_path = (
mocha_install_dir / "python3")
mocha_exporter_path = (
mocha_install_dir / "mochaexport.py")
elif platform.system().lower() == "linux":
mocha_python_path = (
mocha_install_dir / "python" / "bin" / "python3")
mocha_exporter_path = (
mocha_install_dir / "python" / "mochaexport.py")
else:
msg = f"Unsupported platform: {platform.system()}"
raise NotImplementedError(msg)

context.data["mocha_python_path"] = mocha_python_path
context.data["mocha_exporter_path"] = mocha_exporter_path

self.log.info("Collected Mocha Pro executable path: %s",
mocha_executable_path)
self.log.info("Collected Mocha Pro python executable path: %s",
mocha_python_path)
self.log.info("Collected Mocha Pro python export script path: %s",
mocha_exporter_path)
32 changes: 32 additions & 0 deletions client/ayon_mocha/plugins/publish/collect_mocha_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Collect the current Mocha Pro project."""
from __future__ import annotations

from typing import TYPE_CHECKING, ClassVar

import pyblish.api
from mocha.project import get_current_project

if TYPE_CHECKING:
from logging import Logger


class CollectMochaProject(pyblish.api.ContextPlugin):
"""Inject the current working file into context.
Foo batr baz.
"""

order = pyblish.api.CollectorOrder - 0.5
label = "Collect Mocha Pro Project"
hosts: ClassVar[list[str]] = ["mochapro"]
log: Logger

def process(self, context: pyblish.api.Context) -> None:
"""Inject the current working file."""
context.data["project"] = get_current_project()
current_file = context.data["project"].project_file
context.data["currentFile"] = current_file
if not current_file:
self.log.warning(
"Current file is not saved. Save the file before continuing."
)
Loading

0 comments on commit e137d3b

Please sign in to comment.