mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-02 00:44:52 +01:00
Merge branch 'develop' into enhancement/maya_review
# Conflicts: # openpype/hosts/maya/plugins/create/create_review.py # openpype/hosts/maya/plugins/publish/collect_review.py
This commit is contained in:
commit
61ea9ad604
454 changed files with 13472 additions and 6737 deletions
15
.github/pr-branch-labeler.yml
vendored
Normal file
15
.github/pr-branch-labeler.yml
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Apply label "feature" if head matches "feature/*"
|
||||
'type: feature':
|
||||
head: "feature/*"
|
||||
|
||||
# Apply label "feature" if head matches "feature/*"
|
||||
'type: enhancement':
|
||||
head: "enhancement/*"
|
||||
|
||||
# Apply label "bugfix" if head matches one of "bugfix/*" or "hotfix/*"
|
||||
'type: bug':
|
||||
head: ["bugfix/*", "hotfix/*"]
|
||||
|
||||
# Apply label "release" if base matches "release/*"
|
||||
'Bump Minor':
|
||||
base: "release/next-minor"
|
||||
102
.github/pr-glob-labeler.yml
vendored
Normal file
102
.github/pr-glob-labeler.yml
vendored
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Add type: unittest label if any changes in tests folders
|
||||
'type: unittest':
|
||||
- '*/*tests*/**/*'
|
||||
|
||||
# any changes in documentation structure
|
||||
'type: documentation':
|
||||
- '*/**/*website*/**/*'
|
||||
- '*/**/*docs*/**/*'
|
||||
|
||||
# hosts triage
|
||||
'host: Nuke':
|
||||
- '*/**/*nuke*'
|
||||
- '*/**/*nuke*/**/*'
|
||||
|
||||
'host: Photoshop':
|
||||
- '*/**/*photoshop*'
|
||||
- '*/**/*photoshop*/**/*'
|
||||
|
||||
'host: Harmony':
|
||||
- '*/**/*harmony*'
|
||||
- '*/**/*harmony*/**/*'
|
||||
|
||||
'host: UE':
|
||||
- '*/**/*unreal*'
|
||||
- '*/**/*unreal*/**/*'
|
||||
|
||||
'host: Houdini':
|
||||
- '*/**/*houdini*'
|
||||
- '*/**/*houdini*/**/*'
|
||||
|
||||
'host: Maya':
|
||||
- '*/**/*maya*'
|
||||
- '*/**/*maya*/**/*'
|
||||
|
||||
'host: Resolve':
|
||||
- '*/**/*resolve*'
|
||||
- '*/**/*resolve*/**/*'
|
||||
|
||||
'host: Blender':
|
||||
- '*/**/*blender*'
|
||||
- '*/**/*blender*/**/*'
|
||||
|
||||
'host: Hiero':
|
||||
- '*/**/*hiero*'
|
||||
- '*/**/*hiero*/**/*'
|
||||
|
||||
'host: Fusion':
|
||||
- '*/**/*fusion*'
|
||||
- '*/**/*fusion*/**/*'
|
||||
|
||||
'host: Flame':
|
||||
- '*/**/*flame*'
|
||||
- '*/**/*flame*/**/*'
|
||||
|
||||
'host: TrayPublisher':
|
||||
- '*/**/*traypublisher*'
|
||||
- '*/**/*traypublisher*/**/*'
|
||||
|
||||
'host: 3dsmax':
|
||||
- '*/**/*max*'
|
||||
- '*/**/*max*/**/*'
|
||||
|
||||
'host: TV Paint':
|
||||
- '*/**/*tvpaint*'
|
||||
- '*/**/*tvpaint*/**/*'
|
||||
|
||||
'host: CelAction':
|
||||
- '*/**/*celaction*'
|
||||
- '*/**/*celaction*/**/*'
|
||||
|
||||
'host: After Effects':
|
||||
- '*/**/*aftereffects*'
|
||||
- '*/**/*aftereffects*/**/*'
|
||||
|
||||
'host: Substance Painter':
|
||||
- '*/**/*substancepainter*'
|
||||
- '*/**/*substancepainter*/**/*'
|
||||
|
||||
# modules triage
|
||||
'module: Deadline':
|
||||
- '*/**/*deadline*'
|
||||
- '*/**/*deadline*/**/*'
|
||||
|
||||
'module: RoyalRender':
|
||||
- '*/**/*royalrender*'
|
||||
- '*/**/*royalrender*/**/*'
|
||||
|
||||
'module: Sitesync':
|
||||
- '*/**/*sync_server*'
|
||||
- '*/**/*sync_server*/**/*'
|
||||
|
||||
'module: Ftrack':
|
||||
- '*/**/*ftrack*'
|
||||
- '*/**/*ftrack*/**/*'
|
||||
|
||||
'module: Shotgrid':
|
||||
- '*/**/*shotgrid*'
|
||||
- '*/**/*shotgrid*/**/*'
|
||||
|
||||
'module: Kitsu':
|
||||
- '*/**/*kitsu*'
|
||||
- '*/**/*kitsu*/**/*'
|
||||
15
.github/pull_request_template.md
vendored
15
.github/pull_request_template.md
vendored
|
|
@ -1,16 +1,9 @@
|
|||
## Brief description
|
||||
First sentence is brief description.
|
||||
|
||||
## Description
|
||||
Next paragraf is more elaborate text with more info. This will be displayed for example in collapsed form under the first sentence in a changelog.
|
||||
## Changelog Description
|
||||
Paragraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation.
|
||||
|
||||
## Additional info
|
||||
The rest will be ignored in changelog and should contain any additional
|
||||
technical information.
|
||||
|
||||
## Documentation (add _"type: documentation"_ label)
|
||||
[feature_documentation](future_url_after_it_will_be_merged)
|
||||
Paragraphs of text giving context of additional technical information or code examples.
|
||||
|
||||
## Testing notes:
|
||||
1. start with this step
|
||||
2. follow this step
|
||||
2. follow this step
|
||||
|
|
|
|||
99
.github/workflows/project_actions.yml
vendored
Normal file
99
.github/workflows/project_actions.yml
vendored
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
name: project-actions
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
pr_review_started:
|
||||
name: pr_review_started
|
||||
runs-on: ubuntu-latest
|
||||
# -----------------------------
|
||||
# conditions are:
|
||||
# - PR issue comment which is not form Ynbot
|
||||
# - PR review comment which is not Hound (or any other bot)
|
||||
# - PR review submitted which is not from Hound (or any other bot) and is not 'Changes requested'
|
||||
# -----------------------------
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && github.event.comment.user.id != 82967070) ||
|
||||
(github.event_name == 'pull_request_review_comment' && github.event.comment.user.type != 'Bot') ||
|
||||
(github.event_name == 'pull_request_review' && github.event.review.state != 'changes_requested' && github.event.review.user.type != 'Bot')
|
||||
steps:
|
||||
- name: Move PR to 'Review In Progress'
|
||||
uses: leonsteinhaeuser/project-beta-automations@v2.1.0
|
||||
with:
|
||||
gh_token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
organization: ynput
|
||||
project_id: 11
|
||||
resource_node_id: ${{ github.event.pull_request.node_id || github.event.issue.node_id }}
|
||||
status_value: Review In Progress
|
||||
|
||||
# pr_review_requested:
|
||||
# name: pr_review_requested
|
||||
# runs-on: ubuntu-latest
|
||||
# if: github.event_name == 'pull_request_review' && github.event.review.state == 'changes_requested'
|
||||
# steps:
|
||||
# - name: Move PR to 'Change Requested'
|
||||
# uses: leonsteinhaeuser/project-beta-automations@v2.1.0
|
||||
# with:
|
||||
# gh_token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
# organization: ynput
|
||||
# project_id: 11
|
||||
# resource_node_id: ${{ github.event.pull_request.node_id }}
|
||||
# status_value: Change Requested
|
||||
|
||||
size-label:
|
||||
name: pr_size_label
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && github.event.action == 'assigned') ||
|
||||
(github.event_name == 'pull_request' && github.event.action == 'opened')
|
||||
|
||||
steps:
|
||||
- name: Add size label
|
||||
uses: "pascalgn/size-label-action@v0.4.3"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}"
|
||||
IGNORED: ".gitignore\n*.md\n*.json"
|
||||
with:
|
||||
sizes: >
|
||||
{
|
||||
"0": "XS",
|
||||
"100": "S",
|
||||
"500": "M",
|
||||
"1000": "L",
|
||||
"1500": "XL",
|
||||
"2500": "XXL"
|
||||
}
|
||||
|
||||
label_prs_branch:
|
||||
name: pr_branch_label
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && github.event.action == 'assigned') ||
|
||||
(github.event_name == 'pull_request' && github.event.action == 'opened')
|
||||
steps:
|
||||
- name: Label PRs - Branch name detection
|
||||
uses: ffittschen/pr-branch-labeler@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
|
||||
label_prs_globe:
|
||||
name: pr_globe_label
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && github.event.action == 'assigned') ||
|
||||
(github.event_name == 'pull_request' && github.event.action == 'opened')
|
||||
steps:
|
||||
- name: Label PRs - Globe detection
|
||||
uses: actions/labeler@v4.0.3
|
||||
with:
|
||||
repo-token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
configuration-path: ".github/pr-glob-labeler.yml"
|
||||
sync-labels: false
|
||||
77
ARCHITECTURE.md
Normal file
77
ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# Architecture
|
||||
|
||||
OpenPype is a monolithic Python project that bundles several parts, this document will try to give a birds eye overview of the project and, to a certain degree, each of the sub-projects.
|
||||
The current file structure looks like this:
|
||||
|
||||
```
|
||||
.
|
||||
├── common - Code in this folder is backend portion of Addon distribution logic for v4 server.
|
||||
├── docs - Documentation of the source code.
|
||||
├── igniter - The OpenPype bootstrapper, deals with running version resolution and setting up the connection to the mongodb.
|
||||
├── openpype - The actual OpenPype core package.
|
||||
├── schema - Collection of JSON files describing schematics of objects. This follows Avalon's convention.
|
||||
├── tests - Integration and unit tests.
|
||||
├── tools - Conveninece scripts to perform common actions (in both bash and ps1).
|
||||
├── vendor - When using the igniter, it deploys third party tools in here, such as ffmpeg.
|
||||
└── website - Source files for https://openpype.io/ which is Docusaursus (https://docusaurus.io/).
|
||||
```
|
||||
|
||||
The core functionality of the pipeline can be found in `igniter` and `openpype`, which in turn rely on the `schema` files, whenever you build (or download a pre-built) version of OpenPype, these two are bundled in there, and `Igniter` is the entry point.
|
||||
|
||||
|
||||
## Igniter
|
||||
|
||||
It's the setup and update tool for OpenPype, unless you want to package `openpype` separately and deal with all the config manually, this will most likely be your entry point.
|
||||
|
||||
```
|
||||
igniter/
|
||||
├── bootstrap_repos.py - Module that will find or install OpenPype versions in the system.
|
||||
├── __init__.py - Igniter entry point.
|
||||
├── install_dialog.py- Show dialog for choosing central pype repository.
|
||||
├── install_thread.py - Threading helpers for the install process.
|
||||
├── __main__.py - Like `__init__.py` ?
|
||||
├── message_dialog.py - Qt Dialog with a message and "Ok" button.
|
||||
├── nice_progress_bar.py - Fancy Qt progress bar.
|
||||
├── splash.txt - ASCII art for the terminal installer.
|
||||
├── stylesheet.css - Installer Qt styles.
|
||||
├── terminal_splash.py - Terminal installer animation, relies in `splash.txt`.
|
||||
├── tools.py - Collection of methods that don't fit in other modules.
|
||||
├── update_thread.py - Threading helper to update existing OpenPype installs.
|
||||
├── update_window.py - Qt UI to update OpenPype installs.
|
||||
├── user_settings.py - Interface for the OpenPype user settings.
|
||||
└── version.py - Igniter's version number.
|
||||
```
|
||||
|
||||
## OpenPype
|
||||
|
||||
This is the main package of the OpenPype logic, it could be loosely described as a combination of [Avalon](https://getavalon.github.io), [Pyblish](https://pyblish.com/) and glue around those with custom OpenPype only elements, things are in progress of being moved around to better prepare for V4, which will be released under a new name AYON.
|
||||
|
||||
```
|
||||
openpype/
|
||||
├── client - Interface for the MongoDB.
|
||||
├── hooks - Hooks to be executed on certain OpenPype Applications defined in `openpype.lib.applications`.
|
||||
├── host - Base class for the different hosts.
|
||||
├── hosts - Integration with the different DCCs (hosts) using the `host` base class.
|
||||
├── lib - Libraries that stitch together the package, some have been moved into other parts.
|
||||
├── modules - OpenPype modules should contain separated logic of specific kind of implementation, such as Ftrack connection and its python API.
|
||||
├── pipeline - Core of the OpenPype pipeline, handles creation of data, publishing, etc.
|
||||
├── plugins - Global/core plugins for loader and publisher tool.
|
||||
├── resources - Icons, fonts, etc.
|
||||
├── scripts - Loose scipts that get run by tools/publishers.
|
||||
├── settings - OpenPype settings interface.
|
||||
├── style - Qt styling.
|
||||
├── tests - Unit tests.
|
||||
├── tools - Core tools, check out https://openpype.io/docs/artist_tools.
|
||||
├── vendor - Vendoring of needed required Python packes.
|
||||
├── widgets - Common re-usable Qt Widgets.
|
||||
├── action.py - LEGACY: Lives now in `openpype.pipeline.publish.action` Pyblish actions.
|
||||
├── cli.py - Command line interface, leverages `click`.
|
||||
├── __init__.py - Sets two constants.
|
||||
├── __main__.py - Entry point, calls the `cli.py`
|
||||
├── plugin.py - Pyblish plugins.
|
||||
├── pype_commands.py - Implementation of OpenPype commands.
|
||||
└── version.py - Current version number.
|
||||
```
|
||||
|
||||
|
||||
|
||||
1774
CHANGELOG.md
1774
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
|
@ -367,11 +367,15 @@ def run(script):
|
|||
"--timeout",
|
||||
help="Provide specific timeout value for test case",
|
||||
default=None)
|
||||
@click.option("-so",
|
||||
"--setup_only",
|
||||
help="Only create dbs, do not run tests",
|
||||
default=None)
|
||||
def runtests(folder, mark, pyargs, test_data_folder, persist, app_variant,
|
||||
timeout):
|
||||
timeout, setup_only):
|
||||
"""Run all automatic tests after proper initialization via start.py"""
|
||||
PypeCommands().run_tests(folder, mark, pyargs, test_data_folder,
|
||||
persist, app_variant, timeout)
|
||||
persist, app_variant, timeout, setup_only)
|
||||
|
||||
|
||||
@main.command()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Goal is that most of functions here are called on (or with) an object
|
||||
that has project name as a context (e.g. on 'ProjectEntity'?).
|
||||
|
||||
+ We will need more specific functions doing wery specific queires really fast.
|
||||
+ We will need more specific functions doing very specific queries really fast.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
|
@ -193,7 +193,7 @@ def _get_assets(
|
|||
be found.
|
||||
asset_names (Iterable[str]): Name assets that should be found.
|
||||
parent_ids (Iterable[Union[str, ObjectId]]): Parent asset ids.
|
||||
standard (bool): Query standart assets (type 'asset').
|
||||
standard (bool): Query standard assets (type 'asset').
|
||||
archived (bool): Query archived assets (type 'archived_asset').
|
||||
fields (Iterable[str]): Fields that should be returned. All fields are
|
||||
returned if 'None' is passed.
|
||||
|
|
@ -1185,7 +1185,7 @@ def get_representations(
|
|||
standard=True,
|
||||
fields=None
|
||||
):
|
||||
"""Representaion entities data from one project filtered by filters.
|
||||
"""Representation entities data from one project filtered by filters.
|
||||
|
||||
Filters are additive (all conditions must pass to return subset).
|
||||
|
||||
|
|
@ -1231,7 +1231,7 @@ def get_archived_representations(
|
|||
names_by_version_ids=None,
|
||||
fields=None
|
||||
):
|
||||
"""Archived representaion entities data from project with applied filters.
|
||||
"""Archived representation entities data from project with applied filters.
|
||||
|
||||
Filters are additive (all conditions must pass to return subset).
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
## Reason
|
||||
Preparation for OpenPype v4 server. Goal is to remove direct mongo calls in code to prepare a little bit for different source of data for code before. To start think about database calls less as mongo calls but more universally. To do so was implemented simple wrapper around database calls to not use pymongo specific code.
|
||||
|
||||
Current goal is not to make universal database model which can be easily replaced with any different source of data but to make it close as possible. Current implementation of OpenPype is too tighly connected to pymongo and it's abilities so we're trying to get closer with long term changes that can be used even in current state.
|
||||
Current goal is not to make universal database model which can be easily replaced with any different source of data but to make it close as possible. Current implementation of OpenPype is too tightly connected to pymongo and it's abilities so we're trying to get closer with long term changes that can be used even in current state.
|
||||
|
||||
## Queries
|
||||
Query functions don't use full potential of mongo queries like very specific queries based on subdictionaries or unknown structures. We try to avoid these calls as much as possible because they'll probably won't be available in future. If it's really necessary a new function can be added but only if it's reasonable for overall logic. All query functions were moved to `~/client/entities.py`. Each function has arguments with available filters and possible reduce of returned keys for each entity.
|
||||
|
|
@ -14,7 +14,7 @@ Changes are a little bit complicated. Mongo has many options how update can happ
|
|||
Create operations expect already prepared document data, for that are prepared functions creating skeletal structures of documents (do not fill all required data), except `_id` all data should be right. Existence of entity is not validated so if the same creation operation is send n times it will create the entity n times which can cause issues.
|
||||
|
||||
### Update
|
||||
Update operation require entity id and keys that should be changed, update dictionary must have {"key": value}. If value should be set in nested dictionary the key must have also all subkeys joined with dot `.` (e.g. `{"data": {"fps": 25}}` -> `{"data.fps": 25}`). To simplify update dictionaries were prepared functions which does that for you, their name has template `prepare_<entity type>_update_data` - they work on comparison of previous document and new document. If there is missing function for requested entity type it is because we didn't need it yet and require implementaion.
|
||||
Update operation require entity id and keys that should be changed, update dictionary must have {"key": value}. If value should be set in nested dictionary the key must have also all subkeys joined with dot `.` (e.g. `{"data": {"fps": 25}}` -> `{"data.fps": 25}`). To simplify update dictionaries were prepared functions which does that for you, their name has template `prepare_<entity type>_update_data` - they work on comparison of previous document and new document. If there is missing function for requested entity type it is because we didn't need it yet and require implementation.
|
||||
|
||||
### Delete
|
||||
Delete operation need entity id. Entity will be deleted from mongo.
|
||||
|
|
|
|||
|
|
@ -368,7 +368,7 @@ def prepare_workfile_info_update_data(old_doc, new_doc, replace=True):
|
|||
class AbstractOperation(object):
|
||||
"""Base operation class.
|
||||
|
||||
Opration represent a call into database. The call can create, change or
|
||||
Operation represent a call into database. The call can create, change or
|
||||
remove data.
|
||||
|
||||
Args:
|
||||
|
|
@ -409,7 +409,7 @@ class AbstractOperation(object):
|
|||
pass
|
||||
|
||||
def to_data(self):
|
||||
"""Convert opration to data that can be converted to json or others.
|
||||
"""Convert operation to data that can be converted to json or others.
|
||||
|
||||
Warning:
|
||||
Current state returns ObjectId objects which cannot be parsed by
|
||||
|
|
@ -428,7 +428,7 @@ class AbstractOperation(object):
|
|||
|
||||
|
||||
class CreateOperation(AbstractOperation):
|
||||
"""Opeartion to create an entity.
|
||||
"""Operation to create an entity.
|
||||
|
||||
Args:
|
||||
project_name (str): On which project operation will happen.
|
||||
|
|
@ -485,7 +485,7 @@ class CreateOperation(AbstractOperation):
|
|||
|
||||
|
||||
class UpdateOperation(AbstractOperation):
|
||||
"""Opeartion to update an entity.
|
||||
"""Operation to update an entity.
|
||||
|
||||
Args:
|
||||
project_name (str): On which project operation will happen.
|
||||
|
|
@ -552,7 +552,7 @@ class UpdateOperation(AbstractOperation):
|
|||
|
||||
|
||||
class DeleteOperation(AbstractOperation):
|
||||
"""Opeartion to delete an entity.
|
||||
"""Operation to delete an entity.
|
||||
|
||||
Args:
|
||||
project_name (str): On which project operation will happen.
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
|
|||
# Execute after workfile template copy
|
||||
order = 10
|
||||
app_groups = [
|
||||
"3dsmax",
|
||||
"maya",
|
||||
"nuke",
|
||||
"nukex",
|
||||
|
|
|
|||
|
|
@ -3,10 +3,13 @@ from openpype.lib import PreLaunchHook
|
|||
from openpype.pipeline.workfile import create_workdir_extra_folders
|
||||
|
||||
|
||||
class AddLastWorkfileToLaunchArgs(PreLaunchHook):
|
||||
"""Add last workfile path to launch arguments.
|
||||
class CreateWorkdirExtraFolders(PreLaunchHook):
|
||||
"""Create extra folders for the work directory.
|
||||
|
||||
Based on setting `project_settings/global/tools/Workfiles/extra_folders`
|
||||
profile filtering will decide whether extra folders need to be created in
|
||||
the work directory.
|
||||
|
||||
This is not possible to do for all applications the same way.
|
||||
"""
|
||||
|
||||
# Execute after workfile template copy
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ class LaunchFoundryAppsWindows(PreLaunchHook):
|
|||
|
||||
Nuke is executed "like" python process so it is required to pass
|
||||
`CREATE_NEW_CONSOLE` flag on windows to trigger creation of new console.
|
||||
At the same time the newly created console won't create it's own stdout
|
||||
At the same time the newly created console won't create its own stdout
|
||||
and stderr handlers so they should not be redirected to DEVNULL.
|
||||
"""
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ class LaunchFoundryAppsWindows(PreLaunchHook):
|
|||
|
||||
def execute(self):
|
||||
# Change `creationflags` to CREATE_NEW_CONSOLE
|
||||
# - on Windows will nuke create new window using it's console
|
||||
# - on Windows nuke will create new window using its console
|
||||
# Set `stdout` and `stderr` to None so new created console does not
|
||||
# have redirected output to DEVNULL in build
|
||||
self.launch_context.kwargs.update({
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Idea for current dirmap implementation was used from Maya where is possible to
|
||||
enter source and destination roots and maya will try each found source
|
||||
in referenced file replace with each destionation paths. First path which
|
||||
in referenced file replace with each destination paths. First path which
|
||||
exists is used.
|
||||
"""
|
||||
|
||||
|
|
@ -39,7 +39,6 @@ class HostDirmap(object):
|
|||
self._project_settings = project_settings
|
||||
self._sync_module = sync_module # to limit reinit of Modules
|
||||
self._log = None
|
||||
self._mapping = None # cache mapping
|
||||
|
||||
@property
|
||||
def sync_module(self):
|
||||
|
|
@ -70,29 +69,28 @@ class HostDirmap(object):
|
|||
"""Run host dependent remapping from source_path to destination_path"""
|
||||
pass
|
||||
|
||||
def process_dirmap(self):
|
||||
def process_dirmap(self, mapping=None):
|
||||
# type: (dict) -> None
|
||||
"""Go through all paths in Settings and set them using `dirmap`.
|
||||
|
||||
If artists has Site Sync enabled, take dirmap mapping directly from
|
||||
Local Settings when artist is syncing workfile locally.
|
||||
|
||||
Args:
|
||||
project_settings (dict): Settings for current project.
|
||||
"""
|
||||
|
||||
if not self._mapping:
|
||||
self._mapping = self.get_mappings(self.project_settings)
|
||||
if not self._mapping:
|
||||
if not mapping:
|
||||
mapping = self.get_mappings()
|
||||
if not mapping:
|
||||
return
|
||||
|
||||
self.log.info("Processing directory mapping ...")
|
||||
self.on_enable_dirmap()
|
||||
self.log.info("mapping:: {}".format(self._mapping))
|
||||
|
||||
for k, sp in enumerate(self._mapping["source-path"]):
|
||||
dst = self._mapping["destination-path"][k]
|
||||
for k, sp in enumerate(mapping["source-path"]):
|
||||
dst = mapping["destination-path"][k]
|
||||
try:
|
||||
# add trailing slash if missing
|
||||
sp = os.path.join(sp, '')
|
||||
dst = os.path.join(dst, '')
|
||||
print("{} -> {}".format(sp, dst))
|
||||
self.dirmap_routine(sp, dst)
|
||||
except IndexError:
|
||||
|
|
@ -110,28 +108,24 @@ class HostDirmap(object):
|
|||
)
|
||||
continue
|
||||
|
||||
def get_mappings(self, project_settings):
|
||||
def get_mappings(self):
|
||||
"""Get translation from source-path to destination-path.
|
||||
|
||||
It checks if Site Sync is enabled and user chose to use local
|
||||
site, in that case configuration in Local Settings takes precedence
|
||||
"""
|
||||
|
||||
local_mapping = self._get_local_sync_dirmap(project_settings)
|
||||
dirmap_label = "{}-dirmap".format(self.host_name)
|
||||
if (
|
||||
not self.project_settings[self.host_name].get(dirmap_label)
|
||||
and not local_mapping
|
||||
):
|
||||
return {}
|
||||
mapping_settings = self.project_settings[self.host_name][dirmap_label]
|
||||
mapping_enabled = mapping_settings["enabled"] or bool(local_mapping)
|
||||
mapping_sett = self.project_settings[self.host_name].get(dirmap_label,
|
||||
{})
|
||||
local_mapping = self._get_local_sync_dirmap()
|
||||
mapping_enabled = mapping_sett.get("enabled") or bool(local_mapping)
|
||||
if not mapping_enabled:
|
||||
return {}
|
||||
|
||||
mapping = (
|
||||
local_mapping
|
||||
or mapping_settings["paths"]
|
||||
or mapping_sett["paths"]
|
||||
or {}
|
||||
)
|
||||
|
||||
|
|
@ -141,28 +135,27 @@ class HostDirmap(object):
|
|||
or not mapping.get("source-path")
|
||||
):
|
||||
return {}
|
||||
self.log.info("Processing directory mapping ...")
|
||||
self.log.info("mapping:: {}".format(mapping))
|
||||
return mapping
|
||||
|
||||
def _get_local_sync_dirmap(self, project_settings):
|
||||
def _get_local_sync_dirmap(self):
|
||||
"""
|
||||
Returns dirmap if synch to local project is enabled.
|
||||
|
||||
Only valid mapping is from roots of remote site to local site set
|
||||
in Local Settings.
|
||||
|
||||
Args:
|
||||
project_settings (dict)
|
||||
Returns:
|
||||
dict : { "source-path": [XXX], "destination-path": [YYYY]}
|
||||
"""
|
||||
project_name = os.getenv("AVALON_PROJECT")
|
||||
|
||||
mapping = {}
|
||||
|
||||
if not project_settings["global"]["sync_server"]["enabled"]:
|
||||
if (not self.sync_module.enabled or
|
||||
project_name not in self.sync_module.get_enabled_projects()):
|
||||
return mapping
|
||||
|
||||
project_name = os.getenv("AVALON_PROJECT")
|
||||
|
||||
active_site = self.sync_module.get_local_normalized_site(
|
||||
self.sync_module.get_active_site(project_name))
|
||||
remote_site = self.sync_module.get_local_normalized_site(
|
||||
|
|
@ -171,11 +164,7 @@ class HostDirmap(object):
|
|||
"active {} - remote {}".format(active_site, remote_site)
|
||||
)
|
||||
|
||||
if (
|
||||
active_site == "local"
|
||||
and project_name in self.sync_module.get_enabled_projects()
|
||||
and active_site != remote_site
|
||||
):
|
||||
if active_site == "local" and active_site != remote_site:
|
||||
sync_settings = self.sync_module.get_sync_project_setting(
|
||||
project_name,
|
||||
exclude_locals=False,
|
||||
|
|
@ -188,7 +177,15 @@ class HostDirmap(object):
|
|||
|
||||
self.log.debug("local overrides {}".format(active_overrides))
|
||||
self.log.debug("remote overrides {}".format(remote_overrides))
|
||||
|
||||
current_platform = platform.system().lower()
|
||||
remote_provider = self.sync_module.get_provider_for_site(
|
||||
project_name, remote_site
|
||||
)
|
||||
# dirmap has sense only with regular disk provider, in the workfile
|
||||
# won't be root on cloud or sftp provider
|
||||
if remote_provider != "local_drive":
|
||||
remote_site = "studio"
|
||||
for root_name, active_site_dir in active_overrides.items():
|
||||
remote_site_dir = (
|
||||
remote_overrides.get(root_name)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class HostBase(object):
|
|||
Compared to 'avalon' concept:
|
||||
What was before considered as functions in host implementation folder. The
|
||||
host implementation should primarily care about adding ability of creation
|
||||
(mark subsets to be published) and optionaly about referencing published
|
||||
(mark subsets to be published) and optionally about referencing published
|
||||
representations as containers.
|
||||
|
||||
Host may need extend some functionality like working with workfiles
|
||||
|
|
@ -129,9 +129,9 @@ class HostBase(object):
|
|||
"""Get current context information.
|
||||
|
||||
This method should be used to get current context of host. Usage of
|
||||
this method can be crutial for host implementations in DCCs where
|
||||
this method can be crucial for host implementations in DCCs where
|
||||
can be opened multiple workfiles at one moment and change of context
|
||||
can't be catched properly.
|
||||
can't be caught properly.
|
||||
|
||||
Default implementation returns values from 'legacy_io.Session'.
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ class ILoadHost:
|
|||
|
||||
@abstractmethod
|
||||
def get_containers(self):
|
||||
"""Retreive referenced containers from scene.
|
||||
"""Retrieve referenced containers from scene.
|
||||
|
||||
This can be implemented in hosts where referencing can be used.
|
||||
|
||||
|
|
@ -191,7 +191,7 @@ class IWorkfileHost:
|
|||
|
||||
@abstractmethod
|
||||
def get_current_workfile(self):
|
||||
"""Retreive path to current opened file.
|
||||
"""Retrieve path to current opened file.
|
||||
|
||||
Returns:
|
||||
str: Path to file which is currently opened.
|
||||
|
|
@ -220,8 +220,8 @@ class IWorkfileHost:
|
|||
Default implementation keeps workdir untouched.
|
||||
|
||||
Warnings:
|
||||
We must handle this modification with more sofisticated way because
|
||||
this can't be called out of DCC so opening of last workfile
|
||||
We must handle this modification with more sophisticated way
|
||||
because this can't be called out of DCC so opening of last workfile
|
||||
(calculated before DCC is launched) is complicated. Also breaking
|
||||
defined work template is not a good idea.
|
||||
Only place where it's really used and can make sense is Maya. There
|
||||
|
|
@ -302,7 +302,7 @@ class IPublishHost:
|
|||
required methods.
|
||||
|
||||
Returns:
|
||||
list[str]: Missing method implementations for new publsher
|
||||
list[str]: Missing method implementations for new publisher
|
||||
workflow.
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -504,7 +504,7 @@ function addItemAsLayerToComp(comp_id, item_id, found_comp){
|
|||
* Args:
|
||||
* comp_id (int): id of target composition
|
||||
* item_id (int): FootageItem.id
|
||||
* found_comp (CompItem, optional): to limit quering if
|
||||
* found_comp (CompItem, optional): to limit querying if
|
||||
* comp already found previously
|
||||
*/
|
||||
var comp = found_comp || app.project.itemByID(comp_id);
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ class AfterEffectsServerStub():
|
|||
Get complete stored JSON with metadata from AE.Metadata.Label
|
||||
field.
|
||||
|
||||
It contains containers loaded by any Loader OR instances creted
|
||||
It contains containers loaded by any Loader OR instances created
|
||||
by Creator.
|
||||
|
||||
Returns:
|
||||
|
|
|
|||
|
|
@ -31,10 +31,13 @@ from .lib import (
|
|||
lsattrs,
|
||||
read,
|
||||
maintained_selection,
|
||||
maintained_time,
|
||||
get_selection,
|
||||
# unique_name,
|
||||
)
|
||||
|
||||
from .capture import capture
|
||||
|
||||
|
||||
__all__ = [
|
||||
"install",
|
||||
|
|
@ -56,9 +59,11 @@ __all__ = [
|
|||
|
||||
# Utility functions
|
||||
"maintained_selection",
|
||||
"maintained_time",
|
||||
"lsattr",
|
||||
"lsattrs",
|
||||
"read",
|
||||
"get_selection",
|
||||
"capture",
|
||||
# "unique_name",
|
||||
]
|
||||
|
|
|
|||
278
openpype/hosts/blender/api/capture.py
Normal file
278
openpype/hosts/blender/api/capture.py
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
|
||||
"""Blender Capture
|
||||
Playblasting with independent viewport, camera and display options
|
||||
"""
|
||||
import contextlib
|
||||
import bpy
|
||||
|
||||
from .lib import maintained_time
|
||||
from .plugin import deselect_all, create_blender_context
|
||||
|
||||
|
||||
def capture(
|
||||
camera=None,
|
||||
width=None,
|
||||
height=None,
|
||||
filename=None,
|
||||
start_frame=None,
|
||||
end_frame=None,
|
||||
step_frame=None,
|
||||
sound=None,
|
||||
isolate=None,
|
||||
maintain_aspect_ratio=True,
|
||||
overwrite=False,
|
||||
image_settings=None,
|
||||
display_options=None
|
||||
):
|
||||
"""Playblast in an independent windows
|
||||
Arguments:
|
||||
camera (str, optional): Name of camera, defaults to "Camera"
|
||||
width (int, optional): Width of output in pixels
|
||||
height (int, optional): Height of output in pixels
|
||||
filename (str, optional): Name of output file path. Defaults to current
|
||||
render output path.
|
||||
start_frame (int, optional): Defaults to current start frame.
|
||||
end_frame (int, optional): Defaults to current end frame.
|
||||
step_frame (int, optional): Defaults to 1.
|
||||
sound (str, optional): Specify the sound node to be used during
|
||||
playblast. When None (default) no sound will be used.
|
||||
isolate (list): List of nodes to isolate upon capturing
|
||||
maintain_aspect_ratio (bool, optional): Modify height in order to
|
||||
maintain aspect ratio.
|
||||
overwrite (bool, optional): Whether or not to overwrite if file
|
||||
already exists. If disabled and file exists and error will be
|
||||
raised.
|
||||
image_settings (dict, optional): Supplied image settings for render,
|
||||
using `ImageSettings`
|
||||
display_options (dict, optional): Supplied display options for render
|
||||
"""
|
||||
|
||||
scene = bpy.context.scene
|
||||
camera = camera or "Camera"
|
||||
|
||||
# Ensure camera exists.
|
||||
if camera not in scene.objects and camera != "AUTO":
|
||||
raise RuntimeError("Camera does not exist: {0}".format(camera))
|
||||
|
||||
# Ensure resolution.
|
||||
if width and height:
|
||||
maintain_aspect_ratio = False
|
||||
width = width or scene.render.resolution_x
|
||||
height = height or scene.render.resolution_y
|
||||
if maintain_aspect_ratio:
|
||||
ratio = scene.render.resolution_x / scene.render.resolution_y
|
||||
height = round(width / ratio)
|
||||
|
||||
# Get frame range.
|
||||
if start_frame is None:
|
||||
start_frame = scene.frame_start
|
||||
if end_frame is None:
|
||||
end_frame = scene.frame_end
|
||||
if step_frame is None:
|
||||
step_frame = 1
|
||||
frame_range = (start_frame, end_frame, step_frame)
|
||||
|
||||
if filename is None:
|
||||
filename = scene.render.filepath
|
||||
|
||||
render_options = {
|
||||
"filepath": "{}.".format(filename.rstrip(".")),
|
||||
"resolution_x": width,
|
||||
"resolution_y": height,
|
||||
"use_overwrite": overwrite,
|
||||
}
|
||||
|
||||
with _independent_window() as window:
|
||||
|
||||
applied_view(window, camera, isolate, options=display_options)
|
||||
|
||||
with contextlib.ExitStack() as stack:
|
||||
stack.enter_context(maintain_camera(window, camera))
|
||||
stack.enter_context(applied_frame_range(window, *frame_range))
|
||||
stack.enter_context(applied_render_options(window, render_options))
|
||||
stack.enter_context(applied_image_settings(window, image_settings))
|
||||
stack.enter_context(maintained_time())
|
||||
|
||||
bpy.ops.render.opengl(
|
||||
animation=True,
|
||||
render_keyed_only=False,
|
||||
sequencer=False,
|
||||
write_still=False,
|
||||
view_context=True
|
||||
)
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
ImageSettings = {
|
||||
"file_format": "FFMPEG",
|
||||
"color_mode": "RGB",
|
||||
"ffmpeg": {
|
||||
"format": "QUICKTIME",
|
||||
"use_autosplit": False,
|
||||
"codec": "H264",
|
||||
"constant_rate_factor": "MEDIUM",
|
||||
"gopsize": 18,
|
||||
"use_max_b_frames": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def isolate_objects(window, objects):
|
||||
"""Isolate selection"""
|
||||
deselect_all()
|
||||
|
||||
for obj in objects:
|
||||
obj.select_set(True)
|
||||
|
||||
context = create_blender_context(selected=objects, window=window)
|
||||
|
||||
bpy.ops.view3d.view_axis(context, type="FRONT")
|
||||
bpy.ops.view3d.localview(context)
|
||||
|
||||
deselect_all()
|
||||
|
||||
|
||||
def _apply_options(entity, options):
|
||||
for option, value in options.items():
|
||||
if isinstance(value, dict):
|
||||
_apply_options(getattr(entity, option), value)
|
||||
else:
|
||||
setattr(entity, option, value)
|
||||
|
||||
|
||||
def applied_view(window, camera, isolate=None, options=None):
|
||||
"""Apply view options to window."""
|
||||
area = window.screen.areas[0]
|
||||
space = area.spaces[0]
|
||||
|
||||
area.ui_type = "VIEW_3D"
|
||||
|
||||
meshes = [obj for obj in window.scene.objects if obj.type == "MESH"]
|
||||
|
||||
if camera == "AUTO":
|
||||
space.region_3d.view_perspective = "ORTHO"
|
||||
isolate_objects(window, isolate or meshes)
|
||||
else:
|
||||
isolate_objects(window, isolate or meshes)
|
||||
space.camera = window.scene.objects.get(camera)
|
||||
space.region_3d.view_perspective = "CAMERA"
|
||||
|
||||
if isinstance(options, dict):
|
||||
_apply_options(space, options)
|
||||
else:
|
||||
space.shading.type = "SOLID"
|
||||
space.shading.color_type = "MATERIAL"
|
||||
space.show_gizmo = False
|
||||
space.overlay.show_overlays = False
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def applied_frame_range(window, start, end, step):
|
||||
"""Context manager for setting frame range."""
|
||||
# Store current frame range
|
||||
current_frame_start = window.scene.frame_start
|
||||
current_frame_end = window.scene.frame_end
|
||||
current_frame_step = window.scene.frame_step
|
||||
# Apply frame range
|
||||
window.scene.frame_start = start
|
||||
window.scene.frame_end = end
|
||||
window.scene.frame_step = step
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
# Restore frame range
|
||||
window.scene.frame_start = current_frame_start
|
||||
window.scene.frame_end = current_frame_end
|
||||
window.scene.frame_step = current_frame_step
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def applied_render_options(window, options):
|
||||
"""Context manager for setting render options."""
|
||||
render = window.scene.render
|
||||
|
||||
# Store current settings
|
||||
original = {}
|
||||
for opt in options.copy():
|
||||
try:
|
||||
original[opt] = getattr(render, opt)
|
||||
except ValueError:
|
||||
options.pop(opt)
|
||||
|
||||
# Apply settings
|
||||
_apply_options(render, options)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
# Restore previous settings
|
||||
_apply_options(render, original)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def applied_image_settings(window, options):
|
||||
"""Context manager to override image settings."""
|
||||
|
||||
options = options or ImageSettings.copy()
|
||||
ffmpeg = options.pop("ffmpeg", {})
|
||||
render = window.scene.render
|
||||
|
||||
# Store current image settings
|
||||
original = {}
|
||||
for opt in options.copy():
|
||||
try:
|
||||
original[opt] = getattr(render.image_settings, opt)
|
||||
except ValueError:
|
||||
options.pop(opt)
|
||||
|
||||
# Store current ffmpeg settings
|
||||
original_ffmpeg = {}
|
||||
for opt in ffmpeg.copy():
|
||||
try:
|
||||
original_ffmpeg[opt] = getattr(render.ffmpeg, opt)
|
||||
except ValueError:
|
||||
ffmpeg.pop(opt)
|
||||
|
||||
# Apply image settings
|
||||
for opt, value in options.items():
|
||||
setattr(render.image_settings, opt, value)
|
||||
|
||||
# Apply ffmpeg settings
|
||||
for opt, value in ffmpeg.items():
|
||||
setattr(render.ffmpeg, opt, value)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
# Restore previous settings
|
||||
for opt, value in original.items():
|
||||
setattr(render.image_settings, opt, value)
|
||||
for opt, value in original_ffmpeg.items():
|
||||
setattr(render.ffmpeg, opt, value)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def maintain_camera(window, camera):
|
||||
"""Context manager to override camera."""
|
||||
current_camera = window.scene.camera
|
||||
if camera in window.scene.objects:
|
||||
window.scene.camera = window.scene.objects.get(camera)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
window.scene.camera = current_camera
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _independent_window():
|
||||
"""Create capture-window context."""
|
||||
context = create_blender_context()
|
||||
current_windows = set(bpy.context.window_manager.windows)
|
||||
bpy.ops.wm.window_new(context)
|
||||
window = list(set(bpy.context.window_manager.windows) - current_windows)[0]
|
||||
context["window"] = window
|
||||
try:
|
||||
yield window
|
||||
finally:
|
||||
bpy.ops.wm.window_close(context)
|
||||
|
|
@ -284,3 +284,13 @@ def maintained_selection():
|
|||
# This could happen if the active node was deleted during the
|
||||
# context.
|
||||
log.exception("Failed to set active object.")
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def maintained_time():
|
||||
"""Maintain current frame during context."""
|
||||
current_time = bpy.context.scene.frame_current
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
bpy.context.scene.frame_current = current_time
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ from .workio import OpenFileCacher
|
|||
PREVIEW_COLLECTIONS: Dict = dict()
|
||||
|
||||
# This seems like a good value to keep the Qt app responsive and doesn't slow
|
||||
# down Blender. At least on macOS I the interace of Blender gets very laggy if
|
||||
# down Blender. At least on macOS I the interface of Blender gets very laggy if
|
||||
# you make it smaller.
|
||||
TIMER_INTERVAL: float = 0.01 if platform.system() == "Windows" else 0.1
|
||||
|
||||
|
|
@ -84,11 +84,11 @@ class MainThreadItem:
|
|||
self.kwargs = kwargs
|
||||
|
||||
def execute(self):
|
||||
"""Execute callback and store it's result.
|
||||
"""Execute callback and store its result.
|
||||
|
||||
Method must be called from main thread. Item is marked as `done`
|
||||
when callback execution finished. Store output of callback of exception
|
||||
information when callback raise one.
|
||||
information when callback raises one.
|
||||
"""
|
||||
print("Executing process in main thread")
|
||||
if self.done:
|
||||
|
|
@ -382,8 +382,8 @@ class TOPBAR_MT_avalon(bpy.types.Menu):
|
|||
layout.operator(LaunchLibrary.bl_idname, text="Library...")
|
||||
layout.separator()
|
||||
layout.operator(LaunchWorkFiles.bl_idname, text="Work Files...")
|
||||
# TODO (jasper): maybe add 'Reload Pipeline', 'Reset Frame Range' and
|
||||
# 'Reset Resolution'?
|
||||
# TODO (jasper): maybe add 'Reload Pipeline', 'Set Frame Range' and
|
||||
# 'Set Resolution'?
|
||||
|
||||
|
||||
def draw_avalon_menu(self, context):
|
||||
|
|
|
|||
|
|
@ -62,7 +62,8 @@ def prepare_data(data, container_name=None):
|
|||
|
||||
|
||||
def create_blender_context(active: Optional[bpy.types.Object] = None,
|
||||
selected: Optional[bpy.types.Object] = None,):
|
||||
selected: Optional[bpy.types.Object] = None,
|
||||
window: Optional[bpy.types.Window] = None):
|
||||
"""Create a new Blender context. If an object is passed as
|
||||
parameter, it is set as selected and active.
|
||||
"""
|
||||
|
|
@ -72,7 +73,9 @@ def create_blender_context(active: Optional[bpy.types.Object] = None,
|
|||
|
||||
override_context = bpy.context.copy()
|
||||
|
||||
for win in bpy.context.window_manager.windows:
|
||||
windows = [window] if window else bpy.context.window_manager.windows
|
||||
|
||||
for win in windows:
|
||||
for area in win.screen.areas:
|
||||
if area.type == 'VIEW_3D':
|
||||
for region in area.regions:
|
||||
|
|
|
|||
47
openpype/hosts/blender/plugins/create/create_review.py
Normal file
47
openpype/hosts/blender/plugins/create/create_review.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
"""Create review."""
|
||||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
|
||||
|
||||
class CreateReview(plugin.Creator):
|
||||
"""Single baked camera"""
|
||||
|
||||
name = "reviewDefault"
|
||||
label = "Review"
|
||||
family = "review"
|
||||
icon = "video-camera"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.collections.new(name=name)
|
||||
instances.children.link(asset_group)
|
||||
self.data['task'] = legacy_io.Session.get('AVALON_TASK')
|
||||
lib.imprint(asset_group, self.data)
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
asset_group.objects.link(obj)
|
||||
elif (self.options or {}).get("asset_group"):
|
||||
obj = (self.options or {}).get("asset_group")
|
||||
asset_group.objects.link(obj)
|
||||
|
||||
return asset_group
|
||||
64
openpype/hosts/blender/plugins/publish/collect_review.py
Normal file
64
openpype/hosts/blender/plugins/publish/collect_review.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import bpy
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline import legacy_io
|
||||
|
||||
|
||||
class CollectReview(pyblish.api.InstancePlugin):
|
||||
"""Collect Review data
|
||||
|
||||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.3
|
||||
label = "Collect Review Data"
|
||||
families = ["review"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
self.log.debug(f"instance: {instance}")
|
||||
|
||||
# get cameras
|
||||
cameras = [
|
||||
obj
|
||||
for obj in instance
|
||||
if isinstance(obj, bpy.types.Object) and obj.type == "CAMERA"
|
||||
]
|
||||
|
||||
assert len(cameras) == 1, (
|
||||
f"Not a single camera found in extraction: {cameras}"
|
||||
)
|
||||
camera = cameras[0].name
|
||||
self.log.debug(f"camera: {camera}")
|
||||
|
||||
# get isolate objects list from meshes instance members .
|
||||
isolate_objects = [
|
||||
obj
|
||||
for obj in instance
|
||||
if isinstance(obj, bpy.types.Object) and obj.type == "MESH"
|
||||
]
|
||||
|
||||
if not instance.data.get("remove"):
|
||||
|
||||
task = legacy_io.Session.get("AVALON_TASK")
|
||||
|
||||
instance.data.update({
|
||||
"subset": f"{task}Review",
|
||||
"review_camera": camera,
|
||||
"frameStart": instance.context.data["frameStart"],
|
||||
"frameEnd": instance.context.data["frameEnd"],
|
||||
"fps": instance.context.data["fps"],
|
||||
"isolate": isolate_objects,
|
||||
})
|
||||
|
||||
self.log.debug(f"instance data: {instance.data}")
|
||||
|
||||
# TODO : Collect audio
|
||||
audio_tracks = []
|
||||
instance.data["audio"] = []
|
||||
for track in audio_tracks:
|
||||
instance.data["audio"].append(
|
||||
{
|
||||
"offset": track.offset.get(),
|
||||
"filename": track.filename.get(),
|
||||
}
|
||||
)
|
||||
122
openpype/hosts/blender/plugins/publish/extract_playblast.py
Normal file
122
openpype/hosts/blender/plugins/publish/extract_playblast.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import os
|
||||
import clique
|
||||
|
||||
import bpy
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.blender.api import capture
|
||||
from openpype.hosts.blender.api.lib import maintained_time
|
||||
|
||||
|
||||
class ExtractPlayblast(publish.Extractor):
|
||||
"""
|
||||
Extract viewport playblast.
|
||||
|
||||
Takes review camera and creates review Quicktime video based on viewport
|
||||
capture.
|
||||
"""
|
||||
|
||||
label = "Extract Playblast"
|
||||
hosts = ["blender"]
|
||||
families = ["review"]
|
||||
optional = True
|
||||
order = pyblish.api.ExtractorOrder + 0.01
|
||||
|
||||
def process(self, instance):
|
||||
self.log.info("Extracting capture..")
|
||||
|
||||
self.log.info(instance.data)
|
||||
|
||||
# get scene fps
|
||||
fps = instance.data.get("fps")
|
||||
if fps is None:
|
||||
fps = bpy.context.scene.render.fps
|
||||
instance.data["fps"] = fps
|
||||
|
||||
self.log.info(f"fps: {fps}")
|
||||
|
||||
# If start and end frames cannot be determined,
|
||||
# get them from Blender timeline.
|
||||
start = instance.data.get("frameStart", bpy.context.scene.frame_start)
|
||||
end = instance.data.get("frameEnd", bpy.context.scene.frame_end)
|
||||
|
||||
self.log.info(f"start: {start}, end: {end}")
|
||||
assert end > start, "Invalid time range !"
|
||||
|
||||
# get cameras
|
||||
camera = instance.data("review_camera", None)
|
||||
|
||||
# get isolate objects list
|
||||
isolate = instance.data("isolate", None)
|
||||
|
||||
# get output path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = instance.name
|
||||
path = os.path.join(stagingdir, filename)
|
||||
|
||||
self.log.info(f"Outputting images to {path}")
|
||||
|
||||
project_settings = instance.context.data["project_settings"]["blender"]
|
||||
presets = project_settings["publish"]["ExtractPlayblast"]["presets"]
|
||||
preset = presets.get("default")
|
||||
preset.update({
|
||||
"camera": camera,
|
||||
"start_frame": start,
|
||||
"end_frame": end,
|
||||
"filename": path,
|
||||
"overwrite": True,
|
||||
"isolate": isolate,
|
||||
})
|
||||
preset.setdefault(
|
||||
"image_settings",
|
||||
{
|
||||
"file_format": "PNG",
|
||||
"color_mode": "RGB",
|
||||
"color_depth": "8",
|
||||
"compression": 15,
|
||||
},
|
||||
)
|
||||
|
||||
with maintained_time():
|
||||
path = capture(**preset)
|
||||
|
||||
self.log.debug(f"playblast path {path}")
|
||||
|
||||
collected_files = os.listdir(stagingdir)
|
||||
collections, remainder = clique.assemble(
|
||||
collected_files,
|
||||
patterns=[f"{filename}\\.{clique.DIGITS_PATTERN}\\.png$"],
|
||||
)
|
||||
|
||||
if len(collections) > 1:
|
||||
raise RuntimeError(
|
||||
f"More than one collection found in stagingdir: {stagingdir}"
|
||||
)
|
||||
elif len(collections) == 0:
|
||||
raise RuntimeError(
|
||||
f"No collection found in stagingdir: {stagingdir}"
|
||||
)
|
||||
|
||||
frame_collection = collections[0]
|
||||
|
||||
self.log.info(f"We found collection of interest {frame_collection}")
|
||||
|
||||
instance.data.setdefault("representations", [])
|
||||
|
||||
tags = ["review"]
|
||||
if not instance.data.get("keepImages"):
|
||||
tags.append("delete")
|
||||
|
||||
representation = {
|
||||
"name": "png",
|
||||
"ext": "png",
|
||||
"files": list(frame_collection),
|
||||
"stagingDir": stagingdir,
|
||||
"frameStart": start,
|
||||
"frameEnd": end,
|
||||
"fps": fps,
|
||||
"tags": tags,
|
||||
"camera_name": camera
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
99
openpype/hosts/blender/plugins/publish/extract_thumbnail.py
Normal file
99
openpype/hosts/blender/plugins/publish/extract_thumbnail.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import os
|
||||
import glob
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.blender.api import capture
|
||||
from openpype.hosts.blender.api.lib import maintained_time
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
class ExtractThumbnail(publish.Extractor):
|
||||
"""Extract viewport thumbnail.
|
||||
|
||||
Takes review camera and creates a thumbnail based on viewport
|
||||
capture.
|
||||
|
||||
"""
|
||||
|
||||
label = "Extract Thumbnail"
|
||||
hosts = ["blender"]
|
||||
families = ["review"]
|
||||
order = pyblish.api.ExtractorOrder + 0.01
|
||||
presets = {}
|
||||
|
||||
def process(self, instance):
|
||||
self.log.info("Extracting capture..")
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = instance.name
|
||||
path = os.path.join(stagingdir, filename)
|
||||
|
||||
self.log.info(f"Outputting images to {path}")
|
||||
|
||||
camera = instance.data.get("review_camera", "AUTO")
|
||||
start = instance.data.get("frameStart", bpy.context.scene.frame_start)
|
||||
family = instance.data.get("family")
|
||||
isolate = instance.data("isolate", None)
|
||||
|
||||
preset = self.presets.get(family, {})
|
||||
|
||||
preset.update({
|
||||
"camera": camera,
|
||||
"start_frame": start,
|
||||
"end_frame": start,
|
||||
"filename": path,
|
||||
"overwrite": True,
|
||||
"isolate": isolate,
|
||||
})
|
||||
preset.setdefault(
|
||||
"image_settings",
|
||||
{
|
||||
"file_format": "JPEG",
|
||||
"color_mode": "RGB",
|
||||
"quality": 100,
|
||||
},
|
||||
)
|
||||
|
||||
with maintained_time():
|
||||
path = capture(**preset)
|
||||
|
||||
thumbnail = os.path.basename(self._fix_output_path(path))
|
||||
|
||||
self.log.info(f"thumbnail: {thumbnail}")
|
||||
|
||||
instance.data.setdefault("representations", [])
|
||||
|
||||
representation = {
|
||||
"name": "thumbnail",
|
||||
"ext": "jpg",
|
||||
"files": thumbnail,
|
||||
"stagingDir": stagingdir,
|
||||
"thumbnail": True
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
def _fix_output_path(self, filepath):
|
||||
""""Workaround to return correct filepath.
|
||||
|
||||
To workaround this we just glob.glob() for any file extensions and
|
||||
assume the latest modified file is the correct file and return it.
|
||||
|
||||
"""
|
||||
# Catch cancelled playblast
|
||||
if filepath is None:
|
||||
self.log.warning(
|
||||
"Playblast did not result in output path. "
|
||||
"Playblast is probably interrupted."
|
||||
)
|
||||
return None
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
files = glob.glob(f"{filepath}.*.jpg")
|
||||
|
||||
if not files:
|
||||
raise RuntimeError(f"Couldn't find playblast from: {filepath}")
|
||||
filepath = max(files, key=os.path.getmtime)
|
||||
|
||||
return filepath
|
||||
|
|
@ -38,8 +38,9 @@ class CelactionPrelaunchHook(PreLaunchHook):
|
|||
)
|
||||
|
||||
path_to_cli = os.path.join(CELACTION_SCRIPTS_DIR, "publish_cli.py")
|
||||
subproces_args = get_openpype_execute_args("run", path_to_cli)
|
||||
openpype_executable = subproces_args.pop(0)
|
||||
subprocess_args = get_openpype_execute_args("run", path_to_cli)
|
||||
openpype_executable = subprocess_args.pop(0)
|
||||
workfile_settings = self.get_workfile_settings()
|
||||
|
||||
winreg.SetValueEx(
|
||||
hKey,
|
||||
|
|
@ -49,20 +50,34 @@ class CelactionPrelaunchHook(PreLaunchHook):
|
|||
openpype_executable
|
||||
)
|
||||
|
||||
parameters = subproces_args + [
|
||||
"--currentFile", "*SCENE*",
|
||||
"--chunk", "*CHUNK*",
|
||||
"--frameStart", "*START*",
|
||||
"--frameEnd", "*END*",
|
||||
"--resolutionWidth", "*X*",
|
||||
"--resolutionHeight", "*Y*"
|
||||
# add required arguments for workfile path
|
||||
parameters = subprocess_args + [
|
||||
"--currentFile", "*SCENE*"
|
||||
]
|
||||
|
||||
# Add custom parameters from workfile settings
|
||||
if "render_chunk" in workfile_settings["submission_overrides"]:
|
||||
parameters += [
|
||||
"--chunk", "*CHUNK*"
|
||||
]
|
||||
if "resolution" in workfile_settings["submission_overrides"]:
|
||||
parameters += [
|
||||
"--resolutionWidth", "*X*",
|
||||
"--resolutionHeight", "*Y*"
|
||||
]
|
||||
if "frame_range" in workfile_settings["submission_overrides"]:
|
||||
parameters += [
|
||||
"--frameStart", "*START*",
|
||||
"--frameEnd", "*END*"
|
||||
]
|
||||
|
||||
winreg.SetValueEx(
|
||||
hKey, "SubmitParametersTitle", 0, winreg.REG_SZ,
|
||||
subprocess.list2cmdline(parameters)
|
||||
)
|
||||
|
||||
self.log.debug(f"__ parameters: \"{parameters}\"")
|
||||
|
||||
# setting resolution parameters
|
||||
path_submit = "\\".join([
|
||||
path_user_settings, "Dialogs", "SubmitOutput"
|
||||
|
|
@ -135,3 +150,6 @@ class CelactionPrelaunchHook(PreLaunchHook):
|
|||
self.log.info(f"Workfile to open: \"{workfile_path}\"")
|
||||
|
||||
return workfile_path
|
||||
|
||||
def get_workfile_settings(self):
|
||||
return self.data["project_settings"]["celaction"]["workfile"]
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class CollectCelactionCliKwargs(pyblish.api.Collector):
|
|||
passing_kwargs[key] = value
|
||||
|
||||
if missing_kwargs:
|
||||
raise RuntimeError("Missing arguments {}".format(
|
||||
self.log.debug("Missing arguments {}".format(
|
||||
", ".join(
|
||||
[f'"{key}"' for key in missing_kwargs]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -773,7 +773,7 @@ class MediaInfoFile(object):
|
|||
if logger:
|
||||
self.log = logger
|
||||
|
||||
# test if `dl_get_media_info` paht exists
|
||||
# test if `dl_get_media_info` path exists
|
||||
self._validate_media_script_path()
|
||||
|
||||
# derivate other feed variables
|
||||
|
|
@ -993,7 +993,7 @@ class MediaInfoFile(object):
|
|||
|
||||
def _validate_media_script_path(self):
|
||||
if not os.path.isfile(self.MEDIA_SCRIPT_PATH):
|
||||
raise IOError("Media Scirpt does not exist: `{}`".format(
|
||||
raise IOError("Media Script does not exist: `{}`".format(
|
||||
self.MEDIA_SCRIPT_PATH))
|
||||
|
||||
def _generate_media_info_file(self, fpath, feed_ext, feed_dir):
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def install():
|
|||
pyblish.register_plugin_path(PUBLISH_PATH)
|
||||
register_loader_plugin_path(LOAD_PATH)
|
||||
register_creator_plugin_path(CREATE_PATH)
|
||||
log.info("OpenPype Flame plug-ins registred ...")
|
||||
log.info("OpenPype Flame plug-ins registered ...")
|
||||
|
||||
# register callback for switching publishable
|
||||
pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled)
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ class CreatorWidget(QtWidgets.QDialog):
|
|||
# convert label text to normal capitalized text with spaces
|
||||
label_text = self.camel_case_split(text)
|
||||
|
||||
# assign the new text to lable widget
|
||||
# assign the new text to label widget
|
||||
label = QtWidgets.QLabel(label_text)
|
||||
label.setObjectName("LineLabel")
|
||||
|
||||
|
|
@ -345,8 +345,8 @@ class PublishableClip:
|
|||
"track": "sequence",
|
||||
}
|
||||
|
||||
# parents search patern
|
||||
parents_search_patern = r"\{([a-z]*?)\}"
|
||||
# parents search pattern
|
||||
parents_search_pattern = r"\{([a-z]*?)\}"
|
||||
|
||||
# default templates for non-ui use
|
||||
rename_default = False
|
||||
|
|
@ -445,7 +445,7 @@ class PublishableClip:
|
|||
return self.current_segment
|
||||
|
||||
def _populate_segment_default_data(self):
|
||||
""" Populate default formating data from segment. """
|
||||
""" Populate default formatting data from segment. """
|
||||
|
||||
self.current_segment_default_data = {
|
||||
"_folder_": "shots",
|
||||
|
|
@ -538,7 +538,7 @@ class PublishableClip:
|
|||
if not self.index_from_segment:
|
||||
self.count_steps *= self.rename_index
|
||||
|
||||
hierarchy_formating_data = {}
|
||||
hierarchy_formatting_data = {}
|
||||
hierarchy_data = deepcopy(self.hierarchy_data)
|
||||
_data = self.current_segment_default_data.copy()
|
||||
if self.ui_inputs:
|
||||
|
|
@ -552,7 +552,7 @@ class PublishableClip:
|
|||
# mark review layer
|
||||
if self.review_track and (
|
||||
self.review_track not in self.review_track_default):
|
||||
# if review layer is defined and not the same as defalut
|
||||
# if review layer is defined and not the same as default
|
||||
self.review_layer = self.review_track
|
||||
|
||||
# shot num calculate
|
||||
|
|
@ -578,13 +578,13 @@ class PublishableClip:
|
|||
|
||||
# fill up pythonic expresisons in hierarchy data
|
||||
for k, _v in hierarchy_data.items():
|
||||
hierarchy_formating_data[k] = _v["value"].format(**_data)
|
||||
hierarchy_formatting_data[k] = _v["value"].format(**_data)
|
||||
else:
|
||||
# if no gui mode then just pass default data
|
||||
hierarchy_formating_data = hierarchy_data
|
||||
hierarchy_formatting_data = hierarchy_data
|
||||
|
||||
tag_hierarchy_data = self._solve_tag_hierarchy_data(
|
||||
hierarchy_formating_data
|
||||
hierarchy_formatting_data
|
||||
)
|
||||
|
||||
tag_hierarchy_data.update({"heroTrack": True})
|
||||
|
|
@ -615,27 +615,27 @@ class PublishableClip:
|
|||
# in case track name and subset name is the same then add
|
||||
if self.subset_name == self.track_name:
|
||||
_hero_data["subset"] = self.subset
|
||||
# assing data to return hierarchy data to tag
|
||||
# assign data to return hierarchy data to tag
|
||||
tag_hierarchy_data = _hero_data
|
||||
break
|
||||
|
||||
# add data to return data dict
|
||||
self.marker_data.update(tag_hierarchy_data)
|
||||
|
||||
def _solve_tag_hierarchy_data(self, hierarchy_formating_data):
|
||||
def _solve_tag_hierarchy_data(self, hierarchy_formatting_data):
|
||||
""" Solve marker data from hierarchy data and templates. """
|
||||
# fill up clip name and hierarchy keys
|
||||
hierarchy_filled = self.hierarchy.format(**hierarchy_formating_data)
|
||||
clip_name_filled = self.clip_name.format(**hierarchy_formating_data)
|
||||
hierarchy_filled = self.hierarchy.format(**hierarchy_formatting_data)
|
||||
clip_name_filled = self.clip_name.format(**hierarchy_formatting_data)
|
||||
|
||||
# remove shot from hierarchy data: is not needed anymore
|
||||
hierarchy_formating_data.pop("shot")
|
||||
hierarchy_formatting_data.pop("shot")
|
||||
|
||||
return {
|
||||
"newClipName": clip_name_filled,
|
||||
"hierarchy": hierarchy_filled,
|
||||
"parents": self.parents,
|
||||
"hierarchyData": hierarchy_formating_data,
|
||||
"hierarchyData": hierarchy_formatting_data,
|
||||
"subset": self.subset,
|
||||
"family": self.subset_family,
|
||||
"families": [self.family]
|
||||
|
|
@ -650,17 +650,17 @@ class PublishableClip:
|
|||
type
|
||||
)
|
||||
|
||||
# first collect formating data to use for formating template
|
||||
formating_data = {}
|
||||
# first collect formatting data to use for formatting template
|
||||
formatting_data = {}
|
||||
for _k, _v in self.hierarchy_data.items():
|
||||
value = _v["value"].format(
|
||||
**self.current_segment_default_data)
|
||||
formating_data[_k] = value
|
||||
formatting_data[_k] = value
|
||||
|
||||
return {
|
||||
"entity_type": entity_type,
|
||||
"entity_name": template.format(
|
||||
**formating_data
|
||||
**formatting_data
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -668,9 +668,9 @@ class PublishableClip:
|
|||
""" Create parents and return it in list. """
|
||||
self.parents = []
|
||||
|
||||
patern = re.compile(self.parents_search_patern)
|
||||
pattern = re.compile(self.parents_search_pattern)
|
||||
|
||||
par_split = [(patern.findall(t).pop(), t)
|
||||
par_split = [(pattern.findall(t).pop(), t)
|
||||
for t in self.hierarchy.split("/")]
|
||||
|
||||
for type, template in par_split:
|
||||
|
|
@ -902,22 +902,22 @@ class OpenClipSolver(flib.MediaInfoFile):
|
|||
):
|
||||
return
|
||||
|
||||
formating_data = self._update_formating_data(
|
||||
formatting_data = self._update_formatting_data(
|
||||
layerName=layer_name,
|
||||
layerUID=layer_uid
|
||||
)
|
||||
name_obj.text = StringTemplate(
|
||||
self.layer_rename_template
|
||||
).format(formating_data)
|
||||
).format(formatting_data)
|
||||
|
||||
def _update_formating_data(self, **kwargs):
|
||||
""" Updating formating data for layer rename
|
||||
def _update_formatting_data(self, **kwargs):
|
||||
""" Updating formatting data for layer rename
|
||||
|
||||
Attributes:
|
||||
key=value (optional): will be included to formating data
|
||||
key=value (optional): will be included to formatting data
|
||||
as {key: value}
|
||||
Returns:
|
||||
dict: anatomy context data for formating
|
||||
dict: anatomy context data for formatting
|
||||
"""
|
||||
self.log.debug(">> self.clip_data: {}".format(self.clip_data))
|
||||
clip_name_obj = self.clip_data.find("name")
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ class WireTapCom(object):
|
|||
list: all available volumes in server
|
||||
|
||||
Rises:
|
||||
AttributeError: unable to get any volumes childs from server
|
||||
AttributeError: unable to get any volumes children from server
|
||||
"""
|
||||
root = WireTapNodeHandle(self._server, "/volumes")
|
||||
children_num = WireTapInt(0)
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ def _sync_utility_scripts(env=None):
|
|||
shutil.copy2(src, dst)
|
||||
except (PermissionError, FileExistsError) as msg:
|
||||
log.warning(
|
||||
"Not able to coppy to: `{}`, Problem with: `{}`".format(
|
||||
"Not able to copy to: `{}`, Problem with: `{}`".format(
|
||||
dst,
|
||||
msg
|
||||
)
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ class FlamePrelaunch(PreLaunchHook):
|
|||
def _add_pythonpath(self):
|
||||
pythonpath = self.launch_context.env.get("PYTHONPATH")
|
||||
|
||||
# separate it explicity by `;` that is what we use in settings
|
||||
# separate it explicitly by `;` that is what we use in settings
|
||||
new_pythonpath = self.flame_pythonpath.split(os.pathsep)
|
||||
new_pythonpath += pythonpath.split(os.pathsep)
|
||||
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ class CreateShotClip(opfapi.Creator):
|
|||
"type": "QComboBox",
|
||||
"label": "Subset Name",
|
||||
"target": "ui",
|
||||
"toolTip": "chose subset name patern, if [ track name ] is selected, name of track layer will be used", # noqa
|
||||
"toolTip": "chose subset name pattern, if [ track name ] is selected, name of track layer will be used", # noqa
|
||||
"order": 0},
|
||||
"subsetFamily": {
|
||||
"value": ["plate", "take"],
|
||||
|
|
|
|||
|
|
@ -61,9 +61,9 @@ class LoadClip(opfapi.ClipLoader):
|
|||
self.layer_rename_template = self.layer_rename_template.replace(
|
||||
"output", "representation")
|
||||
|
||||
formating_data = deepcopy(context["representation"]["context"])
|
||||
formatting_data = deepcopy(context["representation"]["context"])
|
||||
clip_name = StringTemplate(self.clip_name_template).format(
|
||||
formating_data)
|
||||
formatting_data)
|
||||
|
||||
# convert colorspace with ocio to flame mapping
|
||||
# in imageio flame section
|
||||
|
|
@ -88,7 +88,7 @@ class LoadClip(opfapi.ClipLoader):
|
|||
"version": "v{:0>3}".format(version_name),
|
||||
"layer_rename_template": self.layer_rename_template,
|
||||
"layer_rename_patterns": self.layer_rename_patterns,
|
||||
"context_data": formating_data
|
||||
"context_data": formatting_data
|
||||
}
|
||||
self.log.debug(pformat(
|
||||
loading_context
|
||||
|
|
|
|||
|
|
@ -58,11 +58,11 @@ class LoadClipBatch(opfapi.ClipLoader):
|
|||
self.layer_rename_template = self.layer_rename_template.replace(
|
||||
"output", "representation")
|
||||
|
||||
formating_data = deepcopy(context["representation"]["context"])
|
||||
formating_data["batch"] = self.batch.name.get_value()
|
||||
formatting_data = deepcopy(context["representation"]["context"])
|
||||
formatting_data["batch"] = self.batch.name.get_value()
|
||||
|
||||
clip_name = StringTemplate(self.clip_name_template).format(
|
||||
formating_data)
|
||||
formatting_data)
|
||||
|
||||
# convert colorspace with ocio to flame mapping
|
||||
# in imageio flame section
|
||||
|
|
@ -88,7 +88,7 @@ class LoadClipBatch(opfapi.ClipLoader):
|
|||
"version": "v{:0>3}".format(version_name),
|
||||
"layer_rename_template": self.layer_rename_template,
|
||||
"layer_rename_patterns": self.layer_rename_patterns,
|
||||
"context_data": formating_data
|
||||
"context_data": formatting_data
|
||||
}
|
||||
self.log.debug(pformat(
|
||||
loading_context
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
|
|||
self._get_xml_preset_attrs(
|
||||
attributes, split)
|
||||
|
||||
# add xml overides resolution to instance data
|
||||
# add xml overrides resolution to instance data
|
||||
xml_overrides = attributes["xml_overrides"]
|
||||
if xml_overrides.get("width"):
|
||||
attributes.update({
|
||||
|
|
@ -284,7 +284,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
|
|||
self.log.debug("__ head: `{}`".format(head))
|
||||
self.log.debug("__ tail: `{}`".format(tail))
|
||||
|
||||
# HACK: it is here to serve for versions bellow 2021.1
|
||||
# HACK: it is here to serve for versions below 2021.1
|
||||
if not any([head, tail]):
|
||||
retimed_attributes = get_media_range_with_retimes(
|
||||
otio_clip, handle_start, handle_end)
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ class ExtractSubsetResources(publish.Extractor):
|
|||
self.hide_others(
|
||||
exporting_clip, segment_name, s_track_name)
|
||||
|
||||
# change name patern
|
||||
# change name pattern
|
||||
name_patern_xml = (
|
||||
"<segment name>_<shot name>_{}.").format(
|
||||
unique_name)
|
||||
|
|
@ -358,7 +358,7 @@ class ExtractSubsetResources(publish.Extractor):
|
|||
representation_data["stagingDir"] = n_stage_dir
|
||||
files = n_files
|
||||
|
||||
# add files to represetation but add
|
||||
# add files to representation but add
|
||||
# imagesequence as list
|
||||
if (
|
||||
# first check if path in files is not mov extension
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin):
|
|||
self._load_clip_to_context(instance, bgroup)
|
||||
|
||||
def _add_nodes_to_batch_with_links(self, instance, task_data, batch_group):
|
||||
# get write file node properties > OrederDict because order does mater
|
||||
# get write file node properties > OrederDict because order does matter
|
||||
write_pref_data = self._get_write_prefs(instance, task_data)
|
||||
|
||||
batch_nodes = [
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
from .addon import (
|
||||
get_fusion_version,
|
||||
FusionAddon,
|
||||
FUSION_HOST_DIR,
|
||||
FUSION_VERSIONS_DICT,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"get_fusion_version",
|
||||
"FusionAddon",
|
||||
"FUSION_HOST_DIR",
|
||||
"FUSION_VERSIONS_DICT",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,52 @@
|
|||
import os
|
||||
import re
|
||||
from openpype.modules import OpenPypeModule, IHostAddon
|
||||
from openpype.lib import Logger
|
||||
|
||||
FUSION_HOST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# FUSION_VERSIONS_DICT is used by the pre-launch hooks
|
||||
# The keys correspond to all currently supported Fusion versions
|
||||
# Each value is a list of corresponding Python home variables and a profile
|
||||
# number, which is used by the profile hook to set Fusion profile variables.
|
||||
FUSION_VERSIONS_DICT = {
|
||||
9: ("FUSION_PYTHON36_HOME", 9),
|
||||
16: ("FUSION16_PYTHON36_HOME", 16),
|
||||
17: ("FUSION16_PYTHON36_HOME", 16),
|
||||
18: ("FUSION_PYTHON3_HOME", 16),
|
||||
}
|
||||
|
||||
|
||||
def get_fusion_version(app_name):
|
||||
"""
|
||||
The function is triggered by the prelaunch hooks to get the fusion version.
|
||||
|
||||
`app_name` is obtained by prelaunch hooks from the
|
||||
`launch_context.env.get("AVALON_APP_NAME")`.
|
||||
|
||||
To get a correct Fusion version, a version number should be present
|
||||
in the `applications/fusion/variants` key
|
||||
of the Blackmagic Fusion Application Settings.
|
||||
"""
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
if not app_name:
|
||||
return
|
||||
|
||||
app_version_candidates = re.findall(r"\d+", app_name)
|
||||
if not app_version_candidates:
|
||||
return
|
||||
for app_version in app_version_candidates:
|
||||
if int(app_version) in FUSION_VERSIONS_DICT:
|
||||
return int(app_version)
|
||||
else:
|
||||
log.info(
|
||||
"Unsupported Fusion version: {app_version}".format(
|
||||
app_version=app_version
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class FusionAddon(OpenPypeModule, IHostAddon):
|
||||
name = "fusion"
|
||||
|
|
@ -14,15 +58,11 @@ class FusionAddon(OpenPypeModule, IHostAddon):
|
|||
def get_launch_hook_paths(self, app):
|
||||
if app.host_name != self.host_name:
|
||||
return []
|
||||
return [
|
||||
os.path.join(FUSION_HOST_DIR, "hooks")
|
||||
]
|
||||
return [os.path.join(FUSION_HOST_DIR, "hooks")]
|
||||
|
||||
def add_implementation_envs(self, env, _app):
|
||||
# Set default values if are not already set via settings
|
||||
defaults = {
|
||||
"OPENPYPE_LOG_NO_COLORS": "Yes"
|
||||
}
|
||||
defaults = {"OPENPYPE_LOG_NO_COLORS": "Yes"}
|
||||
for key, value in defaults.items():
|
||||
if not env.get(key):
|
||||
env[key] = value
|
||||
|
|
|
|||
|
|
@ -1,20 +1,11 @@
|
|||
from .pipeline import (
|
||||
install,
|
||||
uninstall,
|
||||
|
||||
FusionHost,
|
||||
ls,
|
||||
|
||||
imprint_container,
|
||||
parse_container
|
||||
)
|
||||
|
||||
from .workio import (
|
||||
open_file,
|
||||
save_file,
|
||||
current_file,
|
||||
has_unsaved_changes,
|
||||
file_extensions,
|
||||
work_root
|
||||
parse_container,
|
||||
list_instances,
|
||||
remove_instance
|
||||
)
|
||||
|
||||
from .lib import (
|
||||
|
|
@ -30,21 +21,11 @@ from .menu import launch_openpype_menu
|
|||
|
||||
__all__ = [
|
||||
# pipeline
|
||||
"install",
|
||||
"uninstall",
|
||||
"ls",
|
||||
|
||||
"imprint_container",
|
||||
"parse_container",
|
||||
|
||||
# workio
|
||||
"open_file",
|
||||
"save_file",
|
||||
"current_file",
|
||||
"has_unsaved_changes",
|
||||
"file_extensions",
|
||||
"work_root",
|
||||
|
||||
# lib
|
||||
"maintained_selection",
|
||||
"update_frame_range",
|
||||
|
|
|
|||
59
openpype/hosts/fusion/api/action.py
Normal file
59
openpype/hosts/fusion/api/action.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import pyblish.api
|
||||
|
||||
|
||||
from openpype.hosts.fusion.api.lib import get_current_comp
|
||||
from openpype.pipeline.publish import get_errored_instances_from_context
|
||||
|
||||
|
||||
class SelectInvalidAction(pyblish.api.Action):
|
||||
"""Select invalid nodes in Fusion when plug-in failed.
|
||||
|
||||
To retrieve the invalid nodes this assumes a static `get_invalid()`
|
||||
method is available on the plugin.
|
||||
|
||||
"""
|
||||
|
||||
label = "Select invalid"
|
||||
on = "failed" # This action is only available on a failed plug-in
|
||||
icon = "search" # Icon from Awesome Icon
|
||||
|
||||
def process(self, context, plugin):
|
||||
errored_instances = get_errored_instances_from_context(context)
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(errored_instances, plugin)
|
||||
|
||||
# Get the invalid nodes for the plug-ins
|
||||
self.log.info("Finding invalid nodes..")
|
||||
invalid = list()
|
||||
for instance in instances:
|
||||
invalid_nodes = plugin.get_invalid(instance)
|
||||
if invalid_nodes:
|
||||
if isinstance(invalid_nodes, (list, tuple)):
|
||||
invalid.extend(invalid_nodes)
|
||||
else:
|
||||
self.log.warning(
|
||||
"Plug-in returned to be invalid, "
|
||||
"but has no selectable nodes."
|
||||
)
|
||||
|
||||
if not invalid:
|
||||
# Assume relevant comp is current comp and clear selection
|
||||
self.log.info("No invalid tools found.")
|
||||
comp = get_current_comp()
|
||||
flow = comp.CurrentFrame.FlowView
|
||||
flow.Select() # No args equals clearing selection
|
||||
return
|
||||
|
||||
# Assume a single comp
|
||||
first_tool = invalid[0]
|
||||
comp = first_tool.Comp()
|
||||
flow = comp.CurrentFrame.FlowView
|
||||
flow.Select() # No args equals clearing selection
|
||||
names = set()
|
||||
for tool in invalid:
|
||||
flow.Select(tool, True)
|
||||
names.add(tool.Name)
|
||||
self.log.info(
|
||||
"Selecting invalid tools: %s" % ", ".join(sorted(names))
|
||||
)
|
||||
|
|
@ -303,10 +303,18 @@ def get_frame_path(path):
|
|||
return filename, padding, ext
|
||||
|
||||
|
||||
def get_current_comp():
|
||||
"""Hack to get current comp in this session"""
|
||||
def get_fusion_module():
|
||||
"""Get current Fusion instance"""
|
||||
fusion = getattr(sys.modules["__main__"], "fusion", None)
|
||||
return fusion.CurrentComp if fusion else None
|
||||
return fusion
|
||||
|
||||
|
||||
def get_current_comp():
|
||||
"""Get current comp in this session"""
|
||||
fusion = get_fusion_module()
|
||||
if fusion is not None:
|
||||
comp = fusion.CurrentComp
|
||||
return comp
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
|
|
|||
|
|
@ -6,12 +6,11 @@ from openpype.tools.utils import host_tools
|
|||
from openpype.style import load_stylesheet
|
||||
from openpype.lib import register_event_callback
|
||||
from openpype.hosts.fusion.scripts import (
|
||||
set_rendermode,
|
||||
duplicate_with_inputs
|
||||
duplicate_with_inputs,
|
||||
)
|
||||
from openpype.hosts.fusion.api.lib import (
|
||||
set_asset_framerange,
|
||||
set_asset_resolution
|
||||
set_asset_resolution,
|
||||
)
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.resources import get_openpype_icon_filepath
|
||||
|
|
@ -45,20 +44,21 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
self.setWindowTitle("OpenPype")
|
||||
|
||||
asset_label = QtWidgets.QLabel("Context", self)
|
||||
asset_label.setStyleSheet("""QLabel {
|
||||
asset_label.setStyleSheet(
|
||||
"""QLabel {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #5f9fb8;
|
||||
}""")
|
||||
}"""
|
||||
)
|
||||
asset_label.setAlignment(QtCore.Qt.AlignHCenter)
|
||||
|
||||
workfiles_btn = QtWidgets.QPushButton("Workfiles...", self)
|
||||
create_btn = QtWidgets.QPushButton("Create...", self)
|
||||
publish_btn = QtWidgets.QPushButton("Publish...", self)
|
||||
load_btn = QtWidgets.QPushButton("Load...", self)
|
||||
publish_btn = QtWidgets.QPushButton("Publish...", self)
|
||||
manager_btn = QtWidgets.QPushButton("Manage...", self)
|
||||
libload_btn = QtWidgets.QPushButton("Library...", self)
|
||||
rendermode_btn = QtWidgets.QPushButton("Set render mode...", self)
|
||||
set_framerange_btn = QtWidgets.QPushButton("Set Frame Range", self)
|
||||
set_resolution_btn = QtWidgets.QPushButton("Set Resolution", self)
|
||||
duplicate_with_inputs_btn = QtWidgets.QPushButton(
|
||||
|
|
@ -89,7 +89,6 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
|
||||
layout.addWidget(set_framerange_btn)
|
||||
layout.addWidget(set_resolution_btn)
|
||||
layout.addWidget(rendermode_btn)
|
||||
|
||||
layout.addSpacing(20)
|
||||
|
||||
|
|
@ -106,9 +105,9 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
load_btn.clicked.connect(self.on_load_clicked)
|
||||
manager_btn.clicked.connect(self.on_manager_clicked)
|
||||
libload_btn.clicked.connect(self.on_libload_clicked)
|
||||
rendermode_btn.clicked.connect(self.on_rendermode_clicked)
|
||||
duplicate_with_inputs_btn.clicked.connect(
|
||||
self.on_duplicate_with_inputs_clicked)
|
||||
self.on_duplicate_with_inputs_clicked
|
||||
)
|
||||
set_resolution_btn.clicked.connect(self.on_set_resolution_clicked)
|
||||
set_framerange_btn.clicked.connect(self.on_set_framerange_clicked)
|
||||
|
||||
|
|
@ -130,7 +129,6 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
self.asset_label.setText(label)
|
||||
|
||||
def register_callback(self, name, fn):
|
||||
|
||||
# Create a wrapper callback that we only store
|
||||
# for as long as we want it to persist as callback
|
||||
def _callback(*args):
|
||||
|
|
@ -146,10 +144,10 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
host_tools.show_workfiles()
|
||||
|
||||
def on_create_clicked(self):
|
||||
host_tools.show_creator()
|
||||
host_tools.show_publisher(tab="create")
|
||||
|
||||
def on_publish_clicked(self):
|
||||
host_tools.show_publish()
|
||||
host_tools.show_publisher(tab="publish")
|
||||
|
||||
def on_load_clicked(self):
|
||||
host_tools.show_loader(use_context=True)
|
||||
|
|
@ -160,15 +158,6 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
def on_libload_clicked(self):
|
||||
host_tools.show_library_loader()
|
||||
|
||||
def on_rendermode_clicked(self):
|
||||
if self.render_mode_widget is None:
|
||||
window = set_rendermode.SetRenderMode()
|
||||
window.setStyleSheet(load_stylesheet())
|
||||
window.show()
|
||||
self.render_mode_widget = window
|
||||
else:
|
||||
self.render_mode_widget.show()
|
||||
|
||||
def on_duplicate_with_inputs_clicked(self):
|
||||
duplicate_with_inputs.duplicate_with_input_connections()
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ Basic avalon integration
|
|||
import os
|
||||
import sys
|
||||
import logging
|
||||
import contextlib
|
||||
|
||||
import pyblish.api
|
||||
from qtpy import QtCore
|
||||
|
|
@ -17,15 +18,14 @@ from openpype.pipeline import (
|
|||
register_loader_plugin_path,
|
||||
register_creator_plugin_path,
|
||||
register_inventory_action_path,
|
||||
deregister_loader_plugin_path,
|
||||
deregister_creator_plugin_path,
|
||||
deregister_inventory_action_path,
|
||||
AVALON_CONTAINER_ID,
|
||||
)
|
||||
from openpype.pipeline.load import any_outdated_containers
|
||||
from openpype.hosts.fusion import FUSION_HOST_DIR
|
||||
from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost
|
||||
from openpype.tools.utils import host_tools
|
||||
|
||||
|
||||
from .lib import (
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk,
|
||||
|
|
@ -66,94 +66,98 @@ class FusionLogHandler(logging.Handler):
|
|||
self.print(entry)
|
||||
|
||||
|
||||
def install():
|
||||
"""Install fusion-specific functionality of OpenPype.
|
||||
class FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
||||
name = "fusion"
|
||||
|
||||
This is where you install menus and register families, data
|
||||
and loaders into fusion.
|
||||
def install(self):
|
||||
"""Install fusion-specific functionality of OpenPype.
|
||||
|
||||
It is called automatically when installing via
|
||||
`openpype.pipeline.install_host(openpype.hosts.fusion.api)`
|
||||
This is where you install menus and register families, data
|
||||
and loaders into fusion.
|
||||
|
||||
See the Maya equivalent for inspiration on how to implement this.
|
||||
It is called automatically when installing via
|
||||
`openpype.pipeline.install_host(openpype.hosts.fusion.api)`
|
||||
|
||||
"""
|
||||
# Remove all handlers associated with the root logger object, because
|
||||
# that one always logs as "warnings" incorrectly.
|
||||
for handler in logging.root.handlers[:]:
|
||||
logging.root.removeHandler(handler)
|
||||
See the Maya equivalent for inspiration on how to implement this.
|
||||
|
||||
# Attach default logging handler that prints to active comp
|
||||
logger = logging.getLogger()
|
||||
formatter = logging.Formatter(fmt="%(message)s\n")
|
||||
handler = FusionLogHandler()
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
"""
|
||||
# Remove all handlers associated with the root logger object, because
|
||||
# that one always logs as "warnings" incorrectly.
|
||||
for handler in logging.root.handlers[:]:
|
||||
logging.root.removeHandler(handler)
|
||||
|
||||
pyblish.api.register_host("fusion")
|
||||
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||
log.info("Registering Fusion plug-ins..")
|
||||
# Attach default logging handler that prints to active comp
|
||||
logger = logging.getLogger()
|
||||
formatter = logging.Formatter(fmt="%(message)s\n")
|
||||
handler = FusionLogHandler()
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
register_loader_plugin_path(LOAD_PATH)
|
||||
register_creator_plugin_path(CREATE_PATH)
|
||||
register_inventory_action_path(INVENTORY_PATH)
|
||||
pyblish.api.register_host("fusion")
|
||||
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||
log.info("Registering Fusion plug-ins..")
|
||||
|
||||
pyblish.api.register_callback(
|
||||
"instanceToggled", on_pyblish_instance_toggled
|
||||
)
|
||||
register_loader_plugin_path(LOAD_PATH)
|
||||
register_creator_plugin_path(CREATE_PATH)
|
||||
register_inventory_action_path(INVENTORY_PATH)
|
||||
|
||||
# Register events
|
||||
register_event_callback("open", on_after_open)
|
||||
register_event_callback("save", on_save)
|
||||
register_event_callback("new", on_new)
|
||||
# Register events
|
||||
register_event_callback("open", on_after_open)
|
||||
register_event_callback("save", on_save)
|
||||
register_event_callback("new", on_new)
|
||||
|
||||
# region workfile io api
|
||||
def has_unsaved_changes(self):
|
||||
comp = get_current_comp()
|
||||
return comp.GetAttrs()["COMPB_Modified"]
|
||||
|
||||
def uninstall():
|
||||
"""Uninstall all that was installed
|
||||
def get_workfile_extensions(self):
|
||||
return [".comp"]
|
||||
|
||||
This is where you undo everything that was done in `install()`.
|
||||
That means, removing menus, deregistering families and data
|
||||
and everything. It should be as though `install()` was never run,
|
||||
because odds are calling this function means the user is interested
|
||||
in re-installing shortly afterwards. If, for example, he has been
|
||||
modifying the menu or registered families.
|
||||
def save_workfile(self, dst_path=None):
|
||||
comp = get_current_comp()
|
||||
comp.Save(dst_path)
|
||||
|
||||
"""
|
||||
pyblish.api.deregister_host("fusion")
|
||||
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
|
||||
log.info("Deregistering Fusion plug-ins..")
|
||||
def open_workfile(self, filepath):
|
||||
# Hack to get fusion, see
|
||||
# openpype.hosts.fusion.api.pipeline.get_current_comp()
|
||||
fusion = getattr(sys.modules["__main__"], "fusion", None)
|
||||
|
||||
deregister_loader_plugin_path(LOAD_PATH)
|
||||
deregister_creator_plugin_path(CREATE_PATH)
|
||||
deregister_inventory_action_path(INVENTORY_PATH)
|
||||
return fusion.LoadComp(filepath)
|
||||
|
||||
pyblish.api.deregister_callback(
|
||||
"instanceToggled", on_pyblish_instance_toggled
|
||||
)
|
||||
def get_current_workfile(self):
|
||||
comp = get_current_comp()
|
||||
current_filepath = comp.GetAttrs()["COMPS_FileName"]
|
||||
if not current_filepath:
|
||||
return None
|
||||
|
||||
return current_filepath
|
||||
|
||||
def on_pyblish_instance_toggled(instance, old_value, new_value):
|
||||
"""Toggle saver tool passthrough states on instance toggles."""
|
||||
comp = instance.context.data.get("currentComp")
|
||||
if not comp:
|
||||
return
|
||||
def work_root(self, session):
|
||||
work_dir = session["AVALON_WORKDIR"]
|
||||
scene_dir = session.get("AVALON_SCENEDIR")
|
||||
if scene_dir:
|
||||
return os.path.join(work_dir, scene_dir)
|
||||
else:
|
||||
return work_dir
|
||||
# endregion
|
||||
|
||||
savers = [tool for tool in instance if
|
||||
getattr(tool, "ID", None) == "Saver"]
|
||||
if not savers:
|
||||
return
|
||||
@contextlib.contextmanager
|
||||
def maintained_selection(self):
|
||||
from .lib import maintained_selection
|
||||
return maintained_selection()
|
||||
|
||||
# Whether instances should be passthrough based on new value
|
||||
passthrough = not new_value
|
||||
with comp_lock_and_undo_chunk(comp,
|
||||
undo_queue_name="Change instance "
|
||||
"active state"):
|
||||
for tool in savers:
|
||||
attrs = tool.GetAttrs()
|
||||
current = attrs["TOOLB_PassThrough"]
|
||||
if current != passthrough:
|
||||
tool.SetAttrs({"TOOLB_PassThrough": passthrough})
|
||||
def get_containers(self):
|
||||
return ls()
|
||||
|
||||
def update_context_data(self, data, changes):
|
||||
comp = get_current_comp()
|
||||
comp.SetData("openpype", data)
|
||||
|
||||
def get_context_data(self):
|
||||
comp = get_current_comp()
|
||||
return comp.GetData("openpype") or {}
|
||||
|
||||
|
||||
def on_new(event):
|
||||
|
|
@ -283,9 +287,51 @@ def parse_container(tool):
|
|||
return container
|
||||
|
||||
|
||||
# TODO: Function below is currently unused prototypes
|
||||
def list_instances(creator_id=None):
|
||||
"""Return created instances in current workfile which will be published.
|
||||
Returns:
|
||||
(list) of dictionaries matching instances format
|
||||
"""
|
||||
|
||||
comp = get_current_comp()
|
||||
tools = comp.GetToolList(False).values()
|
||||
|
||||
instance_signature = {
|
||||
"id": "pyblish.avalon.instance",
|
||||
"identifier": creator_id
|
||||
}
|
||||
instances = []
|
||||
for tool in tools:
|
||||
|
||||
data = tool.GetData('openpype')
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
|
||||
if data.get("id") != instance_signature["id"]:
|
||||
continue
|
||||
|
||||
if creator_id and data.get("identifier") != creator_id:
|
||||
continue
|
||||
|
||||
instances.append(tool)
|
||||
|
||||
return instances
|
||||
|
||||
|
||||
# TODO: Function below is currently unused prototypes
|
||||
def remove_instance(instance):
|
||||
"""Remove instance from current workfile.
|
||||
|
||||
Args:
|
||||
instance (dict): instance representation from subsetmanager model
|
||||
"""
|
||||
# Assume instance is a Fusion tool directly
|
||||
instance["tool"].Delete()
|
||||
|
||||
|
||||
class FusionEventThread(QtCore.QThread):
|
||||
"""QThread which will periodically ping Fusion app for any events.
|
||||
|
||||
The fusion.UIManager must be set up to be notified of events before they'll
|
||||
be reported by this thread, for example:
|
||||
fusion.UIManager.AddNotify("Comp_Save", None)
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
"""Host API required Work Files tool"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
from .lib import get_current_comp
|
||||
|
||||
|
||||
def file_extensions():
|
||||
return [".comp"]
|
||||
|
||||
|
||||
def has_unsaved_changes():
|
||||
comp = get_current_comp()
|
||||
return comp.GetAttrs()["COMPB_Modified"]
|
||||
|
||||
|
||||
def save_file(filepath):
|
||||
comp = get_current_comp()
|
||||
comp.Save(filepath)
|
||||
|
||||
|
||||
def open_file(filepath):
|
||||
# Hack to get fusion, see
|
||||
# openpype.hosts.fusion.api.pipeline.get_current_comp()
|
||||
fusion = getattr(sys.modules["__main__"], "fusion", None)
|
||||
|
||||
return fusion.LoadComp(filepath)
|
||||
|
||||
|
||||
def current_file():
|
||||
comp = get_current_comp()
|
||||
current_filepath = comp.GetAttrs()["COMPS_FileName"]
|
||||
if not current_filepath:
|
||||
return None
|
||||
|
||||
return current_filepath
|
||||
|
||||
|
||||
def work_root(session):
|
||||
work_dir = session["AVALON_WORKDIR"]
|
||||
scene_dir = session.get("AVALON_SCENEDIR")
|
||||
if scene_dir:
|
||||
return os.path.join(work_dir, scene_dir)
|
||||
else:
|
||||
return work_dir
|
||||
|
|
@ -13,11 +13,11 @@ def main(env):
|
|||
# However the contents of that folder can conflict with Qt library dlls
|
||||
# so we make sure to move out of it to avoid DLL Load Failed errors.
|
||||
os.chdir("..")
|
||||
from openpype.hosts.fusion import api
|
||||
from openpype.hosts.fusion.api import FusionHost
|
||||
from openpype.hosts.fusion.api import menu
|
||||
|
||||
# activate resolve from pype
|
||||
install_host(api)
|
||||
install_host(FusionHost())
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
log.info(f"Registered host: {registered_host()}")
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
{
|
||||
Locked = true,
|
||||
Global = {
|
||||
Paths = {
|
||||
Map = {
|
||||
["OpenPype:"] = "$(OPENPYPE_FUSION)/deploy",
|
||||
["Reactor:"] = "$(REACTOR)",
|
||||
|
||||
["Config:"] = "UserPaths:Config;OpenPype:Config",
|
||||
["Scripts:"] = "UserPaths:Scripts;Reactor:System/Scripts;OpenPype:Scripts",
|
||||
["UserPaths:"] = "UserData:;AllData:;Fusion:;Reactor:Deploy"
|
||||
},
|
||||
},
|
||||
Script = {
|
||||
PythonVersion = 3,
|
||||
Python3Forced = true
|
||||
},
|
||||
Paths = {
|
||||
Map = {
|
||||
["OpenPype:"] = "$(OPENPYPE_FUSION)/deploy",
|
||||
["Config:"] = "UserPaths:Config;OpenPype:Config",
|
||||
["Scripts:"] = "UserPaths:Scripts;Reactor:System/Scripts;OpenPype:Scripts",
|
||||
},
|
||||
}
|
||||
},
|
||||
Script = {
|
||||
PythonVersion = 3,
|
||||
Python3Forced = true
|
||||
},
|
||||
UserInterface = {
|
||||
Language = "en_US"
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
import platform
|
||||
from openpype.lib import PreLaunchHook
|
||||
|
||||
from openpype.lib import PreLaunchHook, ApplicationLaunchFailed
|
||||
from openpype.pipeline.colorspace import get_imageio_config
|
||||
from openpype.pipeline.template_data import get_template_data_with_names
|
||||
|
||||
|
||||
class FusionPreLaunchOCIO(PreLaunchHook):
|
||||
|
|
@ -11,24 +11,22 @@ class FusionPreLaunchOCIO(PreLaunchHook):
|
|||
def execute(self):
|
||||
"""Hook entry method."""
|
||||
|
||||
# get image io
|
||||
project_settings = self.data["project_settings"]
|
||||
template_data = get_template_data_with_names(
|
||||
project_name=self.data["project_name"],
|
||||
asset_name=self.data["asset_name"],
|
||||
task_name=self.data["task_name"],
|
||||
host_name=self.host_name,
|
||||
system_settings=self.data["system_settings"]
|
||||
)
|
||||
|
||||
# make sure anatomy settings are having flame key
|
||||
imageio_fusion = project_settings["fusion"]["imageio"]
|
||||
|
||||
ocio = imageio_fusion.get("ocio")
|
||||
enabled = ocio.get("enabled", False)
|
||||
if not enabled:
|
||||
return
|
||||
|
||||
platform_key = platform.system().lower()
|
||||
ocio_path = ocio["configFilePath"][platform_key]
|
||||
if not ocio_path:
|
||||
raise ApplicationLaunchFailed(
|
||||
"Fusion OCIO is enabled in project settings but no OCIO config"
|
||||
f"path is set for your current platform: {platform_key}"
|
||||
)
|
||||
config_data = get_imageio_config(
|
||||
project_name=self.data["project_name"],
|
||||
host_name=self.host_name,
|
||||
project_settings=self.data["project_settings"],
|
||||
anatomy_data=template_data,
|
||||
anatomy=self.data["anatomy"]
|
||||
)
|
||||
ocio_path = config_data["path"]
|
||||
|
||||
self.log.info(f"Setting OCIO config path: {ocio_path}")
|
||||
self.launch_context.env["OCIO"] = os.pathsep.join(ocio_path)
|
||||
self.launch_context.env["OCIO"] = ocio_path
|
||||
|
|
|
|||
161
openpype/hosts/fusion/hooks/pre_fusion_profile_hook.py
Normal file
161
openpype/hosts/fusion/hooks/pre_fusion_profile_hook.py
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import os
|
||||
import shutil
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from openpype.lib import PreLaunchHook, ApplicationLaunchFailed
|
||||
from openpype.hosts.fusion import (
|
||||
FUSION_HOST_DIR,
|
||||
FUSION_VERSIONS_DICT,
|
||||
get_fusion_version,
|
||||
)
|
||||
|
||||
|
||||
class FusionCopyPrefsPrelaunch(PreLaunchHook):
|
||||
"""
|
||||
Prepares local Fusion profile directory, copies existing Fusion profile.
|
||||
This also sets FUSION MasterPrefs variable, which is used
|
||||
to apply Master.prefs file to override some Fusion profile settings to:
|
||||
- enable the OpenPype menu
|
||||
- force Python 3 over Python 2
|
||||
- force English interface
|
||||
Master.prefs is defined in openpype/hosts/fusion/deploy/fusion_shared.prefs
|
||||
"""
|
||||
|
||||
app_groups = ["fusion"]
|
||||
order = 2
|
||||
|
||||
def get_fusion_profile_name(self, profile_version) -> str:
|
||||
# Returns 'Default', unless FUSION16_PROFILE is set
|
||||
return os.getenv(f"FUSION{profile_version}_PROFILE", "Default")
|
||||
|
||||
def get_fusion_profile_dir(self, profile_version) -> Path:
|
||||
# Get FUSION_PROFILE_DIR variable
|
||||
fusion_profile = self.get_fusion_profile_name(profile_version)
|
||||
fusion_var_prefs_dir = os.getenv(
|
||||
f"FUSION{profile_version}_PROFILE_DIR"
|
||||
)
|
||||
|
||||
# Check if FUSION_PROFILE_DIR exists
|
||||
if fusion_var_prefs_dir and Path(fusion_var_prefs_dir).is_dir():
|
||||
fu_prefs_dir = Path(fusion_var_prefs_dir, fusion_profile)
|
||||
self.log.info(f"{fusion_var_prefs_dir} is set to {fu_prefs_dir}")
|
||||
return fu_prefs_dir
|
||||
|
||||
def get_profile_source(self, profile_version) -> Path:
|
||||
"""Get Fusion preferences profile location.
|
||||
See Per-User_Preferences_and_Paths on VFXpedia for reference.
|
||||
"""
|
||||
fusion_profile = self.get_fusion_profile_name(profile_version)
|
||||
profile_source = self.get_fusion_profile_dir(profile_version)
|
||||
if profile_source:
|
||||
return profile_source
|
||||
# otherwise get default location of the profile folder
|
||||
fu_prefs_dir = f"Blackmagic Design/Fusion/Profiles/{fusion_profile}"
|
||||
if platform.system() == "Windows":
|
||||
profile_source = Path(os.getenv("AppData"), fu_prefs_dir)
|
||||
elif platform.system() == "Darwin":
|
||||
profile_source = Path(
|
||||
"~/Library/Application Support/", fu_prefs_dir
|
||||
).expanduser()
|
||||
elif platform.system() == "Linux":
|
||||
profile_source = Path("~/.fusion", fu_prefs_dir).expanduser()
|
||||
self.log.info(
|
||||
f"Locating source Fusion prefs directory: {profile_source}"
|
||||
)
|
||||
return profile_source
|
||||
|
||||
def get_copy_fusion_prefs_settings(self):
|
||||
# Get copy preferences options from the global application settings
|
||||
|
||||
copy_fusion_settings = self.data["project_settings"]["fusion"].get(
|
||||
"copy_fusion_settings", {}
|
||||
)
|
||||
if not copy_fusion_settings:
|
||||
self.log.error("Copy prefs settings not found")
|
||||
copy_status = copy_fusion_settings.get("copy_status", False)
|
||||
force_sync = copy_fusion_settings.get("force_sync", False)
|
||||
copy_path = copy_fusion_settings.get("copy_path") or None
|
||||
if copy_path:
|
||||
copy_path = Path(copy_path).expanduser()
|
||||
return copy_status, copy_path, force_sync
|
||||
|
||||
def copy_fusion_profile(
|
||||
self, copy_from: Path, copy_to: Path, force_sync: bool
|
||||
) -> None:
|
||||
"""On the first Fusion launch copy the contents of Fusion profile
|
||||
directory to the working predefined location. If the Openpype profile
|
||||
folder exists, skip copying, unless re-sync is checked.
|
||||
If the prefs were not copied on the first launch,
|
||||
clean Fusion profile will be created in fu_profile_dir.
|
||||
"""
|
||||
if copy_to.exists() and not force_sync:
|
||||
self.log.info(
|
||||
"Destination Fusion preferences folder already exists: "
|
||||
f"{copy_to} "
|
||||
)
|
||||
return
|
||||
self.log.info("Starting copying Fusion preferences")
|
||||
self.log.debug(f"force_sync option is set to {force_sync}")
|
||||
try:
|
||||
copy_to.mkdir(exist_ok=True, parents=True)
|
||||
except PermissionError:
|
||||
self.log.warning(f"Creating the folder not permitted at {copy_to}")
|
||||
return
|
||||
if not copy_from.exists():
|
||||
self.log.warning(f"Fusion preferences not found in {copy_from}")
|
||||
return
|
||||
for file in copy_from.iterdir():
|
||||
if file.suffix in (
|
||||
".prefs",
|
||||
".def",
|
||||
".blocklist",
|
||||
".fu",
|
||||
".toolbars",
|
||||
):
|
||||
# convert Path to str to be compatible with Python 3.6+
|
||||
shutil.copy(str(file), str(copy_to))
|
||||
self.log.info(
|
||||
f"Successfully copied preferences: {copy_from} to {copy_to}"
|
||||
)
|
||||
|
||||
def execute(self):
|
||||
(
|
||||
copy_status,
|
||||
fu_profile_dir,
|
||||
force_sync,
|
||||
) = self.get_copy_fusion_prefs_settings()
|
||||
|
||||
# Get launched application context and return correct app version
|
||||
app_name = self.launch_context.env.get("AVALON_APP_NAME")
|
||||
app_version = get_fusion_version(app_name)
|
||||
if app_version is None:
|
||||
version_names = ", ".join(str(x) for x in FUSION_VERSIONS_DICT)
|
||||
raise ApplicationLaunchFailed(
|
||||
"Unable to detect valid Fusion version number from app "
|
||||
f"name: {app_name}.\nMake sure to include at least a digit "
|
||||
"to indicate the Fusion version like '18'.\n"
|
||||
f"Detectable Fusion versions are: {version_names}"
|
||||
)
|
||||
|
||||
_, profile_version = FUSION_VERSIONS_DICT[app_version]
|
||||
fu_profile = self.get_fusion_profile_name(profile_version)
|
||||
|
||||
# do a copy of Fusion profile if copy_status toggle is enabled
|
||||
if copy_status and fu_profile_dir is not None:
|
||||
profile_source = self.get_profile_source(profile_version)
|
||||
dest_folder = Path(fu_profile_dir, fu_profile)
|
||||
self.copy_fusion_profile(profile_source, dest_folder, force_sync)
|
||||
|
||||
# Add temporary profile directory variables to customize Fusion
|
||||
# to define where it can read custom scripts and tools from
|
||||
fu_profile_dir_variable = f"FUSION{profile_version}_PROFILE_DIR"
|
||||
self.log.info(f"Setting {fu_profile_dir_variable}: {fu_profile_dir}")
|
||||
self.launch_context.env[fu_profile_dir_variable] = str(fu_profile_dir)
|
||||
|
||||
# Add custom Fusion Master Prefs and the temporary
|
||||
# profile directory variables to customize Fusion
|
||||
# to define where it can read custom scripts and tools from
|
||||
master_prefs_variable = f"FUSION{profile_version}_MasterPrefs"
|
||||
master_prefs = Path(FUSION_HOST_DIR, "deploy", "fusion_shared.prefs")
|
||||
self.log.info(f"Setting {master_prefs_variable}: {master_prefs}")
|
||||
self.launch_context.env[master_prefs_variable] = str(master_prefs)
|
||||
|
|
@ -1,32 +1,43 @@
|
|||
import os
|
||||
from openpype.lib import PreLaunchHook, ApplicationLaunchFailed
|
||||
from openpype.hosts.fusion import FUSION_HOST_DIR
|
||||
from openpype.hosts.fusion import (
|
||||
FUSION_HOST_DIR,
|
||||
FUSION_VERSIONS_DICT,
|
||||
get_fusion_version,
|
||||
)
|
||||
|
||||
|
||||
class FusionPrelaunch(PreLaunchHook):
|
||||
"""Prepares OpenPype Fusion environment
|
||||
|
||||
Requires FUSION_PYTHON3_HOME to be defined in the environment for Fusion
|
||||
to point at a valid Python 3 build for Fusion. That is Python 3.3-3.10
|
||||
for Fusion 18 and Fusion 3.6 for Fusion 16 and 17.
|
||||
|
||||
This also sets FUSION16_MasterPrefs to apply the fusion master prefs
|
||||
as set in openpype/hosts/fusion/deploy/fusion_shared.prefs to enable
|
||||
the OpenPype menu and force Python 3 over Python 2.
|
||||
|
||||
"""
|
||||
Prepares OpenPype Fusion environment.
|
||||
Requires correct Python home variable to be defined in the environment
|
||||
settings for Fusion to point at a valid Python 3 build for Fusion.
|
||||
Python3 versions that are supported by Fusion:
|
||||
Fusion 9, 16, 17 : Python 3.6
|
||||
Fusion 18 : Python 3.6 - 3.10
|
||||
"""
|
||||
|
||||
app_groups = ["fusion"]
|
||||
order = 1
|
||||
|
||||
def execute(self):
|
||||
# making sure python 3 is installed at provided path
|
||||
# Py 3.3-3.10 for Fusion 18+ or Py 3.6 for Fu 16-17
|
||||
py3_var = "FUSION_PYTHON3_HOME"
|
||||
app_data = self.launch_context.env.get("AVALON_APP_NAME")
|
||||
app_version = get_fusion_version(app_data)
|
||||
if not app_version:
|
||||
raise ApplicationLaunchFailed(
|
||||
"Fusion version information not found in System settings.\n"
|
||||
"The key field in the 'applications/fusion/variants' should "
|
||||
"consist a number, corresponding to major Fusion version."
|
||||
)
|
||||
py3_var, _ = FUSION_VERSIONS_DICT[app_version]
|
||||
fusion_python3_home = self.launch_context.env.get(py3_var, "")
|
||||
|
||||
self.log.info(f"Looking for Python 3 in: {fusion_python3_home}")
|
||||
for path in fusion_python3_home.split(os.pathsep):
|
||||
# Allow defining multiple paths to allow "fallback" to other
|
||||
# path. But make to set only a single path as final variable.
|
||||
# Allow defining multiple paths, separated by os.pathsep,
|
||||
# to allow "fallback" to other path.
|
||||
# But make to set only a single path as final variable.
|
||||
py3_dir = os.path.normpath(path)
|
||||
if os.path.isdir(py3_dir):
|
||||
break
|
||||
|
|
@ -43,19 +54,10 @@ class FusionPrelaunch(PreLaunchHook):
|
|||
self.launch_context.env[py3_var] = py3_dir
|
||||
|
||||
# Fusion 18+ requires FUSION_PYTHON3_HOME to also be on PATH
|
||||
self.launch_context.env["PATH"] += ";" + py3_dir
|
||||
if app_version >= 18:
|
||||
self.launch_context.env["PATH"] += os.pathsep + py3_dir
|
||||
|
||||
# Fusion 16 and 17 use FUSION16_PYTHON36_HOME instead of
|
||||
# FUSION_PYTHON3_HOME and will only work with a Python 3.6 version
|
||||
# TODO: Detect Fusion version to only set for specific Fusion build
|
||||
self.launch_context.env["FUSION16_PYTHON36_HOME"] = py3_dir
|
||||
self.launch_context.env[py3_var] = py3_dir
|
||||
|
||||
# Add our Fusion Master Prefs which is the only way to customize
|
||||
# Fusion to define where it can read custom scripts and tools from
|
||||
self.log.info(f"Setting OPENPYPE_FUSION: {FUSION_HOST_DIR}")
|
||||
self.launch_context.env["OPENPYPE_FUSION"] = FUSION_HOST_DIR
|
||||
|
||||
pref_var = "FUSION16_MasterPrefs" # used by Fusion 16, 17 and 18
|
||||
prefs = os.path.join(FUSION_HOST_DIR, "deploy", "fusion_shared.prefs")
|
||||
self.log.info(f"Setting {pref_var}: {prefs}")
|
||||
self.launch_context.env[pref_var] = prefs
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
import os
|
||||
|
||||
from openpype.pipeline import (
|
||||
LegacyCreator,
|
||||
legacy_io
|
||||
)
|
||||
from openpype.hosts.fusion.api import (
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk
|
||||
)
|
||||
|
||||
|
||||
class CreateOpenEXRSaver(LegacyCreator):
|
||||
|
||||
name = "openexrDefault"
|
||||
label = "Create OpenEXR Saver"
|
||||
hosts = ["fusion"]
|
||||
family = "render"
|
||||
defaults = ["Main"]
|
||||
|
||||
def process(self):
|
||||
|
||||
file_format = "OpenEXRFormat"
|
||||
|
||||
comp = get_current_comp()
|
||||
|
||||
workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"])
|
||||
|
||||
filename = "{}..exr".format(self.name)
|
||||
filepath = os.path.join(workdir, "render", filename)
|
||||
|
||||
with comp_lock_and_undo_chunk(comp):
|
||||
args = (-32768, -32768) # Magical position numbers
|
||||
saver = comp.AddTool("Saver", *args)
|
||||
saver.SetAttrs({"TOOLS_Name": self.name})
|
||||
|
||||
# Setting input attributes is different from basic attributes
|
||||
# Not confused with "MainInputAttributes" which
|
||||
saver["Clip"] = filepath
|
||||
saver["OutputFormat"] = file_format
|
||||
|
||||
# Check file format settings are available
|
||||
if saver[file_format] is None:
|
||||
raise RuntimeError("File format is not set to {}, "
|
||||
"this is a bug".format(file_format))
|
||||
|
||||
# Set file format attributes
|
||||
saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other
|
||||
saver[file_format]["SaveAlpha"] = 0
|
||||
244
openpype/hosts/fusion/plugins/create/create_saver.py
Normal file
244
openpype/hosts/fusion/plugins/create/create_saver.py
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
import os
|
||||
|
||||
import qtawesome
|
||||
|
||||
from openpype.hosts.fusion.api import (
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk,
|
||||
)
|
||||
|
||||
from openpype.lib import (
|
||||
BoolDef,
|
||||
EnumDef,
|
||||
)
|
||||
from openpype.pipeline import (
|
||||
legacy_io,
|
||||
Creator,
|
||||
CreatedInstance,
|
||||
)
|
||||
from openpype.client import (
|
||||
get_asset_by_name,
|
||||
)
|
||||
|
||||
|
||||
class CreateSaver(Creator):
|
||||
identifier = "io.openpype.creators.fusion.saver"
|
||||
label = "Render (saver)"
|
||||
name = "render"
|
||||
family = "render"
|
||||
default_variants = ["Main", "Mask"]
|
||||
description = "Fusion Saver to generate image sequence"
|
||||
|
||||
instance_attributes = ["reviewable"]
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
# TODO: Add pre_create attributes to choose file format?
|
||||
file_format = "OpenEXRFormat"
|
||||
|
||||
comp = get_current_comp()
|
||||
with comp_lock_and_undo_chunk(comp):
|
||||
args = (-32768, -32768) # Magical position numbers
|
||||
saver = comp.AddTool("Saver", *args)
|
||||
|
||||
instance_data["subset"] = subset_name
|
||||
self._update_tool_with_data(saver, data=instance_data)
|
||||
|
||||
saver["OutputFormat"] = file_format
|
||||
|
||||
# Check file format settings are available
|
||||
if saver[file_format] is None:
|
||||
raise RuntimeError(
|
||||
f"File format is not set to {file_format}, this is a bug"
|
||||
)
|
||||
|
||||
# Set file format attributes
|
||||
saver[file_format]["Depth"] = 0 # Auto | float16 | float32
|
||||
# TODO Is this needed?
|
||||
saver[file_format]["SaveAlpha"] = 1
|
||||
|
||||
self._imprint(saver, instance_data)
|
||||
|
||||
# Register the CreatedInstance
|
||||
instance = CreatedInstance(
|
||||
family=self.family,
|
||||
subset_name=subset_name,
|
||||
data=instance_data,
|
||||
creator=self,
|
||||
)
|
||||
|
||||
# Insert the transient data
|
||||
instance.transient_data["tool"] = saver
|
||||
|
||||
self._add_instance_to_context(instance)
|
||||
|
||||
return instance
|
||||
|
||||
def collect_instances(self):
|
||||
comp = get_current_comp()
|
||||
tools = comp.GetToolList(False, "Saver").values()
|
||||
for tool in tools:
|
||||
data = self.get_managed_tool_data(tool)
|
||||
if not data:
|
||||
data = self._collect_unmanaged_saver(tool)
|
||||
|
||||
# Add instance
|
||||
created_instance = CreatedInstance.from_existing(data, self)
|
||||
|
||||
# Collect transient data
|
||||
created_instance.transient_data["tool"] = tool
|
||||
|
||||
self._add_instance_to_context(created_instance)
|
||||
|
||||
def get_icon(self):
|
||||
return qtawesome.icon("fa.eye", color="white")
|
||||
|
||||
def update_instances(self, update_list):
|
||||
for created_inst, _changes in update_list:
|
||||
new_data = created_inst.data_to_store()
|
||||
tool = created_inst.transient_data["tool"]
|
||||
self._update_tool_with_data(tool, new_data)
|
||||
self._imprint(tool, new_data)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
for instance in instances:
|
||||
# Remove the tool from the scene
|
||||
|
||||
tool = instance.transient_data["tool"]
|
||||
if tool:
|
||||
tool.Delete()
|
||||
|
||||
# Remove the collected CreatedInstance to remove from UI directly
|
||||
self._remove_instance_from_context(instance)
|
||||
|
||||
def _imprint(self, tool, data):
|
||||
# Save all data in a "openpype.{key}" = value data
|
||||
|
||||
active = data.pop("active", None)
|
||||
if active is not None:
|
||||
# Use active value to set the passthrough state
|
||||
tool.SetAttrs({"TOOLB_PassThrough": not active})
|
||||
|
||||
for key, value in data.items():
|
||||
tool.SetData(f"openpype.{key}", value)
|
||||
|
||||
def _update_tool_with_data(self, tool, data):
|
||||
"""Update tool node name and output path based on subset data"""
|
||||
if "subset" not in data:
|
||||
return
|
||||
|
||||
original_subset = tool.GetData("openpype.subset")
|
||||
subset = data["subset"]
|
||||
if original_subset != subset:
|
||||
# Subset change detected
|
||||
# Update output filepath
|
||||
workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"])
|
||||
filename = f"{subset}..exr"
|
||||
filepath = os.path.join(workdir, "render", subset, filename)
|
||||
tool["Clip"] = filepath
|
||||
|
||||
# Rename tool
|
||||
if tool.Name != subset:
|
||||
print(f"Renaming {tool.Name} -> {subset}")
|
||||
tool.SetAttrs({"TOOLS_Name": subset})
|
||||
|
||||
def _collect_unmanaged_saver(self, tool):
|
||||
# TODO: this should not be done this way - this should actually
|
||||
# get the data as stored on the tool explicitly (however)
|
||||
# that would disallow any 'regular saver' to be collected
|
||||
# unless the instance data is stored on it to begin with
|
||||
|
||||
print("Collecting unmanaged saver..")
|
||||
comp = tool.Comp()
|
||||
|
||||
# Allow regular non-managed savers to also be picked up
|
||||
project = legacy_io.Session["AVALON_PROJECT"]
|
||||
asset = legacy_io.Session["AVALON_ASSET"]
|
||||
task = legacy_io.Session["AVALON_TASK"]
|
||||
|
||||
asset_doc = get_asset_by_name(project_name=project, asset_name=asset)
|
||||
|
||||
path = tool["Clip"][comp.TIME_UNDEFINED]
|
||||
fname = os.path.basename(path)
|
||||
fname, _ext = os.path.splitext(fname)
|
||||
variant = fname.rstrip(".")
|
||||
subset = self.get_subset_name(
|
||||
variant=variant,
|
||||
task_name=task,
|
||||
asset_doc=asset_doc,
|
||||
project_name=project,
|
||||
)
|
||||
|
||||
attrs = tool.GetAttrs()
|
||||
passthrough = attrs["TOOLB_PassThrough"]
|
||||
return {
|
||||
# Required data
|
||||
"project": project,
|
||||
"asset": asset,
|
||||
"subset": subset,
|
||||
"task": task,
|
||||
"variant": variant,
|
||||
"active": not passthrough,
|
||||
"family": self.family,
|
||||
# Unique identifier for instance and this creator
|
||||
"id": "pyblish.avalon.instance",
|
||||
"creator_identifier": self.identifier,
|
||||
}
|
||||
|
||||
def get_managed_tool_data(self, tool):
|
||||
"""Return data of the tool if it matches creator identifier"""
|
||||
data = tool.GetData("openpype")
|
||||
if not isinstance(data, dict):
|
||||
return
|
||||
|
||||
required = {
|
||||
"id": "pyblish.avalon.instance",
|
||||
"creator_identifier": self.identifier,
|
||||
}
|
||||
for key, value in required.items():
|
||||
if key not in data or data[key] != value:
|
||||
return
|
||||
|
||||
# Get active state from the actual tool state
|
||||
attrs = tool.GetAttrs()
|
||||
passthrough = attrs["TOOLB_PassThrough"]
|
||||
data["active"] = not passthrough
|
||||
|
||||
return data
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
"""Settings for create page"""
|
||||
attr_defs = [
|
||||
self._get_render_target_enum(),
|
||||
self._get_reviewable_bool(),
|
||||
]
|
||||
return attr_defs
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
"""Settings for publish page"""
|
||||
attr_defs = [
|
||||
self._get_render_target_enum(),
|
||||
self._get_reviewable_bool(),
|
||||
]
|
||||
return attr_defs
|
||||
|
||||
# These functions below should be moved to another file
|
||||
# so it can be used by other plugins. plugin.py ?
|
||||
|
||||
def _get_render_target_enum(self):
|
||||
rendering_targets = {
|
||||
"local": "Local machine rendering",
|
||||
"frames": "Use existing frames",
|
||||
}
|
||||
if "farm_rendering" in self.instance_attributes:
|
||||
rendering_targets["farm"] = "Farm rendering"
|
||||
|
||||
return EnumDef(
|
||||
"render_target", items=rendering_targets, label="Render target"
|
||||
)
|
||||
|
||||
def _get_reviewable_bool(self):
|
||||
return BoolDef(
|
||||
"review",
|
||||
default=("reviewable" in self.instance_attributes),
|
||||
label="Review",
|
||||
)
|
||||
109
openpype/hosts/fusion/plugins/create/create_workfile.py
Normal file
109
openpype/hosts/fusion/plugins/create/create_workfile.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import qtawesome
|
||||
|
||||
from openpype.hosts.fusion.api import (
|
||||
get_current_comp
|
||||
)
|
||||
from openpype.client import get_asset_by_name
|
||||
from openpype.pipeline import (
|
||||
AutoCreator,
|
||||
CreatedInstance,
|
||||
legacy_io,
|
||||
)
|
||||
|
||||
|
||||
class FusionWorkfileCreator(AutoCreator):
|
||||
identifier = "workfile"
|
||||
family = "workfile"
|
||||
label = "Workfile"
|
||||
|
||||
default_variant = "Main"
|
||||
|
||||
create_allow_context_change = False
|
||||
|
||||
data_key = "openpype_workfile"
|
||||
|
||||
def collect_instances(self):
|
||||
|
||||
comp = get_current_comp()
|
||||
data = comp.GetData(self.data_key)
|
||||
if not data:
|
||||
return
|
||||
|
||||
instance = CreatedInstance(
|
||||
family=self.family,
|
||||
subset_name=data["subset"],
|
||||
data=data,
|
||||
creator=self
|
||||
)
|
||||
instance.transient_data["comp"] = comp
|
||||
|
||||
self._add_instance_to_context(instance)
|
||||
|
||||
def update_instances(self, update_list):
|
||||
for created_inst, _changes in update_list:
|
||||
comp = created_inst.transient_data["comp"]
|
||||
if not hasattr(comp, "SetData"):
|
||||
# Comp is not alive anymore, likely closed by the user
|
||||
self.log.error("Workfile comp not found for existing instance."
|
||||
" Comp might have been closed in the meantime.")
|
||||
continue
|
||||
|
||||
# Imprint data into the comp
|
||||
data = created_inst.data_to_store()
|
||||
comp.SetData(self.data_key, data)
|
||||
|
||||
def create(self, options=None):
|
||||
|
||||
comp = get_current_comp()
|
||||
if not comp:
|
||||
self.log.error("Unable to find current comp")
|
||||
return
|
||||
|
||||
existing_instance = None
|
||||
for instance in self.create_context.instances:
|
||||
if instance.family == self.family:
|
||||
existing_instance = instance
|
||||
break
|
||||
|
||||
project_name = legacy_io.Session["AVALON_PROJECT"]
|
||||
asset_name = legacy_io.Session["AVALON_ASSET"]
|
||||
task_name = legacy_io.Session["AVALON_TASK"]
|
||||
host_name = legacy_io.Session["AVALON_APP"]
|
||||
|
||||
if existing_instance is None:
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
self.default_variant, task_name, asset_doc,
|
||||
project_name, host_name
|
||||
)
|
||||
data = {
|
||||
"asset": asset_name,
|
||||
"task": task_name,
|
||||
"variant": self.default_variant
|
||||
}
|
||||
data.update(self.get_dynamic_data(
|
||||
self.default_variant, task_name, asset_doc,
|
||||
project_name, host_name, None
|
||||
))
|
||||
|
||||
new_instance = CreatedInstance(
|
||||
self.family, subset_name, data, self
|
||||
)
|
||||
new_instance.transient_data["comp"] = comp
|
||||
self._add_instance_to_context(new_instance)
|
||||
|
||||
elif (
|
||||
existing_instance["asset"] != asset_name
|
||||
or existing_instance["task"] != task_name
|
||||
):
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
self.default_variant, task_name, asset_doc,
|
||||
project_name, host_name
|
||||
)
|
||||
existing_instance["asset"] = asset_name
|
||||
existing_instance["task"] = task_name
|
||||
existing_instance["subset"] = subset_name
|
||||
|
||||
def get_icon(self):
|
||||
return qtawesome.icon("fa.file-o", color="white")
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
import os
|
||||
import contextlib
|
||||
|
||||
from openpype.client import get_version_by_id
|
||||
from openpype.pipeline import (
|
||||
load,
|
||||
legacy_io,
|
||||
get_representation_path,
|
||||
import openpype.pipeline.load as load
|
||||
from openpype.pipeline.load import (
|
||||
get_representation_context,
|
||||
get_representation_path_from_context
|
||||
)
|
||||
from openpype.hosts.fusion.api import (
|
||||
imprint_container,
|
||||
|
|
@ -148,7 +146,7 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
namespace = context['asset']['name']
|
||||
|
||||
# Use the first file for now
|
||||
path = self._get_first_image(os.path.dirname(self.fname))
|
||||
path = get_representation_path_from_context(context)
|
||||
|
||||
# Create the Loader with the filename path set
|
||||
comp = get_current_comp()
|
||||
|
|
@ -217,13 +215,11 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
assert tool.ID == "Loader", "Must be Loader"
|
||||
comp = tool.Comp()
|
||||
|
||||
root = os.path.dirname(get_representation_path(representation))
|
||||
path = self._get_first_image(root)
|
||||
context = get_representation_context(representation)
|
||||
path = get_representation_path_from_context(context)
|
||||
|
||||
# Get start frame from version data
|
||||
project_name = legacy_io.active_project()
|
||||
version = get_version_by_id(project_name, representation["parent"])
|
||||
start = self._get_start(version, tool)
|
||||
start = self._get_start(context["version"], tool)
|
||||
|
||||
with comp_lock_and_undo_chunk(comp, "Update Loader"):
|
||||
|
||||
|
|
@ -256,11 +252,6 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
with comp_lock_and_undo_chunk(comp, "Remove Loader"):
|
||||
tool.Delete()
|
||||
|
||||
def _get_first_image(self, root):
|
||||
"""Get first file in representation root"""
|
||||
files = sorted(os.listdir(root))
|
||||
return os.path.join(root, files[0])
|
||||
|
||||
def _get_start(self, version_doc, tool):
|
||||
"""Return real start frame of published files (incl. handles)"""
|
||||
data = version_doc["data"]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.hosts.fusion.api import get_current_comp
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import pyblish.api
|
||||
|
||||
|
||||
def get_comp_render_range(comp):
|
||||
"""Return comp's start-end render range and global start-end range."""
|
||||
comp_attrs = comp.GetAttrs()
|
||||
start = comp_attrs["COMPN_RenderStart"]
|
||||
end = comp_attrs["COMPN_RenderEnd"]
|
||||
global_start = comp_attrs["COMPN_GlobalStart"]
|
||||
global_end = comp_attrs["COMPN_GlobalEnd"]
|
||||
|
||||
# Whenever render ranges are undefined fall back
|
||||
# to the comp's global start and end
|
||||
if start == -1000000000:
|
||||
start = global_start
|
||||
if end == -1000000000:
|
||||
end = global_end
|
||||
|
||||
return start, end, global_start, global_end
|
||||
|
||||
|
||||
class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin):
|
||||
"""Collect current comp"""
|
||||
|
||||
# We run this after CollectorOrder - 0.1 otherwise it gets
|
||||
# overridden by global plug-in `CollectContextEntities`
|
||||
order = pyblish.api.CollectorOrder - 0.05
|
||||
label = "Collect Comp Frame Ranges"
|
||||
hosts = ["fusion"]
|
||||
|
||||
def process(self, context):
|
||||
"""Collect all image sequence tools"""
|
||||
|
||||
comp = context.data["currentComp"]
|
||||
|
||||
# Store comp render ranges
|
||||
start, end, global_start, global_end = get_comp_render_range(comp)
|
||||
context.data["frameStart"] = int(start)
|
||||
context.data["frameEnd"] = int(end)
|
||||
context.data["frameStartHandle"] = int(global_start)
|
||||
context.data["frameEndHandle"] = int(global_end)
|
||||
context.data["handleStart"] = int(start) - int(global_start)
|
||||
context.data["handleEnd"] = int(global_end) - int(end)
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import pyblish.api
|
||||
from openpype.pipeline import publish
|
||||
import os
|
||||
|
||||
|
||||
class CollectFusionExpectedFrames(
|
||||
pyblish.api.InstancePlugin, publish.ColormanagedPyblishPluginMixin
|
||||
):
|
||||
"""Collect all frames needed to publish expected frames"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.5
|
||||
label = "Collect Expected Frames"
|
||||
hosts = ["fusion"]
|
||||
families = ["render"]
|
||||
|
||||
def process(self, instance):
|
||||
context = instance.context
|
||||
|
||||
frame_start = context.data["frameStartHandle"]
|
||||
frame_end = context.data["frameEndHandle"]
|
||||
path = instance.data["path"]
|
||||
output_dir = instance.data["outputDir"]
|
||||
|
||||
basename = os.path.basename(path)
|
||||
head, ext = os.path.splitext(basename)
|
||||
files = [
|
||||
f"{head}{str(frame).zfill(4)}{ext}"
|
||||
for frame in range(frame_start, frame_end + 1)
|
||||
]
|
||||
repre = {
|
||||
"name": ext[1:],
|
||||
"ext": ext[1:],
|
||||
"frameStart": f"%0{len(str(frame_end))}d" % frame_start,
|
||||
"files": files,
|
||||
"stagingDir": output_dir,
|
||||
}
|
||||
|
||||
self.set_representation_colorspace(
|
||||
representation=repre,
|
||||
context=context,
|
||||
)
|
||||
|
||||
# review representation
|
||||
if instance.data.get("review", False):
|
||||
repre["tags"] = ["review"]
|
||||
|
||||
# add the repre to the instance
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
instance.data["representations"].append(repre)
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
from bson.objectid import ObjectId
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import registered_host
|
||||
|
|
@ -97,10 +95,15 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin):
|
|||
label = "Collect Inputs"
|
||||
order = pyblish.api.CollectorOrder + 0.2
|
||||
hosts = ["fusion"]
|
||||
families = ["render"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
# Get all upstream and include itself
|
||||
if not any(instance[:]):
|
||||
self.log.debug("No tool found in instance, skipping..")
|
||||
return
|
||||
|
||||
tool = instance[0]
|
||||
nodes = list(iter_upstream(tool))
|
||||
nodes.append(tool)
|
||||
|
|
@ -108,7 +111,6 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin):
|
|||
# Collect containers for the given set of nodes
|
||||
containers = collect_input_containers(nodes)
|
||||
|
||||
inputs = [ObjectId(c["representation"]) for c in containers]
|
||||
inputs = [c["representation"] for c in containers]
|
||||
instance.data["inputRepresentations"] = inputs
|
||||
|
||||
self.log.info("Collected inputs: %s" % inputs)
|
||||
|
|
|
|||
|
|
@ -3,25 +3,7 @@ import os
|
|||
import pyblish.api
|
||||
|
||||
|
||||
def get_comp_render_range(comp):
|
||||
"""Return comp's start-end render range and global start-end range."""
|
||||
comp_attrs = comp.GetAttrs()
|
||||
start = comp_attrs["COMPN_RenderStart"]
|
||||
end = comp_attrs["COMPN_RenderEnd"]
|
||||
global_start = comp_attrs["COMPN_GlobalStart"]
|
||||
global_end = comp_attrs["COMPN_GlobalEnd"]
|
||||
|
||||
# Whenever render ranges are undefined fall back
|
||||
# to the comp's global start and end
|
||||
if start == -1000000000:
|
||||
start = global_start
|
||||
if end == -1000000000:
|
||||
end = global_end
|
||||
|
||||
return start, end, global_start, global_end
|
||||
|
||||
|
||||
class CollectInstances(pyblish.api.ContextPlugin):
|
||||
class CollectInstanceData(pyblish.api.InstancePlugin):
|
||||
"""Collect Fusion saver instances
|
||||
|
||||
This additionally stores the Comp start and end render range in the
|
||||
|
|
@ -30,77 +12,68 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder
|
||||
label = "Collect Instances"
|
||||
label = "Collect Instances Data"
|
||||
hosts = ["fusion"]
|
||||
|
||||
def process(self, context):
|
||||
def process(self, instance):
|
||||
"""Collect all image sequence tools"""
|
||||
|
||||
from openpype.hosts.fusion.api.lib import get_frame_path
|
||||
context = instance.context
|
||||
|
||||
comp = context.data["currentComp"]
|
||||
# Include creator attributes directly as instance data
|
||||
creator_attributes = instance.data["creator_attributes"]
|
||||
instance.data.update(creator_attributes)
|
||||
|
||||
# Get all savers in the comp
|
||||
tools = comp.GetToolList(False).values()
|
||||
savers = [tool for tool in tools if tool.ID == "Saver"]
|
||||
# Include start and end render frame in label
|
||||
subset = instance.data["subset"]
|
||||
start = context.data["frameStart"]
|
||||
end = context.data["frameEnd"]
|
||||
label = "{subset} ({start}-{end})".format(subset=subset,
|
||||
start=int(start),
|
||||
end=int(end))
|
||||
instance.data.update({
|
||||
"label": label,
|
||||
|
||||
start, end, global_start, global_end = get_comp_render_range(comp)
|
||||
context.data["frameStart"] = int(start)
|
||||
context.data["frameEnd"] = int(end)
|
||||
context.data["frameStartHandle"] = int(global_start)
|
||||
context.data["frameEndHandle"] = int(global_end)
|
||||
# todo: Allow custom frame range per instance
|
||||
"frameStart": context.data["frameStart"],
|
||||
"frameEnd": context.data["frameEnd"],
|
||||
"frameStartHandle": context.data["frameStartHandle"],
|
||||
"frameEndHandle": context.data["frameStartHandle"],
|
||||
"handleStart": context.data["handleStart"],
|
||||
"handleEnd": context.data["handleEnd"],
|
||||
"fps": context.data["fps"],
|
||||
})
|
||||
|
||||
for tool in savers:
|
||||
# Add review family if the instance is marked as 'review'
|
||||
# This could be done through a 'review' Creator attribute.
|
||||
if instance.data.get("review", False):
|
||||
self.log.info("Adding review family..")
|
||||
instance.data["families"].append("review")
|
||||
|
||||
if instance.data["family"] == "render":
|
||||
# TODO: This should probably move into a collector of
|
||||
# its own for the "render" family
|
||||
from openpype.hosts.fusion.api.lib import get_frame_path
|
||||
comp = context.data["currentComp"]
|
||||
|
||||
# This is only the case for savers currently but not
|
||||
# for workfile instances. So we assume saver here.
|
||||
tool = instance.data["transientData"]["tool"]
|
||||
path = tool["Clip"][comp.TIME_UNDEFINED]
|
||||
|
||||
tool_attrs = tool.GetAttrs()
|
||||
active = not tool_attrs["TOOLB_PassThrough"]
|
||||
|
||||
if not path:
|
||||
self.log.warning("Skipping saver because it "
|
||||
"has no path set: {}".format(tool.Name))
|
||||
continue
|
||||
|
||||
filename = os.path.basename(path)
|
||||
head, padding, tail = get_frame_path(filename)
|
||||
ext = os.path.splitext(path)[1]
|
||||
assert tail == ext, ("Tail does not match %s" % ext)
|
||||
subset = head.rstrip("_. ") # subset is head of the filename
|
||||
|
||||
# Include start and end render frame in label
|
||||
label = "{subset} ({start}-{end})".format(subset=subset,
|
||||
start=int(start),
|
||||
end=int(end))
|
||||
|
||||
instance = context.create_instance(subset)
|
||||
instance.data.update({
|
||||
"asset": os.environ["AVALON_ASSET"], # todo: not a constant
|
||||
"subset": subset,
|
||||
"path": path,
|
||||
"outputDir": os.path.dirname(path),
|
||||
"ext": ext, # todo: should be redundant
|
||||
"label": label,
|
||||
"task": context.data["task"],
|
||||
"frameStart": context.data["frameStart"],
|
||||
"frameEnd": context.data["frameEnd"],
|
||||
"frameStartHandle": context.data["frameStartHandle"],
|
||||
"frameEndHandle": context.data["frameStartHandle"],
|
||||
"fps": context.data["fps"],
|
||||
"families": ["render", "review"],
|
||||
"family": "render",
|
||||
"active": active,
|
||||
"publish": active # backwards compatibility
|
||||
"ext": ext, # todo: should be redundant?
|
||||
|
||||
# Backwards compatibility: embed tool in instance.data
|
||||
"tool": tool
|
||||
})
|
||||
|
||||
# Add tool itself as member
|
||||
instance.append(tool)
|
||||
|
||||
self.log.info("Found: \"%s\" " % path)
|
||||
|
||||
# Sort/grouped by family (preserving local index)
|
||||
context[:] = sorted(context, key=self.sort_by_family)
|
||||
|
||||
return context
|
||||
|
||||
def sort_by_family(self, instance):
|
||||
"""Sort by family"""
|
||||
return instance.data.get("families", instance.data.get("family"))
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
import pyblish.api
|
||||
|
||||
|
||||
class CollectFusionRenderMode(pyblish.api.InstancePlugin):
|
||||
"""Collect current comp's render Mode
|
||||
|
||||
Options:
|
||||
local
|
||||
farm
|
||||
|
||||
Note that this value is set for each comp separately. When you save the
|
||||
comp this information will be stored in that file. If for some reason the
|
||||
available tool does not visualize which render mode is set for the
|
||||
current comp, please run the following line in the console (Py2)
|
||||
|
||||
comp.GetData("openpype.rendermode")
|
||||
|
||||
This will return the name of the current render mode as seen above under
|
||||
Options.
|
||||
|
||||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.4
|
||||
label = "Collect Render Mode"
|
||||
hosts = ["fusion"]
|
||||
families = ["render"]
|
||||
|
||||
def process(self, instance):
|
||||
"""Collect all image sequence tools"""
|
||||
options = ["local", "farm"]
|
||||
|
||||
comp = instance.context.data.get("currentComp")
|
||||
if not comp:
|
||||
raise RuntimeError("No comp previously collected, unable to "
|
||||
"retrieve Fusion version.")
|
||||
|
||||
rendermode = comp.GetData("openpype.rendermode") or "local"
|
||||
assert rendermode in options, "Must be supported render mode"
|
||||
|
||||
self.log.info("Render mode: {0}".format(rendermode))
|
||||
|
||||
# Append family
|
||||
family = "render.{0}".format(rendermode)
|
||||
instance.data["families"].append(family)
|
||||
25
openpype/hosts/fusion/plugins/publish/collect_renders.py
Normal file
25
openpype/hosts/fusion/plugins/publish/collect_renders.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import pyblish.api
|
||||
|
||||
|
||||
class CollectFusionRenders(pyblish.api.InstancePlugin):
|
||||
"""Collect current saver node's render Mode
|
||||
|
||||
Options:
|
||||
local (Render locally)
|
||||
frames (Use existing frames)
|
||||
|
||||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.4
|
||||
label = "Collect Renders"
|
||||
hosts = ["fusion"]
|
||||
families = ["render"]
|
||||
|
||||
def process(self, instance):
|
||||
render_target = instance.data["render_target"]
|
||||
family = instance.data["family"]
|
||||
|
||||
# add targeted family to families
|
||||
instance.data["families"].append(
|
||||
"{}.{}".format(family, render_target)
|
||||
)
|
||||
26
openpype/hosts/fusion/plugins/publish/collect_workfile.py
Normal file
26
openpype/hosts/fusion/plugins/publish/collect_workfile.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectFusionWorkfile(pyblish.api.InstancePlugin):
|
||||
"""Collect Fusion workfile representation."""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.1
|
||||
label = "Collect Workfile"
|
||||
hosts = ["fusion"]
|
||||
families = ["workfile"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
current_file = instance.context.data["currentFile"]
|
||||
|
||||
folder, file = os.path.split(current_file)
|
||||
filename, ext = os.path.splitext(file)
|
||||
|
||||
instance.data['representations'] = [{
|
||||
'name': ext.lstrip("."),
|
||||
'ext': ext.lstrip("."),
|
||||
'files': file,
|
||||
"stagingDir": folder,
|
||||
}]
|
||||
109
openpype/hosts/fusion/plugins/publish/extract_render_local.py
Normal file
109
openpype/hosts/fusion/plugins/publish/extract_render_local.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import logging
|
||||
import contextlib
|
||||
import pyblish.api
|
||||
from openpype.hosts.fusion.api import comp_lock_and_undo_chunk
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def enabled_savers(comp, savers):
|
||||
"""Enable only the `savers` in Comp during the context.
|
||||
|
||||
Any Saver tool in the passed composition that is not in the savers list
|
||||
will be set to passthrough during the context.
|
||||
|
||||
Args:
|
||||
comp (object): Fusion composition object.
|
||||
savers (list): List of Saver tool objects.
|
||||
|
||||
"""
|
||||
passthrough_key = "TOOLB_PassThrough"
|
||||
original_states = {}
|
||||
enabled_save_names = {saver.Name for saver in savers}
|
||||
try:
|
||||
all_savers = comp.GetToolList(False, "Saver").values()
|
||||
for saver in all_savers:
|
||||
original_state = saver.GetAttrs()[passthrough_key]
|
||||
original_states[saver] = original_state
|
||||
|
||||
# The passthrough state we want to set (passthrough != enabled)
|
||||
state = saver.Name not in enabled_save_names
|
||||
if state != original_state:
|
||||
saver.SetAttrs({passthrough_key: state})
|
||||
yield
|
||||
finally:
|
||||
for saver, original_state in original_states.items():
|
||||
saver.SetAttrs({"TOOLB_PassThrough": original_state})
|
||||
|
||||
|
||||
class FusionRenderLocal(pyblish.api.InstancePlugin):
|
||||
"""Render the current Fusion composition locally."""
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.2
|
||||
label = "Render Local"
|
||||
hosts = ["fusion"]
|
||||
families = ["render.local"]
|
||||
|
||||
def process(self, instance):
|
||||
context = instance.context
|
||||
|
||||
# Start render
|
||||
self.render_once(context)
|
||||
|
||||
# Log render status
|
||||
self.log.info(
|
||||
"Rendered '{nm}' for asset '{ast}' under the task '{tsk}'".format(
|
||||
nm=instance.data["name"],
|
||||
ast=instance.data["asset"],
|
||||
tsk=instance.data["task"],
|
||||
)
|
||||
)
|
||||
|
||||
def render_once(self, context):
|
||||
"""Render context comp only once, even with more render instances"""
|
||||
|
||||
# This plug-in assumes all render nodes get rendered at the same time
|
||||
# to speed up the rendering. The check below makes sure that we only
|
||||
# execute the rendering once and not for each instance.
|
||||
key = f"__hasRun{self.__class__.__name__}"
|
||||
|
||||
savers_to_render = [
|
||||
# Get the saver tool from the instance
|
||||
instance[0] for instance in context if
|
||||
# Only active instances
|
||||
instance.data.get("publish", True) and
|
||||
# Only render.local instances
|
||||
"render.local" in instance.data["families"]
|
||||
]
|
||||
|
||||
if key not in context.data:
|
||||
# We initialize as false to indicate it wasn't successful yet
|
||||
# so we can keep track of whether Fusion succeeded
|
||||
context.data[key] = False
|
||||
|
||||
current_comp = context.data["currentComp"]
|
||||
frame_start = context.data["frameStartHandle"]
|
||||
frame_end = context.data["frameEndHandle"]
|
||||
|
||||
self.log.info("Starting Fusion render")
|
||||
self.log.info(f"Start frame: {frame_start}")
|
||||
self.log.info(f"End frame: {frame_end}")
|
||||
saver_names = ", ".join(saver.Name for saver in savers_to_render)
|
||||
self.log.info(f"Rendering tools: {saver_names}")
|
||||
|
||||
with comp_lock_and_undo_chunk(current_comp):
|
||||
with enabled_savers(current_comp, savers_to_render):
|
||||
result = current_comp.Render(
|
||||
{
|
||||
"Start": frame_start,
|
||||
"End": frame_end,
|
||||
"Wait": True,
|
||||
}
|
||||
)
|
||||
|
||||
context.data[key] = bool(result)
|
||||
|
||||
if context.data[key] is False:
|
||||
raise RuntimeError("Comp render failed")
|
||||
|
|
@ -11,7 +11,7 @@ class FusionIncrementCurrentFile(pyblish.api.ContextPlugin):
|
|||
label = "Increment current file"
|
||||
order = pyblish.api.IntegratorOrder + 9.0
|
||||
hosts = ["fusion"]
|
||||
families = ["render.farm"]
|
||||
families = ["workfile"]
|
||||
optional = True
|
||||
|
||||
def process(self, context):
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
from openpype.hosts.fusion.api import comp_lock_and_undo_chunk
|
||||
|
||||
|
||||
class Fusionlocal(pyblish.api.InstancePlugin):
|
||||
"""Render the current Fusion composition locally.
|
||||
|
||||
Extract the result of savers by starting a comp render
|
||||
This will run the local render of Fusion.
|
||||
|
||||
"""
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.1
|
||||
label = "Render Local"
|
||||
hosts = ["fusion"]
|
||||
families = ["render.local"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
# This plug-in runs only once and thus assumes all instances
|
||||
# currently will render the same frame range
|
||||
context = instance.context
|
||||
key = f"__hasRun{self.__class__.__name__}"
|
||||
if context.data.get(key, False):
|
||||
return
|
||||
|
||||
context.data[key] = True
|
||||
|
||||
self.render_once(context)
|
||||
|
||||
frame_start = context.data["frameStartHandle"]
|
||||
frame_end = context.data["frameEndHandle"]
|
||||
path = instance.data["path"]
|
||||
output_dir = instance.data["outputDir"]
|
||||
|
||||
basename = os.path.basename(path)
|
||||
head, ext = os.path.splitext(basename)
|
||||
files = [
|
||||
f"{head}{str(frame).zfill(4)}{ext}"
|
||||
for frame in range(frame_start, frame_end + 1)
|
||||
]
|
||||
repre = {
|
||||
'name': ext[1:],
|
||||
'ext': ext[1:],
|
||||
'frameStart': f"%0{len(str(frame_end))}d" % frame_start,
|
||||
'files': files,
|
||||
"stagingDir": output_dir,
|
||||
}
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
instance.data["representations"].append(repre)
|
||||
|
||||
# review representation
|
||||
repre_preview = repre.copy()
|
||||
repre_preview["name"] = repre_preview["ext"] = "mp4"
|
||||
repre_preview["tags"] = ["review", "ftrackreview", "delete"]
|
||||
instance.data["representations"].append(repre_preview)
|
||||
|
||||
def render_once(self, context):
|
||||
"""Render context comp only once, even with more render instances"""
|
||||
|
||||
current_comp = context.data["currentComp"]
|
||||
frame_start = context.data["frameStartHandle"]
|
||||
frame_end = context.data["frameEndHandle"]
|
||||
|
||||
self.log.info("Starting render")
|
||||
self.log.info(f"Start frame: {frame_start}")
|
||||
self.log.info(f"End frame: {frame_end}")
|
||||
|
||||
with comp_lock_and_undo_chunk(current_comp):
|
||||
result = current_comp.Render({
|
||||
"Start": frame_start,
|
||||
"End": frame_end,
|
||||
"Wait": True
|
||||
})
|
||||
|
||||
if not result:
|
||||
raise RuntimeError("Comp render failed")
|
||||
|
|
@ -7,7 +7,7 @@ class FusionSaveComp(pyblish.api.ContextPlugin):
|
|||
label = "Save current file"
|
||||
order = pyblish.api.ExtractorOrder - 0.49
|
||||
hosts = ["fusion"]
|
||||
families = ["render"]
|
||||
families = ["render", "workfile"]
|
||||
|
||||
def process(self, context):
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import RepairAction
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
from openpype.hosts.fusion.api.action import SelectInvalidAction
|
||||
|
||||
|
||||
class ValidateBackgroundDepth(pyblish.api.InstancePlugin):
|
||||
|
|
@ -8,11 +11,12 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin):
|
|||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Background Depth 32 bit"
|
||||
actions = [RepairAction]
|
||||
hosts = ["fusion"]
|
||||
families = ["render"]
|
||||
optional = True
|
||||
|
||||
actions = [SelectInvalidAction, RepairAction]
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
|
||||
|
|
@ -29,8 +33,10 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin):
|
|||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError("Found %i nodes which are not set to float32"
|
||||
% len(invalid))
|
||||
raise PublishValidationError(
|
||||
"Found {} Backgrounds tools which"
|
||||
" are not set to float32".format(len(invalid)),
|
||||
title=self.label)
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
|
||||
class ValidateFusionCompSaved(pyblish.api.ContextPlugin):
|
||||
|
|
@ -19,10 +20,12 @@ class ValidateFusionCompSaved(pyblish.api.ContextPlugin):
|
|||
|
||||
filename = attrs["COMPS_FileName"]
|
||||
if not filename:
|
||||
raise RuntimeError("Comp is not saved.")
|
||||
raise PublishValidationError("Comp is not saved.",
|
||||
title=self.label)
|
||||
|
||||
if not os.path.exists(filename):
|
||||
raise RuntimeError("Comp file does not exist: %s" % filename)
|
||||
raise PublishValidationError(
|
||||
"Comp file does not exist: %s" % filename, title=self.label)
|
||||
|
||||
if attrs["COMPB_Modified"]:
|
||||
self.log.warning("Comp is modified. Save your comp to ensure your "
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import RepairAction
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
from openpype.hosts.fusion.api.action import SelectInvalidAction
|
||||
|
||||
|
||||
class ValidateCreateFolderChecked(pyblish.api.InstancePlugin):
|
||||
|
|
@ -11,28 +14,28 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin):
|
|||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
actions = [RepairAction]
|
||||
label = "Validate Create Folder Checked"
|
||||
families = ["render"]
|
||||
hosts = ["fusion"]
|
||||
actions = [RepairAction, SelectInvalidAction]
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
active = instance.data.get("active", instance.data.get("publish"))
|
||||
if not active:
|
||||
return []
|
||||
|
||||
tool = instance[0]
|
||||
create_dir = tool.GetInput("CreateDir")
|
||||
if create_dir == 0.0:
|
||||
cls.log.error("%s has Create Folder turned off" % instance[0].Name)
|
||||
cls.log.error(
|
||||
"%s has Create Folder turned off" % instance[0].Name
|
||||
)
|
||||
return [tool]
|
||||
|
||||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError("Found Saver with Create Folder During "
|
||||
"Render checked off")
|
||||
raise PublishValidationError(
|
||||
"Found Saver with Create Folder During Render checked off",
|
||||
title=self.label,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import RepairAction
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
from openpype.hosts.fusion.api.action import SelectInvalidAction
|
||||
|
||||
|
||||
class ValidateLocalFramesExistence(pyblish.api.InstancePlugin):
|
||||
"""Checks if files for savers that's set
|
||||
to publish expected frames exists
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Expected Frames Exists"
|
||||
families = ["render"]
|
||||
hosts = ["fusion"]
|
||||
actions = [RepairAction, SelectInvalidAction]
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance, non_existing_frames=None):
|
||||
if non_existing_frames is None:
|
||||
non_existing_frames = []
|
||||
|
||||
if instance.data.get("render_target") == "frames":
|
||||
tool = instance[0]
|
||||
|
||||
frame_start = instance.data["frameStart"]
|
||||
frame_end = instance.data["frameEnd"]
|
||||
path = instance.data["path"]
|
||||
output_dir = instance.data["outputDir"]
|
||||
|
||||
basename = os.path.basename(path)
|
||||
head, ext = os.path.splitext(basename)
|
||||
files = [
|
||||
f"{head}{str(frame).zfill(4)}{ext}"
|
||||
for frame in range(frame_start, frame_end + 1)
|
||||
]
|
||||
|
||||
for file in files:
|
||||
if not os.path.exists(os.path.join(output_dir, file)):
|
||||
cls.log.error(
|
||||
f"Missing file: {os.path.join(output_dir, file)}"
|
||||
)
|
||||
non_existing_frames.append(file)
|
||||
|
||||
if len(non_existing_frames) > 0:
|
||||
cls.log.error(f"Some of {tool.Name}'s files does not exist")
|
||||
return [tool]
|
||||
|
||||
def process(self, instance):
|
||||
non_existing_frames = []
|
||||
invalid = self.get_invalid(instance, non_existing_frames)
|
||||
if invalid:
|
||||
raise PublishValidationError(
|
||||
"{} is set to publish existing frames but "
|
||||
"some frames are missing. "
|
||||
"The missing file(s) are:\n\n{}".format(
|
||||
invalid[0].Name,
|
||||
"\n\n".join(non_existing_frames),
|
||||
),
|
||||
title=self.label,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
invalid = cls.get_invalid(instance)
|
||||
if invalid:
|
||||
tool = invalid[0]
|
||||
|
||||
# Change render target to local to render locally
|
||||
tool.SetData("openpype.creator_attributes.render_target", "local")
|
||||
|
||||
cls.log.info(
|
||||
f"Reload the publisher and {tool.Name} "
|
||||
"will be set to render locally"
|
||||
)
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
from openpype.hosts.fusion.api.action import SelectInvalidAction
|
||||
|
||||
|
||||
class ValidateFilenameHasExtension(pyblish.api.InstancePlugin):
|
||||
|
|
@ -16,11 +19,13 @@ class ValidateFilenameHasExtension(pyblish.api.InstancePlugin):
|
|||
label = "Validate Filename Has Extension"
|
||||
families = ["render"]
|
||||
hosts = ["fusion"]
|
||||
actions = [SelectInvalidAction]
|
||||
|
||||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError("Found Saver without an extension")
|
||||
raise PublishValidationError("Found Saver without an extension",
|
||||
title=self.label)
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
from openpype.hosts.fusion.api.action import SelectInvalidAction
|
||||
|
||||
|
||||
class ValidateSaverHasInput(pyblish.api.InstancePlugin):
|
||||
|
|
@ -12,6 +15,7 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin):
|
|||
label = "Validate Saver Has Input"
|
||||
families = ["render"]
|
||||
hosts = ["fusion"]
|
||||
actions = [SelectInvalidAction]
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
|
|
@ -25,5 +29,8 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin):
|
|||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError("Saver has no incoming connection: "
|
||||
"{} ({})".format(instance, invalid[0].Name))
|
||||
saver_name = invalid[0].Name
|
||||
raise PublishValidationError(
|
||||
"Saver has no incoming connection: {} ({})".format(instance,
|
||||
saver_name),
|
||||
title=self.label)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
from openpype.hosts.fusion.api.action import SelectInvalidAction
|
||||
|
||||
|
||||
class ValidateSaverPassthrough(pyblish.api.ContextPlugin):
|
||||
|
|
@ -8,6 +11,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin):
|
|||
label = "Validate Saver Passthrough"
|
||||
families = ["render"]
|
||||
hosts = ["fusion"]
|
||||
actions = [SelectInvalidAction]
|
||||
|
||||
def process(self, context):
|
||||
|
||||
|
|
@ -27,8 +31,9 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin):
|
|||
if invalid_instances:
|
||||
self.log.info("Reset pyblish to collect your current scene state, "
|
||||
"that should fix error.")
|
||||
raise RuntimeError("Invalid instances: "
|
||||
"{0}".format(invalid_instances))
|
||||
raise PublishValidationError(
|
||||
"Invalid instances: {0}".format(invalid_instances),
|
||||
title=self.label)
|
||||
|
||||
def is_invalid(self, instance):
|
||||
|
||||
|
|
@ -36,7 +41,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin):
|
|||
attr = saver.GetAttrs()
|
||||
active = not attr["TOOLB_PassThrough"]
|
||||
|
||||
if active != instance.data["publish"]:
|
||||
if active != instance.data.get("publish", True):
|
||||
self.log.info("Saver has different passthrough state than "
|
||||
"Pyblish: {} ({})".format(instance, saver.Name))
|
||||
return [saver]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
from collections import defaultdict
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
from openpype.hosts.fusion.api.action import SelectInvalidAction
|
||||
|
||||
|
||||
class ValidateUniqueSubsets(pyblish.api.ContextPlugin):
|
||||
"""Ensure all instances have a unique subset name"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Unique Subsets"
|
||||
families = ["render"]
|
||||
hosts = ["fusion"]
|
||||
actions = [SelectInvalidAction]
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, context):
|
||||
|
||||
# Collect instances per subset per asset
|
||||
instances_per_subset_asset = defaultdict(lambda: defaultdict(list))
|
||||
for instance in context:
|
||||
asset = instance.data.get("asset", context.data.get("asset"))
|
||||
subset = instance.data.get("subset", context.data.get("subset"))
|
||||
instances_per_subset_asset[asset][subset].append(instance)
|
||||
|
||||
# Find which asset + subset combination has more than one instance
|
||||
# Those are considered invalid because they'd integrate to the same
|
||||
# destination.
|
||||
invalid = []
|
||||
for asset, instances_per_subset in instances_per_subset_asset.items():
|
||||
for subset, instances in instances_per_subset.items():
|
||||
if len(instances) > 1:
|
||||
cls.log.warning(
|
||||
"{asset} > {subset} used by more than "
|
||||
"one instance: {instances}".format(
|
||||
asset=asset,
|
||||
subset=subset,
|
||||
instances=instances
|
||||
)
|
||||
)
|
||||
invalid.extend(instances)
|
||||
|
||||
# Return tools for the invalid instances so they can be selected
|
||||
invalid = [instance.data["tool"] for instance in invalid]
|
||||
|
||||
return invalid
|
||||
|
||||
def process(self, context):
|
||||
invalid = self.get_invalid(context)
|
||||
if invalid:
|
||||
raise PublishValidationError("Multiple instances are set to "
|
||||
"the same asset > subset.",
|
||||
title=self.label)
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
from qtpy import QtWidgets
|
||||
import qtawesome
|
||||
from openpype.hosts.fusion.api import get_current_comp
|
||||
|
||||
|
||||
_help = {"local": "Render the comp on your own machine and publish "
|
||||
"it from that the destination folder",
|
||||
"farm": "Submit a Fusion render job to a Render farm to use all other"
|
||||
" computers and add a publish job"}
|
||||
|
||||
|
||||
class SetRenderMode(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QtWidgets.QWidget.__init__(self, parent)
|
||||
|
||||
self._comp = get_current_comp()
|
||||
self._comp_name = self._get_comp_name()
|
||||
|
||||
self.setWindowTitle("Set Render Mode")
|
||||
self.setFixedSize(300, 175)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
# region comp info
|
||||
comp_info_layout = QtWidgets.QHBoxLayout()
|
||||
|
||||
update_btn = QtWidgets.QPushButton(qtawesome.icon("fa.refresh",
|
||||
color="white"), "")
|
||||
update_btn.setFixedWidth(25)
|
||||
update_btn.setFixedHeight(25)
|
||||
|
||||
comp_information = QtWidgets.QLineEdit()
|
||||
comp_information.setEnabled(False)
|
||||
|
||||
comp_info_layout.addWidget(comp_information)
|
||||
comp_info_layout.addWidget(update_btn)
|
||||
# endregion comp info
|
||||
|
||||
# region modes
|
||||
mode_options = QtWidgets.QComboBox()
|
||||
mode_options.addItems(_help.keys())
|
||||
|
||||
mode_information = QtWidgets.QTextEdit()
|
||||
mode_information.setReadOnly(True)
|
||||
# endregion modes
|
||||
|
||||
accept_btn = QtWidgets.QPushButton("Accept")
|
||||
|
||||
layout.addLayout(comp_info_layout)
|
||||
layout.addWidget(mode_options)
|
||||
layout.addWidget(mode_information)
|
||||
layout.addWidget(accept_btn)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
self.comp_information = comp_information
|
||||
self.update_btn = update_btn
|
||||
|
||||
self.mode_options = mode_options
|
||||
self.mode_information = mode_information
|
||||
|
||||
self.accept_btn = accept_btn
|
||||
|
||||
self.connections()
|
||||
self.update()
|
||||
|
||||
# Force updated render mode help text
|
||||
self._update_rendermode_info()
|
||||
|
||||
def connections(self):
|
||||
"""Build connections between code and buttons"""
|
||||
|
||||
self.update_btn.clicked.connect(self.update)
|
||||
self.accept_btn.clicked.connect(self._set_comp_rendermode)
|
||||
self.mode_options.currentIndexChanged.connect(
|
||||
self._update_rendermode_info)
|
||||
|
||||
def update(self):
|
||||
"""Update all information in the UI"""
|
||||
|
||||
self._comp = get_current_comp()
|
||||
self._comp_name = self._get_comp_name()
|
||||
self.comp_information.setText(self._comp_name)
|
||||
|
||||
# Update current comp settings
|
||||
mode = self._get_comp_rendermode()
|
||||
index = self.mode_options.findText(mode)
|
||||
self.mode_options.setCurrentIndex(index)
|
||||
|
||||
def _update_rendermode_info(self):
|
||||
rendermode = self.mode_options.currentText()
|
||||
self.mode_information.setText(_help[rendermode])
|
||||
|
||||
def _get_comp_name(self):
|
||||
return self._comp.GetAttrs("COMPS_Name")
|
||||
|
||||
def _get_comp_rendermode(self):
|
||||
return self._comp.GetData("openpype.rendermode") or "local"
|
||||
|
||||
def _set_comp_rendermode(self):
|
||||
rendermode = self.mode_options.currentText()
|
||||
self._comp.SetData("openpype.rendermode", rendermode)
|
||||
|
||||
self._comp.Print("Updated render mode to '%s'\n" % rendermode)
|
||||
self.hide()
|
||||
|
||||
def _validation(self):
|
||||
ui_mode = self.mode_options.currentText()
|
||||
comp_mode = self._get_comp_rendermode()
|
||||
|
||||
return comp_mode == ui_mode
|
||||
|
|
@ -432,11 +432,11 @@ copy_files = """function copyFile(srcFilename, dstFilename)
|
|||
|
||||
import_files = """function %s_import_files()
|
||||
{
|
||||
var PNGTransparencyMode = 0; // Premultiplied wih Black
|
||||
var TGATransparencyMode = 0; // Premultiplied wih Black
|
||||
var SGITransparencyMode = 0; // Premultiplied wih Black
|
||||
var PNGTransparencyMode = 0; // Premultiplied with Black
|
||||
var TGATransparencyMode = 0; // Premultiplied with Black
|
||||
var SGITransparencyMode = 0; // Premultiplied with Black
|
||||
var LayeredPSDTransparencyMode = 1; // Straight
|
||||
var FlatPSDTransparencyMode = 2; // Premultiplied wih White
|
||||
var FlatPSDTransparencyMode = 2; // Premultiplied with White
|
||||
|
||||
function getUniqueColumnName( column_prefix )
|
||||
{
|
||||
|
|
|
|||
|
|
@ -142,10 +142,10 @@ function Client() {
|
|||
};
|
||||
|
||||
/**
|
||||
* Process recieved request. This will eval recieved function and produce
|
||||
* Process received request. This will eval received function and produce
|
||||
* results.
|
||||
* @function
|
||||
* @param {object} request - recieved request JSON
|
||||
* @param {object} request - received request JSON
|
||||
* @return {object} result of evaled function.
|
||||
*/
|
||||
self.processRequest = function(request) {
|
||||
|
|
@ -245,7 +245,7 @@ function Client() {
|
|||
var request = JSON.parse(to_parse);
|
||||
var mid = request.message_id;
|
||||
// self.logDebug('[' + mid + '] - Request: ' + '\n' + JSON.stringify(request));
|
||||
self.logDebug('[' + mid + '] Recieved.');
|
||||
self.logDebug('[' + mid + '] Received.');
|
||||
|
||||
request.result = self.processRequest(request);
|
||||
self.logDebug('[' + mid + '] Processing done.');
|
||||
|
|
@ -286,8 +286,8 @@ function Client() {
|
|||
/** Harmony 21.1 doesn't have QDataStream anymore.
|
||||
|
||||
This means we aren't able to write bytes into QByteArray so we had
|
||||
modify how content lenght is sent do the server.
|
||||
Content lenght is sent as string of 8 char convertible into integer
|
||||
modify how content length is sent do the server.
|
||||
Content length is sent as string of 8 char convertible into integer
|
||||
(instead of 0x00000001[4 bytes] > "000000001"[8 bytes]) */
|
||||
var codec_name = new QByteArray().append("UTF-8");
|
||||
|
||||
|
|
@ -476,6 +476,25 @@ function start() {
|
|||
action.triggered.connect(self.onSubsetManage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set scene settings from DB to the scene
|
||||
*/
|
||||
self.onSetSceneSettings = function() {
|
||||
app.avalonClient.send(
|
||||
{
|
||||
"module": "openpype.hosts.harmony.api",
|
||||
"method": "ensure_scene_settings",
|
||||
"args": []
|
||||
},
|
||||
false
|
||||
);
|
||||
};
|
||||
// add Set Scene Settings
|
||||
if (app.avalonMenu == null) {
|
||||
action = menu.addAction('Set Scene Settings...');
|
||||
action.triggered.connect(self.onSetSceneSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Experimental dialog
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -394,7 +394,7 @@ def get_scene_data():
|
|||
"function": "AvalonHarmony.getSceneData"
|
||||
})["result"]
|
||||
except json.decoder.JSONDecodeError:
|
||||
# Means no sceen metadata has been made before.
|
||||
# Means no scene metadata has been made before.
|
||||
return {}
|
||||
except KeyError:
|
||||
# Means no existing scene metadata has been made.
|
||||
|
|
@ -465,7 +465,7 @@ def imprint(node_id, data, remove=False):
|
|||
Example:
|
||||
>>> from openpype.hosts.harmony.api import lib
|
||||
>>> node = "Top/Display"
|
||||
>>> data = {"str": "someting", "int": 1, "float": 0.32, "bool": True}
|
||||
>>> data = {"str": "something", "int": 1, "float": 0.32, "bool": True}
|
||||
>>> lib.imprint(layer, data)
|
||||
"""
|
||||
scene_data = get_scene_data()
|
||||
|
|
@ -550,7 +550,7 @@ def save_scene():
|
|||
method prevents this double request and safely saves the scene.
|
||||
|
||||
"""
|
||||
# Need to turn off the backgound watcher else the communication with
|
||||
# Need to turn off the background watcher else the communication with
|
||||
# the server gets spammed with two requests at the same time.
|
||||
scene_path = send(
|
||||
{"function": "AvalonHarmony.saveScene"})["result"]
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ def application_launch(event):
|
|||
harmony.send({"script": script})
|
||||
inject_avalon_js()
|
||||
|
||||
ensure_scene_settings()
|
||||
# ensure_scene_settings()
|
||||
check_inventory()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class Server(threading.Thread):
|
|||
"module": (str), # Module of method.
|
||||
"method" (str), # Name of method in module.
|
||||
"args" (list), # Arguments to pass to method.
|
||||
"kwargs" (dict), # Keywork arguments to pass to method.
|
||||
"kwargs" (dict), # Keyword arguments to pass to method.
|
||||
"reply" (bool), # Optional wait for method completion.
|
||||
}
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -25,8 +25,9 @@ class ExtractRender(pyblish.api.InstancePlugin):
|
|||
application_path = instance.context.data.get("applicationPath")
|
||||
scene_path = instance.context.data.get("scenePath")
|
||||
frame_rate = instance.context.data.get("frameRate")
|
||||
frame_start = instance.context.data.get("frameStart")
|
||||
frame_end = instance.context.data.get("frameEnd")
|
||||
# real value from timeline
|
||||
frame_start = instance.context.data.get("frameStartHandle")
|
||||
frame_end = instance.context.data.get("frameEndHandle")
|
||||
audio_path = instance.context.data.get("audioPath")
|
||||
|
||||
if audio_path and os.path.exists(audio_path):
|
||||
|
|
@ -55,9 +56,13 @@ class ExtractRender(pyblish.api.InstancePlugin):
|
|||
|
||||
# Execute rendering. Ignoring error cause Harmony returns error code
|
||||
# always.
|
||||
self.log.info(f"running [ {application_path} -batch {scene_path}")
|
||||
|
||||
args = [application_path, "-batch",
|
||||
"-frames", str(frame_start), str(frame_end),
|
||||
"-scene", scene_path]
|
||||
self.log.info(f"running [ {application_path} {' '.join(args)}")
|
||||
proc = subprocess.Popen(
|
||||
[application_path, "-batch", scene_path],
|
||||
args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
stdin=subprocess.PIPE
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin):
|
|||
# which is available on 'context.data["assetEntity"]'
|
||||
# - the same approach can be used in 'ValidateSceneSettingsRepair'
|
||||
expected_settings = harmony.get_asset_settings()
|
||||
self.log.info("scene settings from DB:".format(expected_settings))
|
||||
self.log.info("scene settings from DB:{}".format(expected_settings))
|
||||
expected_settings.pop("entityType") # not useful for the validation
|
||||
|
||||
expected_settings = _update_frames(dict.copy(expected_settings))
|
||||
expected_settings["frameEndHandle"] = expected_settings["frameEnd"] +\
|
||||
|
|
@ -68,21 +69,32 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin):
|
|||
|
||||
if (any(re.search(pattern, os.getenv('AVALON_TASK'))
|
||||
for pattern in self.skip_resolution_check)):
|
||||
self.log.info("Skipping resolution check because of "
|
||||
"task name and pattern {}".format(
|
||||
self.skip_resolution_check))
|
||||
expected_settings.pop("resolutionWidth")
|
||||
expected_settings.pop("resolutionHeight")
|
||||
|
||||
entity_type = expected_settings.get("entityType")
|
||||
if (any(re.search(pattern, entity_type)
|
||||
if (any(re.search(pattern, os.getenv('AVALON_TASK'))
|
||||
for pattern in self.skip_timelines_check)):
|
||||
self.log.info("Skipping frames check because of "
|
||||
"task name and pattern {}".format(
|
||||
self.skip_timelines_check))
|
||||
expected_settings.pop('frameStart', None)
|
||||
expected_settings.pop('frameEnd', None)
|
||||
|
||||
expected_settings.pop("entityType") # not useful after the check
|
||||
expected_settings.pop('frameStartHandle', None)
|
||||
expected_settings.pop('frameEndHandle', None)
|
||||
|
||||
asset_name = instance.context.data['anatomyData']['asset']
|
||||
if any(re.search(pattern, asset_name)
|
||||
for pattern in self.frame_check_filter):
|
||||
expected_settings.pop("frameEnd")
|
||||
self.log.info("Skipping frames check because of "
|
||||
"task name and pattern {}".format(
|
||||
self.frame_check_filter))
|
||||
expected_settings.pop('frameStart', None)
|
||||
expected_settings.pop('frameEnd', None)
|
||||
expected_settings.pop('frameStartHandle', None)
|
||||
expected_settings.pop('frameEndHandle', None)
|
||||
|
||||
# handle case where ftrack uses only two decimal places
|
||||
# 23.976023976023978 vs. 23.98
|
||||
|
|
@ -99,6 +111,7 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin):
|
|||
"frameEnd": instance.context.data["frameEnd"],
|
||||
"handleStart": instance.context.data.get("handleStart"),
|
||||
"handleEnd": instance.context.data.get("handleEnd"),
|
||||
"frameStartHandle": instance.context.data.get("frameStartHandle"),
|
||||
"frameEndHandle": instance.context.data.get("frameEndHandle"),
|
||||
"resolutionWidth": instance.context.data.get("resolutionWidth"),
|
||||
"resolutionHeight": instance.context.data.get("resolutionHeight"),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Ever tried to make a simple script for toonboom Harmony, then got stumped by the
|
|||
|
||||
Toonboom Harmony is a very powerful software, with hundreds of functions and tools, and it unlocks a great amount of possibilities for animation studios around the globe. And... being the produce of the hard work of a small team forced to prioritise, it can also be a bit rustic at times!
|
||||
|
||||
We are users at heart, animators and riggers, who just want to interact with the software as simply as possible. Simplicity is at the heart of the design of openHarmony. But we also are developpers, and we made the library for people like us who can't resist tweaking the software and bend it in all possible ways, and are looking for powerful functions to help them do it.
|
||||
We are users at heart, animators and riggers, who just want to interact with the software as simply as possible. Simplicity is at the heart of the design of openHarmony. But we also are developers, and we made the library for people like us who can't resist tweaking the software and bend it in all possible ways, and are looking for powerful functions to help them do it.
|
||||
|
||||
This library's aim is to create a more direct way to interact with Toonboom through scripts, by providing a more intuitive way to access its elements, and help with the cumbersome and repetitive tasks as well as help unlock untapped potential in its many available systems. So we can go from having to do things like this:
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ All you have to do is call :
|
|||
```javascript
|
||||
include("openHarmony.js");
|
||||
```
|
||||
at the beggining of your script.
|
||||
at the beginning of your script.
|
||||
|
||||
You can ask your users to download their copy of the library and store it alongside, or bundle it as you wish as long as you include the license file provided on this repository.
|
||||
|
||||
|
|
@ -129,7 +129,7 @@ Check that the environment variable `LIB_OPENHARMONY_PATH` is set correctly to t
|
|||
## How to add openHarmony to vscode intellisense for autocompletion
|
||||
|
||||
Although not fully supported, you can get most of the autocompletion features to work by adding the following lines to a `jsconfig.json` file placed at the root of your working folder.
|
||||
The paths need to be relative which means the openHarmony source code must be placed directly in your developping environnement.
|
||||
The paths need to be relative which means the openHarmony source code must be placed directly in your developping environment.
|
||||
|
||||
For example, if your working folder contains the openHarmony source in a folder called `OpenHarmony` and your working scripts in a folder called `myScripts`, place the `jsconfig.json` file at the root of the folder and add these lines to the file:
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// openHarmony Library
|
||||
//
|
||||
//
|
||||
// Developped by Mathieu Chaptel, Chris Fourney
|
||||
// Developed by Mathieu Chaptel, Chris Fourney
|
||||
//
|
||||
//
|
||||
// This library is an open source implementation of a Document Object Model
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
// and by hiding the heavy lifting required by the official API.
|
||||
//
|
||||
// This library is provided as is and is a work in progress. As such, not every
|
||||
// function has been implemented or is garanteed to work. Feel free to contribute
|
||||
// function has been implemented or is guaranteed to work. Feel free to contribute
|
||||
// improvements to its official github. If you do make sure you follow the provided
|
||||
// template and naming conventions and document your new methods properly.
|
||||
//
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
* $.log("hello"); // prints out a message to the MessageLog.
|
||||
* var myPoint = new $.oPoint(0,0,0); // create a new class instance from an openHarmony class.
|
||||
*
|
||||
* // function members of the $ objects get published to the global scope, which means $ can be ommited
|
||||
* // function members of the $ objects get published to the global scope, which means $ can be omitted
|
||||
*
|
||||
* log("hello");
|
||||
* var myPoint = new oPoint(0,0,0); // This is all valid
|
||||
|
|
@ -118,7 +118,7 @@ Object.defineProperty( $, "directory", {
|
|||
|
||||
|
||||
/**
|
||||
* Wether Harmony is run with the interface or simply from command line
|
||||
* Whether Harmony is run with the interface or simply from command line
|
||||
*/
|
||||
Object.defineProperty( $, "batchMode", {
|
||||
get: function(){
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// openHarmony Library
|
||||
//
|
||||
//
|
||||
// Developped by Mathieu Chaptel, Chris Fourney
|
||||
// Developed by Mathieu Chaptel, Chris Fourney
|
||||
//
|
||||
//
|
||||
// This library is an open source implementation of a Document Object Model
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
// and by hiding the heavy lifting required by the official API.
|
||||
//
|
||||
// This library is provided as is and is a work in progress. As such, not every
|
||||
// function has been implemented or is garanteed to work. Feel free to contribute
|
||||
// function has been implemented or is guaranteed to work. Feel free to contribute
|
||||
// improvements to its official github. If you do make sure you follow the provided
|
||||
// template and naming conventions and document your new methods properly.
|
||||
//
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
* @hideconstructor
|
||||
* @namespace
|
||||
* @example
|
||||
* // To check wether an action is available, call the synthax:
|
||||
* // To check whether an action is available, call the synthax:
|
||||
* Action.validate (<actionName>, <responder>);
|
||||
*
|
||||
* // To launch an action, call the synthax:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// openHarmony Library
|
||||
//
|
||||
//
|
||||
// Developped by Mathieu Chaptel, Chris Fourney
|
||||
// Developed by Mathieu Chaptel, Chris Fourney
|
||||
//
|
||||
//
|
||||
// This library is an open source implementation of a Document Object Model
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
// and by hiding the heavy lifting required by the official API.
|
||||
//
|
||||
// This library is provided as is and is a work in progress. As such, not every
|
||||
// function has been implemented or is garanteed to work. Feel free to contribute
|
||||
// function has been implemented or is guaranteed to work. Feel free to contribute
|
||||
// improvements to its official github. If you do make sure you follow the provided
|
||||
// template and naming conventions and document your new methods properly.
|
||||
//
|
||||
|
|
@ -409,7 +409,7 @@ $.oApp.prototype.getToolByName = function(toolName){
|
|||
|
||||
|
||||
/**
|
||||
* returns the list of stencils useable by the specified tool
|
||||
* returns the list of stencils usable by the specified tool
|
||||
* @param {$.oTool} tool the tool object we want valid stencils for
|
||||
* @return {$.oStencil[]} the list of stencils compatible with the specified tool
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// openHarmony Library v0.01
|
||||
//
|
||||
//
|
||||
// Developped by Mathieu Chaptel, Chris Fourney...
|
||||
// Developed by Mathieu Chaptel, Chris Fourney...
|
||||
//
|
||||
//
|
||||
// This library is an open source implementation of a Document Object Model
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
// and by hiding the heavy lifting required by the official API.
|
||||
//
|
||||
// This library is provided as is and is a work in progress. As such, not every
|
||||
// function has been implemented or is garanteed to work. Feel free to contribute
|
||||
// function has been implemented or is guaranteed to work. Feel free to contribute
|
||||
// improvements to its official github. If you do make sure you follow the provided
|
||||
// template and naming conventions and document your new methods properly.
|
||||
//
|
||||
|
|
@ -338,7 +338,7 @@ Object.defineProperty($.oAttribute.prototype, "useSeparate", {
|
|||
* Returns the default value of the attribute for most keywords
|
||||
* @name $.oAttribute#defaultValue
|
||||
* @type {bool}
|
||||
* @todo switch the implentation to types?
|
||||
* @todo switch the implementation to types?
|
||||
* @example
|
||||
* // to reset an attribute to its default value:
|
||||
* // (mostly used for position/angle/skew parameters of pegs and drawing nodes)
|
||||
|
|
@ -449,7 +449,7 @@ $.oAttribute.prototype.getLinkedColumns = function(){
|
|||
|
||||
/**
|
||||
* Recursively sets an attribute to the same value as another. Both must have the same keyword.
|
||||
* @param {bool} [duplicateColumns=false] In the case that the attribute has a column, wether to duplicate the column before linking
|
||||
* @param {bool} [duplicateColumns=false] In the case that the attribute has a column, whether to duplicate the column before linking
|
||||
* @private
|
||||
*/
|
||||
$.oAttribute.prototype.setToAttributeValue = function(attributeToCopy, duplicateColumns){
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// openHarmony Library
|
||||
//
|
||||
//
|
||||
// Developped by Mathieu Chaptel, Chris Fourney
|
||||
// Developed by Mathieu Chaptel, Chris Fourney
|
||||
//
|
||||
//
|
||||
// This library is an open source implementation of a Document Object Model
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
// and by hiding the heavy lifting required by the official API.
|
||||
//
|
||||
// This library is provided as is and is a work in progress. As such, not every
|
||||
// function has been implemented or is garanteed to work. Feel free to contribute
|
||||
// function has been implemented or is guaranteed to work. Feel free to contribute
|
||||
// improvements to its official github. If you do make sure you follow the provided
|
||||
// template and naming conventions and document your new methods properly.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// openHarmony Library
|
||||
//
|
||||
//
|
||||
// Developped by Mathieu Chaptel, Chris Fourney
|
||||
// Developed by Mathieu Chaptel, Chris Fourney
|
||||
//
|
||||
//
|
||||
// This library is an open source implementation of a Document Object Model
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
// and by hiding the heavy lifting required by the official API.
|
||||
//
|
||||
// This library is provided as is and is a work in progress. As such, not every
|
||||
// function has been implemented or is garanteed to work. Feel free to contribute
|
||||
// function has been implemented or is guaranteed to work. Feel free to contribute
|
||||
// improvements to its official github. If you do make sure you follow the provided
|
||||
// template and naming conventions and document your new methods properly.
|
||||
//
|
||||
|
|
@ -158,7 +158,7 @@ $.oColorValue.prototype.fromColorString = function (hexString){
|
|||
|
||||
|
||||
/**
|
||||
* Uses a color integer (used in backdrops) and parses the INT; applies the RGBA components of the INT to thos oColorValue
|
||||
* Uses a color integer (used in backdrops) and parses the INT; applies the RGBA components of the INT to the oColorValue
|
||||
* @param { int } colorInt 24 bit-shifted integer containing RGBA values
|
||||
*/
|
||||
$.oColorValue.prototype.parseColorFromInt = function(colorInt){
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// openHarmony Library
|
||||
//
|
||||
//
|
||||
// Developped by Mathieu Chaptel, Chris Fourney
|
||||
// Developed by Mathieu Chaptel, Chris Fourney
|
||||
//
|
||||
//
|
||||
// This library is an open source implementation of a Document Object Model
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
// and by hiding the heavy lifting required by the official API.
|
||||
//
|
||||
// This library is provided as is and is a work in progress. As such, not every
|
||||
// function has been implemented or is garanteed to work. Feel free to contribute
|
||||
// function has been implemented or is guaranteed to work. Feel free to contribute
|
||||
// improvements to its official github. If you do make sure you follow the provided
|
||||
// template and naming conventions and document your new methods properly.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// openHarmony Library
|
||||
//
|
||||
//
|
||||
// Developped by Mathieu Chaptel, Chris Fourney
|
||||
// Developed by Mathieu Chaptel, Chris Fourney
|
||||
//
|
||||
//
|
||||
// This library is an open source implementation of a Document Object Model
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
// and by hiding the heavy lifting required by the official API.
|
||||
//
|
||||
// This library is provided as is and is a work in progress. As such, not every
|
||||
// function has been implemented or is garanteed to work. Feel free to contribute
|
||||
// function has been implemented or is guaranteed to work. Feel free to contribute
|
||||
// improvements to its official github. If you do make sure you follow the provided
|
||||
// template and naming conventions and document your new methods properly.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
// openHarmony Library
|
||||
//
|
||||
//
|
||||
// Developped by Mathieu Chaptel, Chris Fourney
|
||||
// Developed by Mathieu Chaptel, Chris Fourney
|
||||
//
|
||||
//
|
||||
// This library is an open source implementation of a Document Object Model
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
// and by hiding the heavy lifting required by the official API.
|
||||
//
|
||||
// This library is provided as is and is a work in progress. As such, not every
|
||||
// function has been implemented or is garanteed to work. Feel free to contribute
|
||||
// function has been implemented or is guaranteed to work. Feel free to contribute
|
||||
// improvements to its official github. If you do make sure you follow the provided
|
||||
// template and naming conventions and document your new methods properly.
|
||||
//
|
||||
|
|
@ -250,7 +250,7 @@ $.oDialog.prototype.prompt = function( labelText, title, prefilledText){
|
|||
/**
|
||||
* Prompts with a file selector window
|
||||
* @param {string} [text="Select a file:"] The title of the confirmation dialog.
|
||||
* @param {string} [filter="*"] The filter for the file type and/or file name that can be selected. Accepts wildcard charater "*".
|
||||
* @param {string} [filter="*"] The filter for the file type and/or file name that can be selected. Accepts wildcard character "*".
|
||||
* @param {string} [getExisting=true] Whether to select an existing file or a save location
|
||||
* @param {string} [acceptMultiple=false] Whether or not selecting more than one file is ok. Is ignored if getExisting is falses.
|
||||
* @param {string} [startDirectory] The directory showed at the opening of the dialog.
|
||||
|
|
@ -327,14 +327,14 @@ $.oDialog.prototype.browseForFolder = function(text, startDirectory){
|
|||
* @constructor
|
||||
* @classdesc An simple progress dialog to display the progress of a task.
|
||||
* To react to the user clicking the cancel button, connect a function to $.oProgressDialog.canceled() signal.
|
||||
* When $.batchmode is true, the progress will be outputed as a "Progress : value/range" string to the Harmony stdout.
|
||||
* When $.batchmode is true, the progress will be outputted as a "Progress : value/range" string to the Harmony stdout.
|
||||
* @param {string} [labelText] The text displayed above the progress bar.
|
||||
* @param {string} [range=100] The maximum value that represents a full progress bar.
|
||||
* @param {string} [title] The title of the dialog
|
||||
* @param {bool} [show=false] Whether to immediately show the dialog.
|
||||
*
|
||||
* @property {bool} wasCanceled Whether the progress bar was cancelled.
|
||||
* @property {$.oSignal} canceled A Signal emited when the dialog is canceled. Can be connected to a callback.
|
||||
* @property {$.oSignal} canceled A Signal emitted when the dialog is canceled. Can be connected to a callback.
|
||||
*/
|
||||
$.oProgressDialog = function( labelText, range, title, show ){
|
||||
if (typeof title === 'undefined') var title = "Progress";
|
||||
|
|
@ -608,7 +608,7 @@ $.oPieMenu = function( name, widgets, show, minAngle, maxAngle, radius, position
|
|||
this.maxAngle = maxAngle;
|
||||
this.globalCenter = position;
|
||||
|
||||
// how wide outisde the icons is the slice drawn
|
||||
// how wide outside the icons is the slice drawn
|
||||
this._circleMargin = 30;
|
||||
|
||||
// set these values before calling show() to customize the menu appearance
|
||||
|
|
@ -974,7 +974,7 @@ $.oPieMenu.prototype.getMenuRadius = function(){
|
|||
var _minRadius = UiLoader.dpiScale(30);
|
||||
var _speed = 10; // the higher the value, the slower the progression
|
||||
|
||||
// hyperbolic tangent function to determin the radius
|
||||
// hyperbolic tangent function to determine the radius
|
||||
var exp = Math.exp(2*itemsNumber/_speed);
|
||||
var _radius = ((exp-1)/(exp+1))*_maxRadius+_minRadius;
|
||||
|
||||
|
|
@ -1383,7 +1383,7 @@ $.oActionButton.prototype.activate = function(){
|
|||
* This class is a subclass of QPushButton and all the methods from that class are available to modify this button.
|
||||
* @param {string} paletteName The name of the palette that contains the color
|
||||
* @param {string} colorName The name of the color (if more than one is present, will pick the first match)
|
||||
* @param {bool} showName Wether to display the name of the color on the button
|
||||
* @param {bool} showName Whether to display the name of the color on the button
|
||||
* @param {QWidget} parent The parent QWidget for the button. Automatically set during initialisation of the menu.
|
||||
*
|
||||
*/
|
||||
|
|
@ -1437,7 +1437,7 @@ $.oColorButton.prototype.activate = function(){
|
|||
* @name $.oScriptButton
|
||||
* @constructor
|
||||
* @classdescription This subclass of QPushButton provides an easy way to create a button for a widget that will launch a function from another script file.<br>
|
||||
* The buttons created this way automatically load the icon named after the script if it finds one named like the funtion in a script-icons folder next to the script file.<br>
|
||||
* The buttons created this way automatically load the icon named after the script if it finds one named like the function in a script-icons folder next to the script file.<br>
|
||||
* It will also automatically set the callback to lanch the function from the script.<br>
|
||||
* This class is a subclass of QPushButton and all the methods from that class are available to modify this button.
|
||||
* @param {string} scriptFile The path to the script file that will be launched
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// openHarmony Library
|
||||
//
|
||||
//
|
||||
// Developped by Mathieu Chaptel, Chris Fourney
|
||||
// Developed by Mathieu Chaptel, Chris Fourney
|
||||
//
|
||||
//
|
||||
// This library is an open source implementation of a Document Object Model
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
// and by hiding the heavy lifting required by the official API.
|
||||
//
|
||||
// This library is provided as is and is a work in progress. As such, not every
|
||||
// function has been implemented or is garanteed to work. Feel free to contribute
|
||||
// function has been implemented or is guaranteed to work. Feel free to contribute
|
||||
// improvements to its official github. If you do make sure you follow the provided
|
||||
// template and naming conventions and document your new methods properly.
|
||||
//
|
||||
|
|
@ -426,7 +426,7 @@ Object.defineProperty($.oDrawing.prototype, 'drawingData', {
|
|||
/**
|
||||
* Import a given file into an existing drawing.
|
||||
* @param {$.oFile} file The path to the file
|
||||
* @param {bool} [convertToTvg=false] Wether to convert the bitmap to the tvg format (this doesn't vectorise the drawing)
|
||||
* @param {bool} [convertToTvg=false] Whether to convert the bitmap to the tvg format (this doesn't vectorise the drawing)
|
||||
*
|
||||
* @return { $.oFile } the oFile object pointing to the drawing file after being it has been imported into the element folder.
|
||||
*/
|
||||
|
|
@ -878,8 +878,8 @@ $.oArtLayer.prototype.drawCircle = function(center, radius, lineStyle, fillStyle
|
|||
* @param {$.oVertex[]} path an array of $.oVertex objects that describe a path.
|
||||
* @param {$.oLineStyle} [lineStyle] the line style to draw with. (By default, will use the current stencil selection)
|
||||
* @param {$.oFillStyle} [fillStyle] the fill information for the path. (By default, will use the current palette selection)
|
||||
* @param {bool} [polygon] Wether bezier handles should be created for the points in the path (ignores "onCurve" properties of oVertex from path)
|
||||
* @param {bool} [createUnderneath] Wether the new shape will appear on top or underneath the contents of the layer. (not working yet)
|
||||
* @param {bool} [polygon] Whether bezier handles should be created for the points in the path (ignores "onCurve" properties of oVertex from path)
|
||||
* @param {bool} [createUnderneath] Whether the new shape will appear on top or underneath the contents of the layer. (not working yet)
|
||||
*/
|
||||
$.oArtLayer.prototype.drawShape = function(path, lineStyle, fillStyle, polygon, createUnderneath){
|
||||
if (typeof fillStyle === 'undefined') var fillStyle = new this.$.oFillStyle();
|
||||
|
|
@ -959,7 +959,7 @@ $.oArtLayer.prototype.drawContour = function(path, fillStyle){
|
|||
* @param {float} width the width of the rectangle.
|
||||
* @param {float} height the height of the rectangle.
|
||||
* @param {$.oLineStyle} lineStyle a line style to use for the rectangle stroke.
|
||||
* @param {$.oFillStyle} fillStyle a fill style to use for the rectange fill.
|
||||
* @param {$.oFillStyle} fillStyle a fill style to use for the rectangle fill.
|
||||
* @returns {$.oShape} the shape containing the added stroke.
|
||||
*/
|
||||
$.oArtLayer.prototype.drawRectangle = function(x, y, width, height, lineStyle, fillStyle){
|
||||
|
|
@ -1514,7 +1514,7 @@ Object.defineProperty($.oStroke.prototype, "path", {
|
|||
|
||||
|
||||
/**
|
||||
* The oVertex that are on the stroke (Bezier handles exluded.)
|
||||
* The oVertex that are on the stroke (Bezier handles excluded.)
|
||||
* The first is repeated at the last position when the stroke is closed.
|
||||
* @name $.oStroke#points
|
||||
* @type {$.oVertex[]}
|
||||
|
|
@ -1583,7 +1583,7 @@ Object.defineProperty($.oStroke.prototype, "style", {
|
|||
|
||||
|
||||
/**
|
||||
* wether the stroke is a closed shape.
|
||||
* whether the stroke is a closed shape.
|
||||
* @name $.oStroke#closed
|
||||
* @type {bool}
|
||||
*/
|
||||
|
|
@ -1919,7 +1919,7 @@ $.oContour.prototype.toString = function(){
|
|||
* @constructor
|
||||
* @classdesc
|
||||
* The $.oVertex class represents a single control point on a stroke. This class is used to get the index of the point in the stroke path sequence, as well as its position as a float along the stroke's length.
|
||||
* The onCurve property describes wether this control point is a bezier handle or a point on the curve.
|
||||
* The onCurve property describes whether this control point is a bezier handle or a point on the curve.
|
||||
*
|
||||
* @param {$.oStroke} stroke the stroke that this vertex belongs to
|
||||
* @param {float} x the x coordinate of the vertex, in drawing space
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// openHarmony Library v0.01
|
||||
//
|
||||
//
|
||||
// Developped by Mathieu Chaptel, Chris Fourney...
|
||||
// Developed by Mathieu Chaptel, Chris Fourney...
|
||||
//
|
||||
//
|
||||
// This library is an open source implementation of a Document Object Model
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
// and by hiding the heavy lifting required by the official API.
|
||||
//
|
||||
// This library is provided as is and is a work in progress. As such, not every
|
||||
// function has been implemented or is garanteed to work. Feel free to contribute
|
||||
// function has been implemented or is guaranteed to work. Feel free to contribute
|
||||
// improvements to its official github. If you do make sure you follow the provided
|
||||
// template and naming conventions and document your new methods properly.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// openHarmony Library
|
||||
//
|
||||
//
|
||||
// Developped by Mathieu Chaptel, Chris Fourney
|
||||
// Developed by Mathieu Chaptel, Chris Fourney
|
||||
//
|
||||
//
|
||||
// This library is an open source implementation of a Document Object Model
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
// and by hiding the heavy lifting required by the official API.
|
||||
//
|
||||
// This library is provided as is and is a work in progress. As such, not every
|
||||
// function has been implemented or is garanteed to work. Feel free to contribute
|
||||
// function has been implemented or is guaranteed to work. Feel free to contribute
|
||||
// improvements to its official github. If you do make sure you follow the provided
|
||||
// template and naming conventions and document your new methods properly.
|
||||
//
|
||||
|
|
@ -509,7 +509,7 @@ Object.defineProperty($.oFile.prototype, 'fullName', {
|
|||
|
||||
|
||||
/**
|
||||
* The name of the file without extenstion.
|
||||
* The name of the file without extension.
|
||||
* @name $.oFile#name
|
||||
* @type {string}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// openHarmony Library
|
||||
//
|
||||
//
|
||||
// Developped by Mathieu Chaptel, Chris Fourney
|
||||
// Developed by Mathieu Chaptel, Chris Fourney
|
||||
//
|
||||
//
|
||||
// This library is an open source implementation of a Document Object Model
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
// and by hiding the heavy lifting required by the official API.
|
||||
//
|
||||
// This library is provided as is and is a work in progress. As such, not every
|
||||
// function has been implemented or is garanteed to work. Feel free to contribute
|
||||
// function has been implemented or is guaranteed to work. Feel free to contribute
|
||||
// improvements to its official github. If you do make sure you follow the provided
|
||||
// template and naming conventions and document your new methods properly.
|
||||
//
|
||||
|
|
@ -263,7 +263,7 @@ Object.defineProperty($.oFrame.prototype, 'duration', {
|
|||
return _sceneLength;
|
||||
}
|
||||
|
||||
// walk up the frames of the scene to the next keyFrame to determin duration
|
||||
// walk up the frames of the scene to the next keyFrame to determine duration
|
||||
var _frames = this.column.frames
|
||||
for (var i=this.frameNumber+1; i<_sceneLength; i++){
|
||||
if (_frames[i].isKeyframe) return _frames[i].frameNumber - _startFrame;
|
||||
|
|
@ -426,7 +426,7 @@ Object.defineProperty($.oFrame.prototype, 'velocity', {
|
|||
* easeIn : a $.oPoint object representing the left handle for bezier columns, or a {point, ease} object for ease columns.
|
||||
* easeOut : a $.oPoint object representing the left handle for bezier columns, or a {point, ease} object for ease columns.
|
||||
* continuity : the type of bezier used by the point.
|
||||
* constant : wether the frame is interpolated or a held value.
|
||||
* constant : whether the frame is interpolated or a held value.
|
||||
* @name $.oFrame#ease
|
||||
* @type {oPoint/object}
|
||||
*/
|
||||
|
|
@ -520,7 +520,7 @@ Object.defineProperty($.oFrame.prototype, 'easeOut', {
|
|||
|
||||
|
||||
/**
|
||||
* Determines the frame's continuity setting. Can take the values "CORNER", (two independant bezier handles on each side), "SMOOTH"(handles are aligned) or "STRAIGHT" (no handles and in straight lines).
|
||||
* Determines the frame's continuity setting. Can take the values "CORNER", (two independent bezier handles on each side), "SMOOTH"(handles are aligned) or "STRAIGHT" (no handles and in straight lines).
|
||||
* @name $.oFrame#continuity
|
||||
* @type {string}
|
||||
*/
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue