mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
Merge branch 'develop' into feature/maya_python_3
# Conflicts: # openpype/modules/default_modules/ftrack/python2_vendor/arrow # openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api
This commit is contained in:
commit
bb97c3cdd2
1200 changed files with 87071 additions and 24206 deletions
146
.dockerignore
Normal file
146
.dockerignore
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Python template
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
.poetry/
|
||||
.github/
|
||||
vendor/bin/
|
||||
docs/
|
||||
website/
|
||||
1
.github/workflows/documentation.yml
vendored
1
.github/workflows/documentation.yml
vendored
|
|
@ -10,6 +10,7 @@ on:
|
|||
branches: [main]
|
||||
paths:
|
||||
- 'website/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-build:
|
||||
|
|
|
|||
29
.github/workflows/nightly_merge.yml
vendored
Normal file
29
.github/workflows/nightly_merge.yml
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
name: Dev -> Main
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '21 3 * * 3,6'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
develop-to-main:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 🚛 Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: 🔨 Merge develop to main
|
||||
uses: everlytic/branch-merge@1.1.0
|
||||
with:
|
||||
github_token: ${{ secrets.ADMIN_TOKEN }}
|
||||
source_ref: 'develop'
|
||||
target_branch: 'main'
|
||||
commit_message_template: '[Automated] Merged {source_ref} into {target_branch}'
|
||||
|
||||
- name: Invoke pre-release workflow
|
||||
uses: benc-uk/workflow-dispatch@v1
|
||||
with:
|
||||
workflow: Nightly Prerelease
|
||||
token: ${{ secrets.ADMIN_TOKEN }}
|
||||
100
.github/workflows/prerelease.yml
vendored
Normal file
100
.github/workflows/prerelease.yml
vendored
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
name: Nightly Prerelease
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
create_nightly:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 🚛 Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
|
||||
- name: Install Python requirements
|
||||
run: pip install gitpython semver
|
||||
|
||||
- name: 🔎 Determine next version type
|
||||
id: version_type
|
||||
run: |
|
||||
TYPE=$(python ./tools/ci_tools.py --bump)
|
||||
|
||||
echo ::set-output name=type::$TYPE
|
||||
|
||||
- name: 💉 Inject new version into files
|
||||
id: version
|
||||
if: steps.version_type.outputs.type != 'skip'
|
||||
run: |
|
||||
RESULT=$(python ./tools/ci_tools.py --nightly)
|
||||
|
||||
echo ::set-output name=next_tag::$RESULT
|
||||
|
||||
- name: "✏️ Generate full changelog"
|
||||
if: steps.version_type.outputs.type != 'skip'
|
||||
id: generate-full-changelog
|
||||
uses: heinrichreimer/github-changelog-generator-action@v2.2
|
||||
with:
|
||||
token: ${{ secrets.ADMIN_TOKEN }}
|
||||
breakingLabel: '**💥 Breaking**'
|
||||
enhancementLabel: '**🚀 Enhancements**'
|
||||
bugsLabel: '**🐛 Bug fixes**'
|
||||
deprecatedLabel: '**⚠️ Deprecations**'
|
||||
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"### 🆕 New features","labels":["feature"]},}'
|
||||
issues: false
|
||||
issuesWoLabels: false
|
||||
sinceTag: "3.0.0"
|
||||
maxIssues: 100
|
||||
pullRequests: true
|
||||
prWoLabels: false
|
||||
author: false
|
||||
unreleased: true
|
||||
compareLink: true
|
||||
stripGeneratorNotice: true
|
||||
verbose: true
|
||||
unreleasedLabel: ${{ steps.version.outputs.next_tag }}
|
||||
excludeTagsRegex: "CI/.+"
|
||||
releaseBranch: "main"
|
||||
|
||||
- name: "🖨️ Print changelog to console"
|
||||
if: steps.version_type.outputs.type != 'skip'
|
||||
run: cat CHANGELOG.md
|
||||
|
||||
- name: 💾 Commit and Tag
|
||||
id: git_commit
|
||||
if: steps.version_type.outputs.type != 'skip'
|
||||
run: |
|
||||
git config user.email ${{ secrets.CI_EMAIL }}
|
||||
git config user.name ${{ secrets.CI_USER }}
|
||||
cd repos/avalon-core
|
||||
git checkout main
|
||||
git pull
|
||||
cd ../..
|
||||
git add .
|
||||
git commit -m "[Automated] Bump version"
|
||||
tag_name="CI/${{ steps.version.outputs.next_tag }}"
|
||||
git tag -a $tag_name -m "nightly build"
|
||||
|
||||
- name: Push to protected main branch
|
||||
uses: CasperWA/push-protected@v2
|
||||
with:
|
||||
token: ${{ secrets.ADMIN_TOKEN }}
|
||||
branch: main
|
||||
tags: true
|
||||
unprotect_reviews: true
|
||||
|
||||
- name: 🔨 Merge main back to develop
|
||||
uses: everlytic/branch-merge@1.1.0
|
||||
if: steps.version_type.outputs.type != 'skip'
|
||||
with:
|
||||
github_token: ${{ secrets.ADMIN_TOKEN }}
|
||||
source_ref: 'main'
|
||||
target_branch: 'develop'
|
||||
commit_message_template: '[Automated] Merged {source_ref} into {target_branch}'
|
||||
132
.github/workflows/release.yml
vendored
Normal file
132
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
name: Stable Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- prereleased
|
||||
|
||||
jobs:
|
||||
create_release:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'pypebot'
|
||||
|
||||
steps:
|
||||
- name: 🚛 Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Install Python requirements
|
||||
run: pip install gitpython semver
|
||||
|
||||
- name: 💉 Inject new version into files
|
||||
id: version
|
||||
run: |
|
||||
echo ::set-output name=current_version::${GITHUB_REF#refs/*/}
|
||||
RESULT=$(python ./tools/ci_tools.py --finalize ${GITHUB_REF#refs/*/})
|
||||
LASTRELEASE=$(python ./tools/ci_tools.py --lastversion release)
|
||||
|
||||
echo ::set-output name=last_release::$LASTRELEASE
|
||||
echo ::set-output name=release_tag::$RESULT
|
||||
|
||||
- name: "✏️ Generate full changelog"
|
||||
if: steps.version.outputs.release_tag != 'skip'
|
||||
id: generate-full-changelog
|
||||
uses: heinrichreimer/github-changelog-generator-action@v2.2
|
||||
with:
|
||||
token: ${{ secrets.ADMIN_TOKEN }}
|
||||
breakingLabel: '**💥 Breaking**'
|
||||
enhancementLabel: '**🚀 Enhancements**'
|
||||
bugsLabel: '**🐛 Bug fixes**'
|
||||
deprecatedLabel: '**⚠️ Deprecations**'
|
||||
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}'
|
||||
issues: false
|
||||
issuesWoLabels: false
|
||||
sinceTag: "3.0.0"
|
||||
maxIssues: 100
|
||||
pullRequests: true
|
||||
prWoLabels: false
|
||||
author: false
|
||||
unreleased: true
|
||||
compareLink: true
|
||||
stripGeneratorNotice: true
|
||||
verbose: true
|
||||
futureRelease: ${{ steps.version.outputs.release_tag }}
|
||||
excludeTagsRegex: "CI/.+"
|
||||
releaseBranch: "main"
|
||||
|
||||
- name: 💾 Commit and Tag
|
||||
id: git_commit
|
||||
if: steps.version.outputs.release_tag != 'skip'
|
||||
run: |
|
||||
git config user.email ${{ secrets.CI_EMAIL }}
|
||||
git config user.name ${{ secrets.CI_USER }}
|
||||
git add .
|
||||
git commit -m "[Automated] Release"
|
||||
tag_name="${{ steps.version.outputs.release_tag }}"
|
||||
git tag -a $tag_name -m "stable release"
|
||||
|
||||
- name: 🔏 Push to protected main branch
|
||||
if: steps.version.outputs.release_tag != 'skip'
|
||||
uses: CasperWA/push-protected@v2
|
||||
with:
|
||||
token: ${{ secrets.ADMIN_TOKEN }}
|
||||
branch: main
|
||||
tags: true
|
||||
unprotect_reviews: true
|
||||
|
||||
- name: "✏️ Generate last changelog"
|
||||
if: steps.version.outputs.release_tag != 'skip'
|
||||
id: generate-last-changelog
|
||||
uses: heinrichreimer/github-changelog-generator-action@v2.2
|
||||
with:
|
||||
token: ${{ secrets.ADMIN_TOKEN }}
|
||||
breakingLabel: '**💥 Breaking**'
|
||||
enhancementLabel: '**🚀 Enhancements**'
|
||||
bugsLabel: '**🐛 Bug fixes**'
|
||||
deprecatedLabel: '**⚠️ Deprecations**'
|
||||
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}'
|
||||
issues: false
|
||||
issuesWoLabels: false
|
||||
sinceTag: ${{ steps.version.outputs.last_release }}
|
||||
maxIssues: 100
|
||||
pullRequests: true
|
||||
prWoLabels: false
|
||||
author: false
|
||||
unreleased: true
|
||||
compareLink: true
|
||||
stripGeneratorNotice: true
|
||||
verbose: true
|
||||
futureRelease: ${{ steps.version.outputs.release_tag }}
|
||||
excludeTagsRegex: "CI/.+"
|
||||
releaseBranch: "main"
|
||||
stripHeaders: true
|
||||
base: 'none'
|
||||
|
||||
|
||||
- name: 🚀 Github Release
|
||||
if: steps.version.outputs.release_tag != 'skip'
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
body: ${{ steps.generate-last-changelog.outputs.changelog }}
|
||||
tag: ${{ steps.version.outputs.release_tag }}
|
||||
token: ${{ secrets.ADMIN_TOKEN }}
|
||||
|
||||
- name: ☠ Delete Pre-release
|
||||
if: steps.version.outputs.release_tag != 'skip'
|
||||
uses: cb80/delrel@latest
|
||||
with:
|
||||
tag: "${{ steps.version.outputs.current_version }}"
|
||||
|
||||
- name: 🔁 Merge main back to develop
|
||||
if: steps.version.outputs.release_tag != 'skip'
|
||||
uses: everlytic/branch-merge@1.1.0
|
||||
with:
|
||||
github_token: ${{ secrets.ADMIN_TOKEN }}
|
||||
source_ref: 'main'
|
||||
target_branch: 'develop'
|
||||
commit_message_template: '[Automated] Merged release {source_ref} into {target_branch}'
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
pr-wo-labels=False
|
||||
exclude-labels=duplicate,question,invalid,wontfix,weekly-digest
|
||||
author=False
|
||||
unreleased=True
|
||||
since-tag=2.13.6
|
||||
release-branch=master
|
||||
enhancement-label=**Enhancements:**
|
||||
issues=False
|
||||
pulls=False
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -36,6 +36,7 @@ Temporary Items
|
|||
# CX_Freeze
|
||||
###########
|
||||
/build
|
||||
/dist/
|
||||
|
||||
/vendor/bin/*
|
||||
/.venv
|
||||
|
|
@ -64,7 +65,6 @@ coverage.xml
|
|||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
|
||||
# Node JS packages
|
||||
##################
|
||||
node_modules
|
||||
|
|
@ -92,4 +92,10 @@ website/i18n/*
|
|||
|
||||
website/debug.log
|
||||
|
||||
website/.docusaurus
|
||||
website/.docusaurus
|
||||
|
||||
# Poetry
|
||||
########
|
||||
|
||||
.poetry/
|
||||
.python-version
|
||||
|
|
|
|||
13
.gitmodules
vendored
13
.gitmodules
vendored
|
|
@ -1,13 +1,12 @@
|
|||
[submodule "repos/avalon-core"]
|
||||
path = repos/avalon-core
|
||||
url = https://github.com/pypeclub/avalon-core.git
|
||||
branch = develop
|
||||
[submodule "repos/avalon-unreal-integration"]
|
||||
path = repos/avalon-unreal-integration
|
||||
url = https://github.com/pypeclub/avalon-unreal-integration.git
|
||||
[submodule "openpype/modules/ftrack/python2_vendor/ftrack-python-api"]
|
||||
path = openpype/modules/ftrack/python2_vendor/ftrack-python-api
|
||||
url = https://bitbucket.org/ftrack/ftrack-python-api.git
|
||||
[submodule "openpype/modules/ftrack/python2_vendor/arrow"]
|
||||
path = openpype/modules/ftrack/python2_vendor/arrow
|
||||
url = https://github.com/arrow-py/arrow.git
|
||||
[submodule "openpype/modules/default_modules/ftrack/python2_vendor/arrow"]
|
||||
path = openpype/modules/default_modules/ftrack/python2_vendor/arrow
|
||||
url = https://github.com/arrow-py/arrow.git
|
||||
[submodule "openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api"]
|
||||
path = openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api
|
||||
url = https://bitbucket.org/ftrack/ftrack-python-api.git
|
||||
644
CHANGELOG.md
644
CHANGELOG.md
|
|
@ -1,34 +1,622 @@
|
|||
# Changelog
|
||||
|
||||
## [2.16.1](https://github.com/pypeclub/pype/tree/2.16.1) (2021-04-13)
|
||||
## [3.4.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.16.0...2.16.1)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Nuke: comp renders mix up [\#1301](https://github.com/pypeclub/pype/pull/1301)
|
||||
- Validate project settings [\#1297](https://github.com/pypeclub/pype/pull/1297)
|
||||
- After Effects: added SubsetManager [\#1234](https://github.com/pypeclub/pype/pull/1234)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Ftrack custom attributes in bulks [\#1312](https://github.com/pypeclub/pype/pull/1312)
|
||||
- Ftrack optional pypclub role [\#1303](https://github.com/pypeclub/pype/pull/1303)
|
||||
- AE remove orphaned instance from workfile - fix self.stub [\#1282](https://github.com/pypeclub/pype/pull/1282)
|
||||
- Avalon schema names [\#1242](https://github.com/pypeclub/pype/pull/1242)
|
||||
- Handle duplication of Task name [\#1226](https://github.com/pypeclub/pype/pull/1226)
|
||||
- Modified path of plugin loads for Harmony and TVPaint [\#1217](https://github.com/pypeclub/pype/pull/1217)
|
||||
- Regex checks in profiles filtering [\#1214](https://github.com/pypeclub/pype/pull/1214)
|
||||
- Bulk mov strict task [\#1204](https://github.com/pypeclub/pype/pull/1204)
|
||||
- Update custom ftrack session attributes [\#1202](https://github.com/pypeclub/pype/pull/1202)
|
||||
- Nuke: write node colorspace ignore `default\(\)` label [\#1199](https://github.com/pypeclub/pype/pull/1199)
|
||||
- Nuke: reverse search to make it more versatile [\#1178](https://github.com/pypeclub/pype/pull/1178)
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.3.1...HEAD)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Forward compatible ftrack group [\#1243](https://github.com/pypeclub/pype/pull/1243)
|
||||
- Error message in pyblish UI [\#1206](https://github.com/pypeclub/pype/pull/1206)
|
||||
- Nuke: deadline submission with search replaced env values from preset [\#1194](https://github.com/pypeclub/pype/pull/1194)
|
||||
- Ftrack: Fix hosts attribute in collect ftrack username [\#1972](https://github.com/pypeclub/OpenPype/pull/1972)
|
||||
- Removed deprecated submodules [\#1967](https://github.com/pypeclub/OpenPype/pull/1967)
|
||||
- Launcher: Fix crashes on action click [\#1964](https://github.com/pypeclub/OpenPype/pull/1964)
|
||||
- Settings: Minor fixes in UI and missing default values [\#1963](https://github.com/pypeclub/OpenPype/pull/1963)
|
||||
- Blender: Toggle system console works on windows [\#1962](https://github.com/pypeclub/OpenPype/pull/1962)
|
||||
- Resolve path when adding to zip [\#1960](https://github.com/pypeclub/OpenPype/pull/1960)
|
||||
- Bump url-parse from 1.5.1 to 1.5.3 in /website [\#1958](https://github.com/pypeclub/OpenPype/pull/1958)
|
||||
- Global: Avalon Host name collector [\#1949](https://github.com/pypeclub/OpenPype/pull/1949)
|
||||
- Global: Define hosts in CollectSceneVersion [\#1948](https://github.com/pypeclub/OpenPype/pull/1948)
|
||||
- Maya: Add Xgen family support [\#1947](https://github.com/pypeclub/OpenPype/pull/1947)
|
||||
- Add face sets to exported alembics [\#1942](https://github.com/pypeclub/OpenPype/pull/1942)
|
||||
- Bump path-parse from 1.0.6 to 1.0.7 in /website [\#1933](https://github.com/pypeclub/OpenPype/pull/1933)
|
||||
- \#1894 - adds host to template\_name\_profiles for filtering [\#1915](https://github.com/pypeclub/OpenPype/pull/1915)
|
||||
- Environments: Tool environments in alphabetical order [\#1910](https://github.com/pypeclub/OpenPype/pull/1910)
|
||||
- Disregard publishing time. [\#1888](https://github.com/pypeclub/OpenPype/pull/1888)
|
||||
- Feature/webpublisher backend [\#1876](https://github.com/pypeclub/OpenPype/pull/1876)
|
||||
- Dynamic modules [\#1872](https://github.com/pypeclub/OpenPype/pull/1872)
|
||||
- Houdini: add Camera, Point Cache, Composite, Redshift ROP and VDB Cache support [\#1821](https://github.com/pypeclub/OpenPype/pull/1821)
|
||||
|
||||
## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.1-nightly.1...3.3.1)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- TVPaint: Fixed rendered frame indexes [\#1946](https://github.com/pypeclub/OpenPype/pull/1946)
|
||||
- Maya: Menu actions fix [\#1945](https://github.com/pypeclub/OpenPype/pull/1945)
|
||||
- standalone: editorial shared object problem [\#1941](https://github.com/pypeclub/OpenPype/pull/1941)
|
||||
- Bugfix nuke deadline app name [\#1928](https://github.com/pypeclub/OpenPype/pull/1928)
|
||||
|
||||
## [3.3.0](https://github.com/pypeclub/OpenPype/tree/3.3.0) (2021-08-17)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.3.0-nightly.11...3.3.0)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Python console interpreter [\#1940](https://github.com/pypeclub/OpenPype/pull/1940)
|
||||
- Fix - make AE workfile publish to Ftrack configurable [\#1937](https://github.com/pypeclub/OpenPype/pull/1937)
|
||||
- Fix - ftrack family was added incorrectly in some cases [\#1935](https://github.com/pypeclub/OpenPype/pull/1935)
|
||||
- Settings UI: Breadcrumbs in settings [\#1932](https://github.com/pypeclub/OpenPype/pull/1932)
|
||||
- Fix - Deadline publish on Linux started Tray instead of headless publishing [\#1930](https://github.com/pypeclub/OpenPype/pull/1930)
|
||||
- Maya: Validate Model Name - repair accident deletion in settings defaults [\#1929](https://github.com/pypeclub/OpenPype/pull/1929)
|
||||
- Global: Updated logos and Default settings [\#1927](https://github.com/pypeclub/OpenPype/pull/1927)
|
||||
- Nuke: submit to farm failed due `ftrack` family remove [\#1926](https://github.com/pypeclub/OpenPype/pull/1926)
|
||||
- Check for missing ✨ Python when using `pyenv` [\#1925](https://github.com/pypeclub/OpenPype/pull/1925)
|
||||
- Maya: Scene patching 🩹on submission to Deadline [\#1923](https://github.com/pypeclub/OpenPype/pull/1923)
|
||||
- Fix - validate takes repre\["files"\] as list all the time [\#1922](https://github.com/pypeclub/OpenPype/pull/1922)
|
||||
- Settings: Default values for enum [\#1920](https://github.com/pypeclub/OpenPype/pull/1920)
|
||||
- Settings UI: Modifiable dict view enhance [\#1919](https://github.com/pypeclub/OpenPype/pull/1919)
|
||||
- standalone: validator asset parents [\#1917](https://github.com/pypeclub/OpenPype/pull/1917)
|
||||
- Nuke: update video file crassing [\#1916](https://github.com/pypeclub/OpenPype/pull/1916)
|
||||
- Fix - texture validators for workfiles triggers only for textures workfiles [\#1914](https://github.com/pypeclub/OpenPype/pull/1914)
|
||||
- submodules: avalon-core update [\#1911](https://github.com/pypeclub/OpenPype/pull/1911)
|
||||
- Settings UI: List order works as expected [\#1906](https://github.com/pypeclub/OpenPype/pull/1906)
|
||||
- Add support for multiple Deadline ☠️➖ servers [\#1905](https://github.com/pypeclub/OpenPype/pull/1905)
|
||||
- Hiero: loaded clip was not set colorspace from version data [\#1904](https://github.com/pypeclub/OpenPype/pull/1904)
|
||||
- Pyblish UI: Fix collecting stage processing [\#1903](https://github.com/pypeclub/OpenPype/pull/1903)
|
||||
- Burnins: Use input's bitrate in h624 [\#1902](https://github.com/pypeclub/OpenPype/pull/1902)
|
||||
- Feature AE local render [\#1901](https://github.com/pypeclub/OpenPype/pull/1901)
|
||||
- Ftrack: Where I run action enhancement [\#1900](https://github.com/pypeclub/OpenPype/pull/1900)
|
||||
- Ftrack: Private project server actions [\#1899](https://github.com/pypeclub/OpenPype/pull/1899)
|
||||
- Support nested studio plugins paths. [\#1898](https://github.com/pypeclub/OpenPype/pull/1898)
|
||||
- Bug: fixed python detection [\#1893](https://github.com/pypeclub/OpenPype/pull/1893)
|
||||
- Settings: global validators with options [\#1892](https://github.com/pypeclub/OpenPype/pull/1892)
|
||||
- Settings: Conditional dict enum positioning [\#1891](https://github.com/pypeclub/OpenPype/pull/1891)
|
||||
- global: integrate name missing default template [\#1890](https://github.com/pypeclub/OpenPype/pull/1890)
|
||||
- publisher: editorial plugins fixes [\#1889](https://github.com/pypeclub/OpenPype/pull/1889)
|
||||
- Expose stop timer through rest api. [\#1886](https://github.com/pypeclub/OpenPype/pull/1886)
|
||||
- TVPaint: Increment workfile [\#1885](https://github.com/pypeclub/OpenPype/pull/1885)
|
||||
- Allow Multiple Notes to run on tasks. [\#1882](https://github.com/pypeclub/OpenPype/pull/1882)
|
||||
- Normalize path returned from Workfiles. [\#1880](https://github.com/pypeclub/OpenPype/pull/1880)
|
||||
- Prepare for pyside2 [\#1869](https://github.com/pypeclub/OpenPype/pull/1869)
|
||||
- Filter hosts in settings host-enum [\#1868](https://github.com/pypeclub/OpenPype/pull/1868)
|
||||
- Local actions with process identifier [\#1867](https://github.com/pypeclub/OpenPype/pull/1867)
|
||||
- Workfile tool start at host launch support [\#1865](https://github.com/pypeclub/OpenPype/pull/1865)
|
||||
- Maya: add support for `RedshiftNormalMap` node, fix `tx` linear space 🚀 [\#1863](https://github.com/pypeclub/OpenPype/pull/1863)
|
||||
- Workfiles tool event arguments fix [\#1862](https://github.com/pypeclub/OpenPype/pull/1862)
|
||||
- Maya: support for configurable `dirmap` 🗺️ [\#1859](https://github.com/pypeclub/OpenPype/pull/1859)
|
||||
- Maya: don't add reference members as connections to the container set 📦 [\#1855](https://github.com/pypeclub/OpenPype/pull/1855)
|
||||
- Settings list can use template or schema as object type [\#1815](https://github.com/pypeclub/OpenPype/pull/1815)
|
||||
|
||||
## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.7...3.2.0)
|
||||
|
||||
## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...2.18.4)
|
||||
|
||||
## [2.18.3](https://github.com/pypeclub/OpenPype/tree/2.18.3) (2021-06-23)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.2...2.18.3)
|
||||
|
||||
## [2.18.2](https://github.com/pypeclub/OpenPype/tree/2.18.2) (2021-06-16)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.1.0...2.18.2)
|
||||
|
||||
## [3.1.0](https://github.com/pypeclub/OpenPype/tree/3.1.0) (2021-06-15)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.1.0-nightly.4...3.1.0)
|
||||
|
||||
# Changelog
|
||||
|
||||
|
||||
## [3.0.0](https://github.com/pypeclub/openpype/tree/3.0.0)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/2.18.1...3.0.0)
|
||||
|
||||
### Configuration
|
||||
- Studio Settings GUI: no more json configuration files.
|
||||
- OpenPype Modules can be turned on and off.
|
||||
- Easy to add Application versions.
|
||||
- Per Project Environment and plugin management.
|
||||
- Robust profile system for creating reviewables and burnins, with filtering based on Application, Task and data family.
|
||||
- Configurable publish plugins.
|
||||
- Options to make any validator or extractor, optional or disabled.
|
||||
- Color Management is now unified under anatomy settings.
|
||||
- Subset naming and grouping is fully configurable.
|
||||
- All project attributes can now be set directly in OpenPype settings.
|
||||
- Studio Setting can be locked to prevent unwanted artist changes.
|
||||
- You can now add per project and per task type templates for workfile initialization in most hosts.
|
||||
- Too many other individual configurable option to list in this changelog :)
|
||||
|
||||
### Local Settings
|
||||
- Local Settings GUI where users can change certain option on individual basis.
|
||||
- Application executables.
|
||||
- Project roots.
|
||||
- Project site sync settings.
|
||||
|
||||
### Build, Installation and Deployments
|
||||
- No requirements on artist machine.
|
||||
- Fully distributed workflow possible.
|
||||
- Self-contained installation.
|
||||
- Available on all three major platforms.
|
||||
- Automatic artist OpenPype updates.
|
||||
- Studio OpenPype repository for updates distribution.
|
||||
- Robust Build system.
|
||||
- Safe studio update versioning with staging and production options.
|
||||
- MacOS build generates .app and .dmg installer.
|
||||
- Windows build with installer creation script.
|
||||
|
||||
### Misc
|
||||
- System and diagnostic info tool in the tray.
|
||||
- Launching application from Launcher indicates activity.
|
||||
- All project roots are now named. Single root project are now achieved by having a single named root in the project anatomy.
|
||||
- Every project root is cast into environment variable as well, so it can be used in DCC instead of absolute path (depends on DCC support for env vars).
|
||||
- Basic support for task types, on top of task names.
|
||||
- Timer now change automatically when the context is switched inside running application.
|
||||
- 'Master" versions have been renamed to "Hero".
|
||||
- Extract Burnins now supports file sequences and color settings.
|
||||
- Extract Review support overscan cropping, better letterboxes and background colour fill.
|
||||
- Delivery tool for copying and renaming any published assets in bulk.
|
||||
- Harmony, Photoshop and After Effects now connect directly with OpenPype tray instead of spawning their own terminal.
|
||||
|
||||
### Project Manager GUI
|
||||
- Create Projects.
|
||||
- Create Shots and Assets.
|
||||
- Create Tasks and assign task types.
|
||||
- Fill required asset attributes.
|
||||
- Validations for duplicated or unsupported names.
|
||||
- Archive Assets.
|
||||
- Move Asset within hierarchy.
|
||||
|
||||
### Site Sync (beta)
|
||||
- Synchronization of published files between workstations and central storage.
|
||||
- Ability to add arbitrary storage providers to the Site Sync system.
|
||||
- Default setup includes Disk and Google Drive providers as examples.
|
||||
- Access to availability information from Loader and Scene Manager.
|
||||
- Sync queue GUI with filtering, error and status reporting.
|
||||
- Site sync can be configured on a per-project basis.
|
||||
- Bulk upload and download from the loader.
|
||||
|
||||
### Ftrack
|
||||
- Actions have customisable roles.
|
||||
- Settings on all actions are updated live and don't need openpype restart.
|
||||
- Ftrack module can now be turned off completely.
|
||||
- It is enough to specify ftrack server name and the URL will be formed correctly. So instead of mystudio.ftrackapp.com, it's possible to use simply: "mystudio".
|
||||
|
||||
### Editorial
|
||||
- Fully OTIO based editorial publishing.
|
||||
- Completely re-done Hiero publishing to be a lot simpler and faster.
|
||||
- Consistent conforming from Resolve, Hiero and Standalone Publisher.
|
||||
|
||||
### Backend
|
||||
- OpenPype and Avalon now always share the same database (in 2.x is was possible to split them).
|
||||
- Major codebase refactoring to allow for better CI, versioning and control of individual integrations.
|
||||
- OTIO is bundled with build.
|
||||
- OIIO is bundled with build.
|
||||
- FFMPEG is bundled with build.
|
||||
- Rest API and host WebSocket servers have been unified into a single local webserver.
|
||||
- Maya look assigner has been integrated into the main codebase.
|
||||
- Publish GUI has been integrated into the main codebase.
|
||||
- Studio and Project settings overrides are now stored in Mongo.
|
||||
- Too many other backend fixes and tweaks to list :), you can see full changelog on github for those.
|
||||
- OpenPype uses Poetry to manage it's virtual environment when running from code.
|
||||
- all applications can be marked as python 2 or 3 compatible to make the switch a bit easier.
|
||||
|
||||
|
||||
### Pull Requests since 3.0.0-rc.6
|
||||
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- settings: task types enum entity [\#1605](https://github.com/pypeclub/OpenPype/issues/1605)
|
||||
- Settings: ignore keys in referenced schema [\#1600](https://github.com/pypeclub/OpenPype/issues/1600)
|
||||
- Maya: support for frame steps and frame lists [\#1585](https://github.com/pypeclub/OpenPype/issues/1585)
|
||||
- TVPaint: Publish workfile. [\#1548](https://github.com/pypeclub/OpenPype/issues/1548)
|
||||
- Loader: Current Asset Button [\#1448](https://github.com/pypeclub/OpenPype/issues/1448)
|
||||
- Hiero: publish with retiming [\#1377](https://github.com/pypeclub/OpenPype/issues/1377)
|
||||
- Ask user to restart after changing global environments in settings [\#910](https://github.com/pypeclub/OpenPype/issues/910)
|
||||
- add option to define paht to workfile template [\#895](https://github.com/pypeclub/OpenPype/issues/895)
|
||||
- Harmony: move server console to system tray [\#676](https://github.com/pypeclub/OpenPype/issues/676)
|
||||
- Standalone style [\#1630](https://github.com/pypeclub/OpenPype/pull/1630) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Faster hierarchical values push [\#1627](https://github.com/pypeclub/OpenPype/pull/1627) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Launcher tool style [\#1624](https://github.com/pypeclub/OpenPype/pull/1624) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Loader and Library loader enhancements [\#1623](https://github.com/pypeclub/OpenPype/pull/1623) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Tray style [\#1622](https://github.com/pypeclub/OpenPype/pull/1622) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Maya schemas cleanup [\#1610](https://github.com/pypeclub/OpenPype/pull/1610) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Settings: ignore keys in referenced schema [\#1608](https://github.com/pypeclub/OpenPype/pull/1608) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- settings: task types enum entity [\#1606](https://github.com/pypeclub/OpenPype/pull/1606) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Openpype style [\#1604](https://github.com/pypeclub/OpenPype/pull/1604) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- TVPaint: Publish workfile. [\#1597](https://github.com/pypeclub/OpenPype/pull/1597) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Nuke: add option to define path to workfile template [\#1571](https://github.com/pypeclub/OpenPype/pull/1571) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Crop overscan in Extract Review [\#1569](https://github.com/pypeclub/OpenPype/pull/1569) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Unreal and Blender: Material Workflow [\#1562](https://github.com/pypeclub/OpenPype/pull/1562) ([simonebarbieri](https://github.com/simonebarbieri))
|
||||
- Harmony: move server console to system tray [\#1560](https://github.com/pypeclub/OpenPype/pull/1560) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Ask user to restart after changing global environments in settings [\#1550](https://github.com/pypeclub/OpenPype/pull/1550) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Hiero: publish with retiming [\#1545](https://github.com/pypeclub/OpenPype/pull/1545) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Library loader load asset documents on OpenPype start [\#1603](https://github.com/pypeclub/OpenPype/issues/1603)
|
||||
- Resolve: unable to load the same footage twice [\#1317](https://github.com/pypeclub/OpenPype/issues/1317)
|
||||
- Resolve: unable to load footage [\#1316](https://github.com/pypeclub/OpenPype/issues/1316)
|
||||
- Add required Python 2 modules [\#1291](https://github.com/pypeclub/OpenPype/issues/1291)
|
||||
- GUi scaling with hires displays [\#705](https://github.com/pypeclub/OpenPype/issues/705)
|
||||
- Maya: non unicode string in publish validation [\#673](https://github.com/pypeclub/OpenPype/issues/673)
|
||||
- Nuke: Rendered Frame validation is triggered by multiple collections [\#156](https://github.com/pypeclub/OpenPype/issues/156)
|
||||
- avalon-core debugging failing [\#80](https://github.com/pypeclub/OpenPype/issues/80)
|
||||
- Only check arnold shading group if arnold is used [\#72](https://github.com/pypeclub/OpenPype/issues/72)
|
||||
- Sync server Qt layout fix [\#1621](https://github.com/pypeclub/OpenPype/pull/1621) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Console Listener on Python 2 fix [\#1620](https://github.com/pypeclub/OpenPype/pull/1620) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Bug: Initialize blessed term only in console mode [\#1619](https://github.com/pypeclub/OpenPype/pull/1619) ([antirotor](https://github.com/antirotor))
|
||||
- Settings template skip paths support wrappers [\#1618](https://github.com/pypeclub/OpenPype/pull/1618) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Maya capture 'isolate\_view' fix + minor corrections [\#1617](https://github.com/pypeclub/OpenPype/pull/1617) ([2-REC](https://github.com/2-REC))
|
||||
- MacOs Fix launch of standalone publisher [\#1616](https://github.com/pypeclub/OpenPype/pull/1616) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- 'Delivery action' report fix + typos [\#1612](https://github.com/pypeclub/OpenPype/pull/1612) ([2-REC](https://github.com/2-REC))
|
||||
- List append fix in mutable dict settings [\#1599](https://github.com/pypeclub/OpenPype/pull/1599) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Documentation: Maya: fix review [\#1598](https://github.com/pypeclub/OpenPype/pull/1598) ([antirotor](https://github.com/antirotor))
|
||||
- Bugfix: Set certifi CA bundle for all platforms [\#1596](https://github.com/pypeclub/OpenPype/pull/1596) ([antirotor](https://github.com/antirotor))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Bump dns-packet from 1.3.1 to 1.3.4 in /website [\#1611](https://github.com/pypeclub/OpenPype/pull/1611) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Maya: Render workflow fixes [\#1607](https://github.com/pypeclub/OpenPype/pull/1607) ([antirotor](https://github.com/antirotor))
|
||||
- Maya: support for frame steps and frame lists [\#1586](https://github.com/pypeclub/OpenPype/pull/1586) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- 3.0.0 - curated changelog [\#1284](https://github.com/pypeclub/OpenPype/pull/1284) ([mkolar](https://github.com/mkolar))
|
||||
|
||||
## [2.18.1](https://github.com/pypeclub/openpype/tree/2.18.1) (2021-06-03)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/2.18.0...2.18.1)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Faster hierarchical values push [\#1626](https://github.com/pypeclub/OpenPype/pull/1626)
|
||||
- Feature Delivery in library loader [\#1549](https://github.com/pypeclub/OpenPype/pull/1549)
|
||||
- Hiero: Initial frame publish support. [\#1172](https://github.com/pypeclub/OpenPype/pull/1172)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Maya capture 'isolate\_view' fix + minor corrections [\#1614](https://github.com/pypeclub/OpenPype/pull/1614)
|
||||
- 'Delivery action' report fix +typos [\#1613](https://github.com/pypeclub/OpenPype/pull/1613)
|
||||
- Delivery in LibraryLoader - fixed sequence issue [\#1590](https://github.com/pypeclub/OpenPype/pull/1590)
|
||||
- FFmpeg filters in quote marks [\#1588](https://github.com/pypeclub/OpenPype/pull/1588)
|
||||
- Ftrack delete action cause circular error [\#1581](https://github.com/pypeclub/OpenPype/pull/1581)
|
||||
- Fix Maya playblast. [\#1566](https://github.com/pypeclub/OpenPype/pull/1566)
|
||||
- More failsafes prevent errored runs. [\#1554](https://github.com/pypeclub/OpenPype/pull/1554)
|
||||
- Celaction publishing [\#1539](https://github.com/pypeclub/OpenPype/pull/1539)
|
||||
- celaction: app not starting [\#1533](https://github.com/pypeclub/OpenPype/pull/1533)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Maya: Render workflow fixes - 2.0 backport [\#1609](https://github.com/pypeclub/OpenPype/pull/1609)
|
||||
- Maya Hardware support [\#1553](https://github.com/pypeclub/OpenPype/pull/1553)
|
||||
|
||||
|
||||
## [CI/3.0.0-rc.6](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.6) (2021-05-27)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.5...CI/3.0.0-rc.6)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Hiero: publish color and transformation soft-effects [\#1376](https://github.com/pypeclub/OpenPype/issues/1376)
|
||||
- Get rid of `AVALON\_HIERARCHY` and `hiearchy` key on asset [\#432](https://github.com/pypeclub/OpenPype/issues/432)
|
||||
- Sync to avalon do not store hierarchy key [\#1582](https://github.com/pypeclub/OpenPype/pull/1582) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Tools: launcher scripts for project manager [\#1557](https://github.com/pypeclub/OpenPype/pull/1557) ([antirotor](https://github.com/antirotor))
|
||||
- Simple tvpaint publish [\#1555](https://github.com/pypeclub/OpenPype/pull/1555) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Feature Delivery in library loader [\#1546](https://github.com/pypeclub/OpenPype/pull/1546) ([kalisp](https://github.com/kalisp))
|
||||
- Documentation: Dev and system build documentation [\#1543](https://github.com/pypeclub/OpenPype/pull/1543) ([antirotor](https://github.com/antirotor))
|
||||
- Color entity [\#1542](https://github.com/pypeclub/OpenPype/pull/1542) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Extract review bg color [\#1534](https://github.com/pypeclub/OpenPype/pull/1534) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- TVPaint loader settings [\#1530](https://github.com/pypeclub/OpenPype/pull/1530) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Blender can initialize differente user script paths [\#1528](https://github.com/pypeclub/OpenPype/pull/1528) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Blender and Unreal: Improved Animation Workflow [\#1514](https://github.com/pypeclub/OpenPype/pull/1514) ([simonebarbieri](https://github.com/simonebarbieri))
|
||||
- Hiero: publish color and transformation soft-effects [\#1511](https://github.com/pypeclub/OpenPype/pull/1511) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- OpenPype specific version issues [\#1583](https://github.com/pypeclub/OpenPype/issues/1583)
|
||||
- Ftrack login server can't work without stderr [\#1576](https://github.com/pypeclub/OpenPype/issues/1576)
|
||||
- Mac application launch [\#1575](https://github.com/pypeclub/OpenPype/issues/1575)
|
||||
- Settings are not propagated to Nuke write nodes [\#1538](https://github.com/pypeclub/OpenPype/issues/1538)
|
||||
- Subset names settings not applied for publishing [\#1537](https://github.com/pypeclub/OpenPype/issues/1537)
|
||||
- Nuke: callback at start not setting colorspace [\#1412](https://github.com/pypeclub/OpenPype/issues/1412)
|
||||
- Pype 3: Missing icon for Settings [\#1272](https://github.com/pypeclub/OpenPype/issues/1272)
|
||||
- Blender: cannot initialize Avalon if BLENDER\_USER\_SCRIPTS is already used [\#1050](https://github.com/pypeclub/OpenPype/issues/1050)
|
||||
- Ftrack delete action cause circular error [\#206](https://github.com/pypeclub/OpenPype/issues/206)
|
||||
- Build: stop cleaning of pyc files in build directory [\#1592](https://github.com/pypeclub/OpenPype/pull/1592) ([antirotor](https://github.com/antirotor))
|
||||
- Ftrack login server can't work without stderr [\#1591](https://github.com/pypeclub/OpenPype/pull/1591) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- FFmpeg filters in quote marks [\#1589](https://github.com/pypeclub/OpenPype/pull/1589) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- OpenPype specific version issues [\#1584](https://github.com/pypeclub/OpenPype/pull/1584) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Mac application launch [\#1580](https://github.com/pypeclub/OpenPype/pull/1580) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Ftrack delete action cause circular error [\#1579](https://github.com/pypeclub/OpenPype/pull/1579) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Hiero: publishing issues [\#1578](https://github.com/pypeclub/OpenPype/pull/1578) ([jezscha](https://github.com/jezscha))
|
||||
- Nuke: callback at start not setting colorspace [\#1561](https://github.com/pypeclub/OpenPype/pull/1561) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Bugfix PS subset and quick review [\#1541](https://github.com/pypeclub/OpenPype/pull/1541) ([kalisp](https://github.com/kalisp))
|
||||
- Settings are not propagated to Nuke write nodes [\#1540](https://github.com/pypeclub/OpenPype/pull/1540) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- OpenPype: Powershell scripts polishing [\#1536](https://github.com/pypeclub/OpenPype/pull/1536) ([antirotor](https://github.com/antirotor))
|
||||
- Host name collecting fix [\#1535](https://github.com/pypeclub/OpenPype/pull/1535) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Handle duplicated task names in project manager [\#1531](https://github.com/pypeclub/OpenPype/pull/1531) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Validate is file attribute in settings schema [\#1529](https://github.com/pypeclub/OpenPype/pull/1529) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Bump postcss from 8.2.8 to 8.3.0 in /website [\#1593](https://github.com/pypeclub/OpenPype/pull/1593) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- User installation documentation [\#1532](https://github.com/pypeclub/OpenPype/pull/1532) ([64qam](https://github.com/64qam))
|
||||
|
||||
## [CI/3.0.0-rc.5](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.5) (2021-05-19)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/2.18.0...CI/3.0.0-rc.5)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- OpenPype: Build - Add progress bars [\#1524](https://github.com/pypeclub/OpenPype/pull/1524) ([antirotor](https://github.com/antirotor))
|
||||
- Default environments per host imlementation [\#1522](https://github.com/pypeclub/OpenPype/pull/1522) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- OpenPype: use `semver` module for version resolution [\#1513](https://github.com/pypeclub/OpenPype/pull/1513) ([antirotor](https://github.com/antirotor))
|
||||
- Feature Aftereffects setting cleanup documentation [\#1510](https://github.com/pypeclub/OpenPype/pull/1510) ([kalisp](https://github.com/kalisp))
|
||||
- Feature Sync server settings enhancement [\#1501](https://github.com/pypeclub/OpenPype/pull/1501) ([kalisp](https://github.com/kalisp))
|
||||
- Project manager [\#1396](https://github.com/pypeclub/OpenPype/pull/1396) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Unified schema definition [\#874](https://github.com/pypeclub/OpenPype/issues/874)
|
||||
- Maya: fix look assignment [\#1526](https://github.com/pypeclub/OpenPype/pull/1526) ([antirotor](https://github.com/antirotor))
|
||||
- Bugfix Sync server local site issues [\#1523](https://github.com/pypeclub/OpenPype/pull/1523) ([kalisp](https://github.com/kalisp))
|
||||
- Store as list dictionary check initial value with right type [\#1520](https://github.com/pypeclub/OpenPype/pull/1520) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Maya: wrong collection of playblasted frames [\#1515](https://github.com/pypeclub/OpenPype/pull/1515) ([mkolar](https://github.com/mkolar))
|
||||
- Convert pyblish logs to string at the moment of logging [\#1512](https://github.com/pypeclub/OpenPype/pull/1512) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- 3.0 | nuke: fixing start\_at with option gui [\#1509](https://github.com/pypeclub/OpenPype/pull/1509) ([jezscha](https://github.com/jezscha))
|
||||
- Tests: fix pype -\> openpype to make tests work again [\#1508](https://github.com/pypeclub/OpenPype/pull/1508) ([antirotor](https://github.com/antirotor))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- OpenPype: disable submodule update with `--no-submodule-update` [\#1525](https://github.com/pypeclub/OpenPype/pull/1525) ([antirotor](https://github.com/antirotor))
|
||||
- Ftrack without autosync in Pype 3 [\#1519](https://github.com/pypeclub/OpenPype/pull/1519) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Feature Harmony setting cleanup documentation [\#1506](https://github.com/pypeclub/OpenPype/pull/1506) ([kalisp](https://github.com/kalisp))
|
||||
- Sync Server beginning of documentation [\#1471](https://github.com/pypeclub/OpenPype/pull/1471) ([kalisp](https://github.com/kalisp))
|
||||
- Blender: publish layout json [\#1348](https://github.com/pypeclub/OpenPype/pull/1348) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
|
||||
## [2.18.0](https://github.com/pypeclub/openpype/tree/2.18.0) (2021-05-18)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.4...2.18.0)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Default environments per host imlementation [\#1405](https://github.com/pypeclub/OpenPype/issues/1405)
|
||||
- Blender: publish layout json [\#1346](https://github.com/pypeclub/OpenPype/issues/1346)
|
||||
- Ftrack without autosync in Pype 3 [\#1128](https://github.com/pypeclub/OpenPype/issues/1128)
|
||||
- Launcher: started action indicator [\#1102](https://github.com/pypeclub/OpenPype/issues/1102)
|
||||
- Launch arguments of applications [\#1094](https://github.com/pypeclub/OpenPype/issues/1094)
|
||||
- Publish: instance info [\#724](https://github.com/pypeclub/OpenPype/issues/724)
|
||||
- Review: ability to control review length [\#482](https://github.com/pypeclub/OpenPype/issues/482)
|
||||
- Colorized recognition of creator result [\#394](https://github.com/pypeclub/OpenPype/issues/394)
|
||||
- event assign user to started task [\#49](https://github.com/pypeclub/OpenPype/issues/49)
|
||||
- rebuild containers from reference in maya [\#55](https://github.com/pypeclub/OpenPype/issues/55)
|
||||
- nuke Load metadata [\#66](https://github.com/pypeclub/OpenPype/issues/66)
|
||||
- Maya: Safer handling of expected render output names [\#1496](https://github.com/pypeclub/OpenPype/pull/1496) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- TVPaint: Increment workfile version on successfull publish. [\#1489](https://github.com/pypeclub/OpenPype/pull/1489) ([tokejepsen](https://github.com/tokejepsen))
|
||||
- Use SubsetLoader and multiple contexts for delete\_old\_versions [\#1484](https://github.com/pypeclub/OpenPype/pull/1484) ([tokejepsen](https://github.com/tokejepsen))
|
||||
- Maya: Use of multiple deadline servers [\#1483](https://github.com/pypeclub/OpenPype/pull/1483) ([antirotor](https://github.com/antirotor))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Igniter version resolution doesn't consider it's own version [\#1505](https://github.com/pypeclub/OpenPype/issues/1505)
|
||||
- Maya: Safer handling of expected render output names [\#1159](https://github.com/pypeclub/OpenPype/issues/1159)
|
||||
- Harmony: Invalid render output from non-conventionally named instance [\#871](https://github.com/pypeclub/OpenPype/issues/871)
|
||||
- Existing subsets hints in creator [\#1503](https://github.com/pypeclub/OpenPype/pull/1503) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- nuke: space in node name breaking process [\#1494](https://github.com/pypeclub/OpenPype/pull/1494) ([jezscha](https://github.com/jezscha))
|
||||
- Maya: wrong collection of playblasted frames [\#1517](https://github.com/pypeclub/OpenPype/pull/1517) ([mkolar](https://github.com/mkolar))
|
||||
- Existing subsets hints in creator [\#1502](https://github.com/pypeclub/OpenPype/pull/1502) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Use instance frame start instead of timeline. [\#1486](https://github.com/pypeclub/OpenPype/pull/1486) ([tokejepsen](https://github.com/tokejepsen))
|
||||
- Maya: Redshift - set proper start frame on proxy [\#1480](https://github.com/pypeclub/OpenPype/pull/1480) ([antirotor](https://github.com/antirotor))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Nuke: wrong "star at" value on render load [\#1352](https://github.com/pypeclub/OpenPype/issues/1352)
|
||||
- DV Resolve - loading/updating - image video [\#915](https://github.com/pypeclub/OpenPype/issues/915)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- nuke: fixing start\_at with option gui [\#1507](https://github.com/pypeclub/OpenPype/pull/1507) ([jezscha](https://github.com/jezscha))
|
||||
|
||||
## [CI/3.0.0-rc.4](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.4) (2021-05-12)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.3...CI/3.0.0-rc.4)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Resolve: documentation [\#1490](https://github.com/pypeclub/OpenPype/issues/1490)
|
||||
- Hiero: audio to review [\#1378](https://github.com/pypeclub/OpenPype/issues/1378)
|
||||
- nks color clips after publish [\#44](https://github.com/pypeclub/OpenPype/issues/44)
|
||||
- Store data from modifiable dict as list [\#1504](https://github.com/pypeclub/OpenPype/pull/1504) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Use SubsetLoader and multiple contexts for delete\_old\_versions [\#1497](https://github.com/pypeclub/OpenPype/pull/1497) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Hiero: publish audio and add to review [\#1493](https://github.com/pypeclub/OpenPype/pull/1493) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Resolve: documentation [\#1491](https://github.com/pypeclub/OpenPype/pull/1491) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Change integratenew template profiles setting [\#1487](https://github.com/pypeclub/OpenPype/pull/1487) ([kalisp](https://github.com/kalisp))
|
||||
- Settings tool cleanup [\#1477](https://github.com/pypeclub/OpenPype/pull/1477) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Sorted Applications and Tools in Custom attribute [\#1476](https://github.com/pypeclub/OpenPype/pull/1476) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- PS - group all published instances [\#1416](https://github.com/pypeclub/OpenPype/pull/1416) ([kalisp](https://github.com/kalisp))
|
||||
- OpenPype: Support for Docker [\#1289](https://github.com/pypeclub/OpenPype/pull/1289) ([antirotor](https://github.com/antirotor))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Harmony: palettes publishing [\#1439](https://github.com/pypeclub/OpenPype/issues/1439)
|
||||
- Photoshop: validation for already created images [\#1435](https://github.com/pypeclub/OpenPype/issues/1435)
|
||||
- Nuke Extracts Thumbnail from frame out of shot range [\#963](https://github.com/pypeclub/OpenPype/issues/963)
|
||||
- Instance in same Context repairing [\#390](https://github.com/pypeclub/OpenPype/issues/390)
|
||||
- User Inactivity - Start timers sets wrong time [\#91](https://github.com/pypeclub/OpenPype/issues/91)
|
||||
- Use instance frame start instead of timeline [\#1499](https://github.com/pypeclub/OpenPype/pull/1499) ([mkolar](https://github.com/mkolar))
|
||||
- Various smaller fixes [\#1498](https://github.com/pypeclub/OpenPype/pull/1498) ([mkolar](https://github.com/mkolar))
|
||||
- nuke: space in node name breaking process [\#1495](https://github.com/pypeclub/OpenPype/pull/1495) ([jezscha](https://github.com/jezscha))
|
||||
- Codec determination in extract burnin [\#1492](https://github.com/pypeclub/OpenPype/pull/1492) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Undefined constant in subprocess module [\#1485](https://github.com/pypeclub/OpenPype/pull/1485) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- List entity catch add/remove item changes properly [\#1482](https://github.com/pypeclub/OpenPype/pull/1482) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Resolve: additional fixes of publishing workflow [\#1481](https://github.com/pypeclub/OpenPype/pull/1481) ([jezscha](https://github.com/jezscha))
|
||||
- Photoshop: validation for already created images [\#1436](https://github.com/pypeclub/OpenPype/pull/1436) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Maya: Support for looks on VRay Proxies [\#1443](https://github.com/pypeclub/OpenPype/pull/1443) ([antirotor](https://github.com/antirotor))
|
||||
|
||||
## [2.17.3](https://github.com/pypeclub/openpype/tree/2.17.3) (2021-05-06)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.3...2.17.3)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Nuke: workfile version synced to db version always [\#1479](https://github.com/pypeclub/OpenPype/pull/1479) ([jezscha](https://github.com/jezscha))
|
||||
|
||||
## [CI/3.0.0-rc.3](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.3) (2021-05-05)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.2...CI/3.0.0-rc.3)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Path entity with placeholder [\#1473](https://github.com/pypeclub/OpenPype/pull/1473) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Burnin custom font filepath [\#1472](https://github.com/pypeclub/OpenPype/pull/1472) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Poetry: Move to OpenPype [\#1449](https://github.com/pypeclub/OpenPype/pull/1449) ([antirotor](https://github.com/antirotor))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Mac SSL path needs to be relative to pype\_root [\#1469](https://github.com/pypeclub/OpenPype/issues/1469)
|
||||
- Resolve: fix loading clips to timeline [\#1421](https://github.com/pypeclub/OpenPype/issues/1421)
|
||||
- Wrong handling of slashes when loading on mac [\#1411](https://github.com/pypeclub/OpenPype/issues/1411)
|
||||
- Nuke openpype3 [\#1342](https://github.com/pypeclub/OpenPype/issues/1342)
|
||||
- Houdini launcher [\#1171](https://github.com/pypeclub/OpenPype/issues/1171)
|
||||
- Fix SyncServer get\_enabled\_projects should handle global state [\#1475](https://github.com/pypeclub/OpenPype/pull/1475) ([kalisp](https://github.com/kalisp))
|
||||
- Igniter buttons enable/disable fix [\#1474](https://github.com/pypeclub/OpenPype/pull/1474) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Mac SSL path needs to be relative to pype\_root [\#1470](https://github.com/pypeclub/OpenPype/pull/1470) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Resolve: 17 compatibility issues and load image sequences [\#1422](https://github.com/pypeclub/OpenPype/pull/1422) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
|
||||
## [CI/3.0.0-rc.2](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.2) (2021-05-04)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.2...CI/3.0.0-rc.2)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Extract burnins with sequences [\#1467](https://github.com/pypeclub/OpenPype/pull/1467) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Extract burnins with color setting [\#1466](https://github.com/pypeclub/OpenPype/pull/1466) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix groups check in Python 2 [\#1468](https://github.com/pypeclub/OpenPype/pull/1468) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
|
||||
## [2.17.2](https://github.com/pypeclub/openpype/tree/2.17.2) (2021-05-04)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.1...2.17.2)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Forward/Backward compatible apps and tools with OpenPype 3 [\#1463](https://github.com/pypeclub/OpenPype/pull/1463) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
|
||||
## [CI/3.0.0-rc.1](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.1) (2021-05-04)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.1...CI/3.0.0-rc.1)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Only show studio settings to admins [\#1406](https://github.com/pypeclub/OpenPype/issues/1406)
|
||||
- Ftrack specific settings save warning messages [\#1458](https://github.com/pypeclub/OpenPype/pull/1458) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Faster settings actions [\#1446](https://github.com/pypeclub/OpenPype/pull/1446) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Feature/sync server priority [\#1444](https://github.com/pypeclub/OpenPype/pull/1444) ([kalisp](https://github.com/kalisp))
|
||||
- Faster settings UI loading [\#1442](https://github.com/pypeclub/OpenPype/pull/1442) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Igniter re-write [\#1441](https://github.com/pypeclub/OpenPype/pull/1441) ([mkolar](https://github.com/mkolar))
|
||||
- Wrap openpype build into installers [\#1419](https://github.com/pypeclub/OpenPype/pull/1419) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Extract review first documentation [\#1404](https://github.com/pypeclub/OpenPype/pull/1404) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Blender PySide2 install guide [\#1403](https://github.com/pypeclub/OpenPype/pull/1403) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Nuke: deadline submission with gpu [\#1394](https://github.com/pypeclub/OpenPype/pull/1394) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Igniter: Reverse item filter for OpenPype version [\#1349](https://github.com/pypeclub/OpenPype/pull/1349) ([antirotor](https://github.com/antirotor))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- OpenPype Mongo URL definition [\#1450](https://github.com/pypeclub/OpenPype/issues/1450)
|
||||
- Various typos and smaller fixes [\#1464](https://github.com/pypeclub/OpenPype/pull/1464) ([mkolar](https://github.com/mkolar))
|
||||
- Validation of dynamic items in settings [\#1462](https://github.com/pypeclub/OpenPype/pull/1462) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- List can handle new items correctly [\#1459](https://github.com/pypeclub/OpenPype/pull/1459) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Settings actions process fix [\#1457](https://github.com/pypeclub/OpenPype/pull/1457) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Add to overrides actions fix [\#1456](https://github.com/pypeclub/OpenPype/pull/1456) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- OpenPype Mongo URL definition [\#1455](https://github.com/pypeclub/OpenPype/pull/1455) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Global settings save/load out of system settings [\#1447](https://github.com/pypeclub/OpenPype/pull/1447) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Keep metadata on remove overrides [\#1445](https://github.com/pypeclub/OpenPype/pull/1445) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Nuke: fixing undo for loaded mov and sequence [\#1432](https://github.com/pypeclub/OpenPype/pull/1432) ([jezscha](https://github.com/jezscha))
|
||||
- ExtractReview skip empty strings from settings [\#1431](https://github.com/pypeclub/OpenPype/pull/1431) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Bugfix Sync server tweaks [\#1430](https://github.com/pypeclub/OpenPype/pull/1430) ([kalisp](https://github.com/kalisp))
|
||||
- Hiero: missing thumbnail in review [\#1429](https://github.com/pypeclub/OpenPype/pull/1429) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Bugfix Maya in deadline for OpenPype [\#1428](https://github.com/pypeclub/OpenPype/pull/1428) ([kalisp](https://github.com/kalisp))
|
||||
- AE - validation for duration was 1 frame shorter [\#1427](https://github.com/pypeclub/OpenPype/pull/1427) ([kalisp](https://github.com/kalisp))
|
||||
- Houdini menu filename [\#1418](https://github.com/pypeclub/OpenPype/pull/1418) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Fix Avalon plugins attribute overrides [\#1413](https://github.com/pypeclub/OpenPype/pull/1413) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Nuke: submit to Deadline fails [\#1409](https://github.com/pypeclub/OpenPype/pull/1409) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Validate MongoDB Url on start [\#1407](https://github.com/pypeclub/OpenPype/pull/1407) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Nuke: fix set colorspace with new settings [\#1386](https://github.com/pypeclub/OpenPype/pull/1386) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- MacOs build and install issues [\#1380](https://github.com/pypeclub/OpenPype/pull/1380) ([mkolar](https://github.com/mkolar))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- test [\#1452](https://github.com/pypeclub/OpenPype/issues/1452)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- TVPaint frame range definition [\#1425](https://github.com/pypeclub/OpenPype/pull/1425) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Only show studio settings to admins [\#1420](https://github.com/pypeclub/OpenPype/pull/1420) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- TVPaint documentation [\#1305](https://github.com/pypeclub/OpenPype/pull/1305) ([64qam](https://github.com/64qam))
|
||||
|
||||
## [2.17.1](https://github.com/pypeclub/openpype/tree/2.17.1) (2021-04-30)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.0...2.17.1)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Nuke: deadline submission with gpu [\#1414](https://github.com/pypeclub/OpenPype/pull/1414)
|
||||
- TVPaint frame range definition [\#1424](https://github.com/pypeclub/OpenPype/pull/1424)
|
||||
- PS - group all published instances [\#1415](https://github.com/pypeclub/OpenPype/pull/1415)
|
||||
- Add task name to context pop up. [\#1383](https://github.com/pypeclub/OpenPype/pull/1383)
|
||||
- Enhance review letterbox feature. [\#1371](https://github.com/pypeclub/OpenPype/pull/1371)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Houdini menu filename [\#1417](https://github.com/pypeclub/OpenPype/pull/1417)
|
||||
- AE - validation for duration was 1 frame shorter [\#1426](https://github.com/pypeclub/OpenPype/pull/1426)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Maya: Vray - problem getting all file nodes for look publishing [\#1399](https://github.com/pypeclub/OpenPype/pull/1399)
|
||||
- Maya: Support for Redshift proxies [\#1360](https://github.com/pypeclub/OpenPype/pull/1360)
|
||||
|
||||
## [2.17.0](https://github.com/pypeclub/openpype/tree/2.17.0) (2021-04-20)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-beta.2...2.17.0)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Forward compatible ftrack group [\#1243](https://github.com/pypeclub/OpenPype/pull/1243)
|
||||
- Settings in mongo as dict [\#1221](https://github.com/pypeclub/OpenPype/pull/1221)
|
||||
- Maya: Make tx option configurable with presets [\#1328](https://github.com/pypeclub/OpenPype/pull/1328)
|
||||
- TVPaint asset name validation [\#1302](https://github.com/pypeclub/OpenPype/pull/1302)
|
||||
- TV Paint: Set initial project settings. [\#1299](https://github.com/pypeclub/OpenPype/pull/1299)
|
||||
- TV Paint: Validate mark in and out. [\#1298](https://github.com/pypeclub/OpenPype/pull/1298)
|
||||
- Validate project settings [\#1297](https://github.com/pypeclub/OpenPype/pull/1297)
|
||||
- After Effects: added SubsetManager [\#1234](https://github.com/pypeclub/OpenPype/pull/1234)
|
||||
- Show error message in pyblish UI [\#1206](https://github.com/pypeclub/OpenPype/pull/1206)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Hiero: fixing source frame from correct object [\#1362](https://github.com/pypeclub/OpenPype/pull/1362)
|
||||
- Nuke: fix colourspace, prerenders and nuke panes opening [\#1308](https://github.com/pypeclub/OpenPype/pull/1308)
|
||||
- AE remove orphaned instance from workfile - fix self.stub [\#1282](https://github.com/pypeclub/OpenPype/pull/1282)
|
||||
- Nuke: deadline submission with search replaced env values from preset [\#1194](https://github.com/pypeclub/OpenPype/pull/1194)
|
||||
- Ftrack custom attributes in bulks [\#1312](https://github.com/pypeclub/OpenPype/pull/1312)
|
||||
- Ftrack optional pypclub role [\#1303](https://github.com/pypeclub/OpenPype/pull/1303)
|
||||
- After Effects: remove orphaned instances [\#1275](https://github.com/pypeclub/OpenPype/pull/1275)
|
||||
- Avalon schema names [\#1242](https://github.com/pypeclub/OpenPype/pull/1242)
|
||||
- Handle duplication of Task name [\#1226](https://github.com/pypeclub/OpenPype/pull/1226)
|
||||
- Modified path of plugin loads for Harmony and TVPaint [\#1217](https://github.com/pypeclub/OpenPype/pull/1217)
|
||||
- Regex checks in profiles filtering [\#1214](https://github.com/pypeclub/OpenPype/pull/1214)
|
||||
- Bulk mov strict task [\#1204](https://github.com/pypeclub/OpenPype/pull/1204)
|
||||
- Update custom ftrack session attributes [\#1202](https://github.com/pypeclub/OpenPype/pull/1202)
|
||||
- Nuke: write node colorspace ignore `default\(\)` label [\#1199](https://github.com/pypeclub/OpenPype/pull/1199)
|
||||
- Nuke: reverse search to make it more versatile [\#1178](https://github.com/pypeclub/OpenPype/pull/1178)
|
||||
|
||||
|
||||
|
||||
## [2.16.0](https://github.com/pypeclub/pype/tree/2.16.0) (2021-03-22)
|
||||
|
||||
|
|
@ -1086,7 +1674,9 @@ A large cleanup release. Most of the change are under the hood.
|
|||
- _(avalon)_ subsets in maya 2019 weren't behaving correctly in the outliner
|
||||
|
||||
|
||||
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
||||
|
||||
|
||||
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
## How to contribute to Pype
|
||||
|
||||
We are always happy for any contributions for OpenPype improvements. Before making a PR and starting working on an issue, please read these simple guidelines.
|
||||
|
||||
#### **Did you find a bug?**
|
||||
|
||||
1. Check in the issues and our [bug triage[(https://github.com/pypeclub/pype/projects/2) to make sure it wasn't reported already.
|
||||
|
|
@ -13,11 +15,11 @@
|
|||
- Open a new GitHub pull request with the patch.
|
||||
- Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
|
||||
|
||||
|
||||
#### **Do you intend to add a new feature or change an existing one?**
|
||||
|
||||
- Open a new thread in the [github discussions](https://github.com/pypeclub/pype/discussions/new)
|
||||
- Do not open issue untill the suggestion is discussed. We will convert accepted suggestions into backlog and point them to the relevant discussion thread to keep the context.
|
||||
- Do not open issue until the suggestion is discussed. We will convert accepted suggestions into backlog and point them to the relevant discussion thread to keep the context.
|
||||
- If you are already working on a new feature and you'd like it eventually merged to the main codebase, please consider making a DRAFT PR as soon as possible. This makes it a lot easier to give feedback, discuss the code and functionalit, plus it prevents multiple people tackling the same problem independently.
|
||||
|
||||
#### **Do you have questions about the source code?**
|
||||
|
||||
|
|
@ -41,13 +43,11 @@ A few important notes about 2.x and 3.x development:
|
|||
- Please keep the corresponding 2 and 3 PR names the same so they can be easily identified from the PR list page.
|
||||
- Each 2.x PR should be labeled with `2.x-dev` label.
|
||||
|
||||
Inside each PR, put a link to the corresponding PR
|
||||
Inside each PR, put a link to the corresponding PR for the other version
|
||||
|
||||
Of course if you want to contribute, feel free to make a PR to only 2.x/develop or develop, based on what you are using. While reviewing the PRs, we might convert the code to corresponding PR for the other release ourselves.
|
||||
|
||||
We might also change the target of you PR to and intermediate branch, rather than `develop` if we feel it requires some extra work on our end. That way, we preserve all your commits so you don't loos out on the contribution credits.
|
||||
|
||||
|
||||
We might also change the target of you PR to and intermediate branch, rather than `develop` if we feel it requires some extra work on our end. That way, we preserve all your commits so you don't loose out on the contribution credits.
|
||||
|
||||
|
||||
If a PR is targeted at 2.x release it must be labelled with 2x-dev label in Github.
|
||||
|
|
|
|||
82
Dockerfile
Normal file
82
Dockerfile
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# Build Pype docker image
|
||||
FROM centos:7 AS builder
|
||||
ARG OPENPYPE_PYTHON_VERSION=3.7.10
|
||||
|
||||
LABEL org.opencontainers.image.name="pypeclub/openpype"
|
||||
LABEL org.opencontainers.image.title="OpenPype Docker Image"
|
||||
LABEL org.opencontainers.image.url="https://openpype.io/"
|
||||
LABEL org.opencontainers.image.source="https://github.com/pypeclub/pype"
|
||||
|
||||
USER root
|
||||
|
||||
# update base
|
||||
RUN yum -y install deltarpm \
|
||||
&& yum -y update \
|
||||
&& yum clean all
|
||||
|
||||
# add tools we need
|
||||
RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm \
|
||||
&& yum -y install centos-release-scl \
|
||||
&& yum -y install \
|
||||
bash \
|
||||
which \
|
||||
git \
|
||||
devtoolset-7-gcc* \
|
||||
make \
|
||||
cmake \
|
||||
curl \
|
||||
wget \
|
||||
gcc \
|
||||
zlib-devel \
|
||||
bzip2 \
|
||||
bzip2-devel \
|
||||
readline-devel \
|
||||
sqlite sqlite-devel \
|
||||
openssl-devel \
|
||||
tk-devel libffi-devel \
|
||||
qt5-qtbase-devel \
|
||||
patchelf \
|
||||
&& yum clean all
|
||||
|
||||
RUN mkdir /opt/openpype
|
||||
# RUN useradd -m pype
|
||||
# RUN chown pype /opt/openpype
|
||||
# USER pype
|
||||
|
||||
RUN curl https://pyenv.run | bash
|
||||
ENV PYTHON_CONFIGURE_OPTS --enable-shared
|
||||
|
||||
RUN echo 'export PATH="$HOME/.pyenv/bin:$PATH"'>> $HOME/.bashrc \
|
||||
&& echo 'eval "$(pyenv init -)"' >> $HOME/.bashrc \
|
||||
&& echo 'eval "$(pyenv virtualenv-init -)"' >> $HOME/.bashrc \
|
||||
&& echo 'eval "$(pyenv init --path)"' >> $HOME/.bashrc
|
||||
RUN source $HOME/.bashrc && pyenv install ${OPENPYPE_PYTHON_VERSION}
|
||||
|
||||
COPY . /opt/openpype/
|
||||
RUN rm -rf /openpype/.poetry || echo "No Poetry installed yet."
|
||||
# USER root
|
||||
# RUN chown -R pype /opt/openpype
|
||||
RUN chmod +x /opt/openpype/tools/create_env.sh && chmod +x /opt/openpype/tools/build.sh
|
||||
|
||||
# USER pype
|
||||
|
||||
WORKDIR /opt/openpype
|
||||
|
||||
RUN cd /opt/openpype \
|
||||
&& source $HOME/.bashrc \
|
||||
&& pyenv local ${OPENPYPE_PYTHON_VERSION}
|
||||
|
||||
RUN source $HOME/.bashrc \
|
||||
&& ./tools/create_env.sh
|
||||
|
||||
RUN source $HOME/.bashrc \
|
||||
&& ./tools/fetch_thirdparty_libs.sh
|
||||
|
||||
RUN source $HOME/.bashrc \
|
||||
&& bash ./tools/build.sh \
|
||||
&& cp /usr/lib64/libffi* ./build/exe.linux-x86_64-3.7/lib \
|
||||
&& cp /usr/lib64/libssl* ./build/exe.linux-x86_64-3.7/lib \
|
||||
&& cp /usr/lib64/libcrypto* ./build/exe.linux-x86_64-3.7/lib
|
||||
|
||||
RUN cd /opt/openpype \
|
||||
rm -rf ./vendor/bin
|
||||
514
HISTORY.md
514
HISTORY.md
|
|
@ -1,3 +1,514 @@
|
|||
# Changelog
|
||||
|
||||
|
||||
## [3.0.0](https://github.com/pypeclub/openpype/tree/3.0.0)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/2.18.1...3.0.0)
|
||||
|
||||
### Configuration
|
||||
- Studio Settings GUI: no more json configuration files.
|
||||
- OpenPype Modules can be turned on and off.
|
||||
- Easy to add Application versions.
|
||||
- Per Project Environment and plugin management.
|
||||
- Robust profile system for creating reviewables and burnins, with filtering based on Application, Task and data family.
|
||||
- Configurable publish plugins.
|
||||
- Options to make any validator or extractor, optional or disabled.
|
||||
- Color Management is now unified under anatomy settings.
|
||||
- Subset naming and grouping is fully configurable.
|
||||
- All project attributes can now be set directly in OpenPype settings.
|
||||
- Studio Setting can be locked to prevent unwanted artist changes.
|
||||
- You can now add per project and per task type templates for workfile initialization in most hosts.
|
||||
- Too many other individual configurable option to list in this changelog :)
|
||||
|
||||
### Local Settings
|
||||
- Local Settings GUI where users can change certain option on individual basis.
|
||||
- Application executables.
|
||||
- Project roots.
|
||||
- Project site sync settings.
|
||||
|
||||
### Build, Installation and Deployments
|
||||
- No requirements on artist machine.
|
||||
- Fully distributed workflow possible.
|
||||
- Self-contained installation.
|
||||
- Available on all three major platforms.
|
||||
- Automatic artist OpenPype updates.
|
||||
- Studio OpenPype repository for updates distribution.
|
||||
- Robust Build system.
|
||||
- Safe studio update versioning with staging and production options.
|
||||
- MacOS build generates .app and .dmg installer.
|
||||
- Windows build with installer creation script.
|
||||
|
||||
### Misc
|
||||
- System and diagnostic info tool in the tray.
|
||||
- Launching application from Launcher indicates activity.
|
||||
- All project roots are now named. Single root project are now achieved by having a single named root in the project anatomy.
|
||||
- Every project root is cast into environment variable as well, so it can be used in DCC instead of absolute path (depends on DCC support for env vars).
|
||||
- Basic support for task types, on top of task names.
|
||||
- Timer now change automatically when the context is switched inside running application.
|
||||
- 'Master" versions have been renamed to "Hero".
|
||||
- Extract Burnins now supports file sequences and color settings.
|
||||
- Extract Review support overscan cropping, better letterboxes and background colour fill.
|
||||
- Delivery tool for copying and renaming any published assets in bulk.
|
||||
- Harmony, Photoshop and After Effects now connect directly with OpenPype tray instead of spawning their own terminal.
|
||||
|
||||
### Project Manager GUI
|
||||
- Create Projects.
|
||||
- Create Shots and Assets.
|
||||
- Create Tasks and assign task types.
|
||||
- Fill required asset attributes.
|
||||
- Validations for duplicated or unsupported names.
|
||||
- Archive Assets.
|
||||
- Move Asset within hierarchy.
|
||||
|
||||
### Site Sync (beta)
|
||||
- Synchronization of published files between workstations and central storage.
|
||||
- Ability to add arbitrary storage providers to the Site Sync system.
|
||||
- Default setup includes Disk and Google Drive providers as examples.
|
||||
- Access to availability information from Loader and Scene Manager.
|
||||
- Sync queue GUI with filtering, error and status reporting.
|
||||
- Site sync can be configured on a per-project basis.
|
||||
- Bulk upload and download from the loader.
|
||||
|
||||
### Ftrack
|
||||
- Actions have customisable roles.
|
||||
- Settings on all actions are updated live and don't need openpype restart.
|
||||
- Ftrack module can now be turned off completely.
|
||||
- It is enough to specify ftrack server name and the URL will be formed correctly. So instead of mystudio.ftrackapp.com, it's possible to use simply: "mystudio".
|
||||
|
||||
### Editorial
|
||||
- Fully OTIO based editorial publishing.
|
||||
- Completely re-done Hiero publishing to be a lot simpler and faster.
|
||||
- Consistent conforming from Resolve, Hiero and Standalone Publisher.
|
||||
|
||||
### Backend
|
||||
- OpenPype and Avalon now always share the same database (in 2.x is was possible to split them).
|
||||
- Major codebase refactoring to allow for better CI, versioning and control of individual integrations.
|
||||
- OTIO is bundled with build.
|
||||
- OIIO is bundled with build.
|
||||
- FFMPEG is bundled with build.
|
||||
- Rest API and host WebSocket servers have been unified into a single local webserver.
|
||||
- Maya look assigner has been integrated into the main codebase.
|
||||
- Publish GUI has been integrated into the main codebase.
|
||||
- Studio and Project settings overrides are now stored in Mongo.
|
||||
- Too many other backend fixes and tweaks to list :), you can see full changelog on github for those.
|
||||
- OpenPype uses Poetry to manage it's virtual environment when running from code.
|
||||
- all applications can be marked as python 2 or 3 compatible to make the switch a bit easier.
|
||||
|
||||
|
||||
### Pull Requests since 3.0.0-rc.6
|
||||
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- settings: task types enum entity [\#1605](https://github.com/pypeclub/OpenPype/issues/1605)
|
||||
- Settings: ignore keys in referenced schema [\#1600](https://github.com/pypeclub/OpenPype/issues/1600)
|
||||
- Maya: support for frame steps and frame lists [\#1585](https://github.com/pypeclub/OpenPype/issues/1585)
|
||||
- TVPaint: Publish workfile. [\#1548](https://github.com/pypeclub/OpenPype/issues/1548)
|
||||
- Loader: Current Asset Button [\#1448](https://github.com/pypeclub/OpenPype/issues/1448)
|
||||
- Hiero: publish with retiming [\#1377](https://github.com/pypeclub/OpenPype/issues/1377)
|
||||
- Ask user to restart after changing global environments in settings [\#910](https://github.com/pypeclub/OpenPype/issues/910)
|
||||
- add option to define paht to workfile template [\#895](https://github.com/pypeclub/OpenPype/issues/895)
|
||||
- Harmony: move server console to system tray [\#676](https://github.com/pypeclub/OpenPype/issues/676)
|
||||
- Standalone style [\#1630](https://github.com/pypeclub/OpenPype/pull/1630) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Faster hierarchical values push [\#1627](https://github.com/pypeclub/OpenPype/pull/1627) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Launcher tool style [\#1624](https://github.com/pypeclub/OpenPype/pull/1624) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Loader and Library loader enhancements [\#1623](https://github.com/pypeclub/OpenPype/pull/1623) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Tray style [\#1622](https://github.com/pypeclub/OpenPype/pull/1622) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Maya schemas cleanup [\#1610](https://github.com/pypeclub/OpenPype/pull/1610) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Settings: ignore keys in referenced schema [\#1608](https://github.com/pypeclub/OpenPype/pull/1608) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- settings: task types enum entity [\#1606](https://github.com/pypeclub/OpenPype/pull/1606) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Openpype style [\#1604](https://github.com/pypeclub/OpenPype/pull/1604) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- TVPaint: Publish workfile. [\#1597](https://github.com/pypeclub/OpenPype/pull/1597) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Nuke: add option to define path to workfile template [\#1571](https://github.com/pypeclub/OpenPype/pull/1571) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Crop overscan in Extract Review [\#1569](https://github.com/pypeclub/OpenPype/pull/1569) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Unreal and Blender: Material Workflow [\#1562](https://github.com/pypeclub/OpenPype/pull/1562) ([simonebarbieri](https://github.com/simonebarbieri))
|
||||
- Harmony: move server console to system tray [\#1560](https://github.com/pypeclub/OpenPype/pull/1560) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Ask user to restart after changing global environments in settings [\#1550](https://github.com/pypeclub/OpenPype/pull/1550) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Hiero: publish with retiming [\#1545](https://github.com/pypeclub/OpenPype/pull/1545) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Library loader load asset documents on OpenPype start [\#1603](https://github.com/pypeclub/OpenPype/issues/1603)
|
||||
- Resolve: unable to load the same footage twice [\#1317](https://github.com/pypeclub/OpenPype/issues/1317)
|
||||
- Resolve: unable to load footage [\#1316](https://github.com/pypeclub/OpenPype/issues/1316)
|
||||
- Add required Python 2 modules [\#1291](https://github.com/pypeclub/OpenPype/issues/1291)
|
||||
- GUi scaling with hires displays [\#705](https://github.com/pypeclub/OpenPype/issues/705)
|
||||
- Maya: non unicode string in publish validation [\#673](https://github.com/pypeclub/OpenPype/issues/673)
|
||||
- Nuke: Rendered Frame validation is triggered by multiple collections [\#156](https://github.com/pypeclub/OpenPype/issues/156)
|
||||
- avalon-core debugging failing [\#80](https://github.com/pypeclub/OpenPype/issues/80)
|
||||
- Only check arnold shading group if arnold is used [\#72](https://github.com/pypeclub/OpenPype/issues/72)
|
||||
- Sync server Qt layout fix [\#1621](https://github.com/pypeclub/OpenPype/pull/1621) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Console Listener on Python 2 fix [\#1620](https://github.com/pypeclub/OpenPype/pull/1620) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Bug: Initialize blessed term only in console mode [\#1619](https://github.com/pypeclub/OpenPype/pull/1619) ([antirotor](https://github.com/antirotor))
|
||||
- Settings template skip paths support wrappers [\#1618](https://github.com/pypeclub/OpenPype/pull/1618) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Maya capture 'isolate\_view' fix + minor corrections [\#1617](https://github.com/pypeclub/OpenPype/pull/1617) ([2-REC](https://github.com/2-REC))
|
||||
- MacOs Fix launch of standalone publisher [\#1616](https://github.com/pypeclub/OpenPype/pull/1616) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- 'Delivery action' report fix + typos [\#1612](https://github.com/pypeclub/OpenPype/pull/1612) ([2-REC](https://github.com/2-REC))
|
||||
- List append fix in mutable dict settings [\#1599](https://github.com/pypeclub/OpenPype/pull/1599) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Documentation: Maya: fix review [\#1598](https://github.com/pypeclub/OpenPype/pull/1598) ([antirotor](https://github.com/antirotor))
|
||||
- Bugfix: Set certifi CA bundle for all platforms [\#1596](https://github.com/pypeclub/OpenPype/pull/1596) ([antirotor](https://github.com/antirotor))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Bump dns-packet from 1.3.1 to 1.3.4 in /website [\#1611](https://github.com/pypeclub/OpenPype/pull/1611) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- Maya: Render workflow fixes [\#1607](https://github.com/pypeclub/OpenPype/pull/1607) ([antirotor](https://github.com/antirotor))
|
||||
- Maya: support for frame steps and frame lists [\#1586](https://github.com/pypeclub/OpenPype/pull/1586) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- 3.0.0 - curated changelog [\#1284](https://github.com/pypeclub/OpenPype/pull/1284) ([mkolar](https://github.com/mkolar))
|
||||
|
||||
## [2.18.1](https://github.com/pypeclub/openpype/tree/2.18.1) (2021-06-03)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/2.18.0...2.18.1)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Faster hierarchical values push [\#1626](https://github.com/pypeclub/OpenPype/pull/1626)
|
||||
- Feature Delivery in library loader [\#1549](https://github.com/pypeclub/OpenPype/pull/1549)
|
||||
- Hiero: Initial frame publish support. [\#1172](https://github.com/pypeclub/OpenPype/pull/1172)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Maya capture 'isolate\_view' fix + minor corrections [\#1614](https://github.com/pypeclub/OpenPype/pull/1614)
|
||||
- 'Delivery action' report fix +typos [\#1613](https://github.com/pypeclub/OpenPype/pull/1613)
|
||||
- Delivery in LibraryLoader - fixed sequence issue [\#1590](https://github.com/pypeclub/OpenPype/pull/1590)
|
||||
- FFmpeg filters in quote marks [\#1588](https://github.com/pypeclub/OpenPype/pull/1588)
|
||||
- Ftrack delete action cause circular error [\#1581](https://github.com/pypeclub/OpenPype/pull/1581)
|
||||
- Fix Maya playblast. [\#1566](https://github.com/pypeclub/OpenPype/pull/1566)
|
||||
- More failsafes prevent errored runs. [\#1554](https://github.com/pypeclub/OpenPype/pull/1554)
|
||||
- Celaction publishing [\#1539](https://github.com/pypeclub/OpenPype/pull/1539)
|
||||
- celaction: app not starting [\#1533](https://github.com/pypeclub/OpenPype/pull/1533)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Maya: Render workflow fixes - 2.0 backport [\#1609](https://github.com/pypeclub/OpenPype/pull/1609)
|
||||
- Maya Hardware support [\#1553](https://github.com/pypeclub/OpenPype/pull/1553)
|
||||
|
||||
|
||||
## [CI/3.0.0-rc.6](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.6) (2021-05-27)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.5...CI/3.0.0-rc.6)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Hiero: publish color and transformation soft-effects [\#1376](https://github.com/pypeclub/OpenPype/issues/1376)
|
||||
- Get rid of `AVALON\_HIERARCHY` and `hiearchy` key on asset [\#432](https://github.com/pypeclub/OpenPype/issues/432)
|
||||
- Sync to avalon do not store hierarchy key [\#1582](https://github.com/pypeclub/OpenPype/pull/1582) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Tools: launcher scripts for project manager [\#1557](https://github.com/pypeclub/OpenPype/pull/1557) ([antirotor](https://github.com/antirotor))
|
||||
- Simple tvpaint publish [\#1555](https://github.com/pypeclub/OpenPype/pull/1555) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Feature Delivery in library loader [\#1546](https://github.com/pypeclub/OpenPype/pull/1546) ([kalisp](https://github.com/kalisp))
|
||||
- Documentation: Dev and system build documentation [\#1543](https://github.com/pypeclub/OpenPype/pull/1543) ([antirotor](https://github.com/antirotor))
|
||||
- Color entity [\#1542](https://github.com/pypeclub/OpenPype/pull/1542) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Extract review bg color [\#1534](https://github.com/pypeclub/OpenPype/pull/1534) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- TVPaint loader settings [\#1530](https://github.com/pypeclub/OpenPype/pull/1530) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Blender can initialize differente user script paths [\#1528](https://github.com/pypeclub/OpenPype/pull/1528) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Blender and Unreal: Improved Animation Workflow [\#1514](https://github.com/pypeclub/OpenPype/pull/1514) ([simonebarbieri](https://github.com/simonebarbieri))
|
||||
- Hiero: publish color and transformation soft-effects [\#1511](https://github.com/pypeclub/OpenPype/pull/1511) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- OpenPype specific version issues [\#1583](https://github.com/pypeclub/OpenPype/issues/1583)
|
||||
- Ftrack login server can't work without stderr [\#1576](https://github.com/pypeclub/OpenPype/issues/1576)
|
||||
- Mac application launch [\#1575](https://github.com/pypeclub/OpenPype/issues/1575)
|
||||
- Settings are not propagated to Nuke write nodes [\#1538](https://github.com/pypeclub/OpenPype/issues/1538)
|
||||
- Subset names settings not applied for publishing [\#1537](https://github.com/pypeclub/OpenPype/issues/1537)
|
||||
- Nuke: callback at start not setting colorspace [\#1412](https://github.com/pypeclub/OpenPype/issues/1412)
|
||||
- Pype 3: Missing icon for Settings [\#1272](https://github.com/pypeclub/OpenPype/issues/1272)
|
||||
- Blender: cannot initialize Avalon if BLENDER\_USER\_SCRIPTS is already used [\#1050](https://github.com/pypeclub/OpenPype/issues/1050)
|
||||
- Ftrack delete action cause circular error [\#206](https://github.com/pypeclub/OpenPype/issues/206)
|
||||
- Build: stop cleaning of pyc files in build directory [\#1592](https://github.com/pypeclub/OpenPype/pull/1592) ([antirotor](https://github.com/antirotor))
|
||||
- Ftrack login server can't work without stderr [\#1591](https://github.com/pypeclub/OpenPype/pull/1591) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- FFmpeg filters in quote marks [\#1589](https://github.com/pypeclub/OpenPype/pull/1589) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- OpenPype specific version issues [\#1584](https://github.com/pypeclub/OpenPype/pull/1584) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Mac application launch [\#1580](https://github.com/pypeclub/OpenPype/pull/1580) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Ftrack delete action cause circular error [\#1579](https://github.com/pypeclub/OpenPype/pull/1579) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Hiero: publishing issues [\#1578](https://github.com/pypeclub/OpenPype/pull/1578) ([jezscha](https://github.com/jezscha))
|
||||
- Nuke: callback at start not setting colorspace [\#1561](https://github.com/pypeclub/OpenPype/pull/1561) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Bugfix PS subset and quick review [\#1541](https://github.com/pypeclub/OpenPype/pull/1541) ([kalisp](https://github.com/kalisp))
|
||||
- Settings are not propagated to Nuke write nodes [\#1540](https://github.com/pypeclub/OpenPype/pull/1540) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- OpenPype: Powershell scripts polishing [\#1536](https://github.com/pypeclub/OpenPype/pull/1536) ([antirotor](https://github.com/antirotor))
|
||||
- Host name collecting fix [\#1535](https://github.com/pypeclub/OpenPype/pull/1535) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Handle duplicated task names in project manager [\#1531](https://github.com/pypeclub/OpenPype/pull/1531) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Validate is file attribute in settings schema [\#1529](https://github.com/pypeclub/OpenPype/pull/1529) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Bump postcss from 8.2.8 to 8.3.0 in /website [\#1593](https://github.com/pypeclub/OpenPype/pull/1593) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- User installation documentation [\#1532](https://github.com/pypeclub/OpenPype/pull/1532) ([64qam](https://github.com/64qam))
|
||||
|
||||
## [CI/3.0.0-rc.5](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.5) (2021-05-19)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/2.18.0...CI/3.0.0-rc.5)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- OpenPype: Build - Add progress bars [\#1524](https://github.com/pypeclub/OpenPype/pull/1524) ([antirotor](https://github.com/antirotor))
|
||||
- Default environments per host imlementation [\#1522](https://github.com/pypeclub/OpenPype/pull/1522) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- OpenPype: use `semver` module for version resolution [\#1513](https://github.com/pypeclub/OpenPype/pull/1513) ([antirotor](https://github.com/antirotor))
|
||||
- Feature Aftereffects setting cleanup documentation [\#1510](https://github.com/pypeclub/OpenPype/pull/1510) ([kalisp](https://github.com/kalisp))
|
||||
- Feature Sync server settings enhancement [\#1501](https://github.com/pypeclub/OpenPype/pull/1501) ([kalisp](https://github.com/kalisp))
|
||||
- Project manager [\#1396](https://github.com/pypeclub/OpenPype/pull/1396) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Unified schema definition [\#874](https://github.com/pypeclub/OpenPype/issues/874)
|
||||
- Maya: fix look assignment [\#1526](https://github.com/pypeclub/OpenPype/pull/1526) ([antirotor](https://github.com/antirotor))
|
||||
- Bugfix Sync server local site issues [\#1523](https://github.com/pypeclub/OpenPype/pull/1523) ([kalisp](https://github.com/kalisp))
|
||||
- Store as list dictionary check initial value with right type [\#1520](https://github.com/pypeclub/OpenPype/pull/1520) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Maya: wrong collection of playblasted frames [\#1515](https://github.com/pypeclub/OpenPype/pull/1515) ([mkolar](https://github.com/mkolar))
|
||||
- Convert pyblish logs to string at the moment of logging [\#1512](https://github.com/pypeclub/OpenPype/pull/1512) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- 3.0 | nuke: fixing start\_at with option gui [\#1509](https://github.com/pypeclub/OpenPype/pull/1509) ([jezscha](https://github.com/jezscha))
|
||||
- Tests: fix pype -\> openpype to make tests work again [\#1508](https://github.com/pypeclub/OpenPype/pull/1508) ([antirotor](https://github.com/antirotor))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- OpenPype: disable submodule update with `--no-submodule-update` [\#1525](https://github.com/pypeclub/OpenPype/pull/1525) ([antirotor](https://github.com/antirotor))
|
||||
- Ftrack without autosync in Pype 3 [\#1519](https://github.com/pypeclub/OpenPype/pull/1519) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Feature Harmony setting cleanup documentation [\#1506](https://github.com/pypeclub/OpenPype/pull/1506) ([kalisp](https://github.com/kalisp))
|
||||
- Sync Server beginning of documentation [\#1471](https://github.com/pypeclub/OpenPype/pull/1471) ([kalisp](https://github.com/kalisp))
|
||||
- Blender: publish layout json [\#1348](https://github.com/pypeclub/OpenPype/pull/1348) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
|
||||
## [2.18.0](https://github.com/pypeclub/openpype/tree/2.18.0) (2021-05-18)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.4...2.18.0)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Default environments per host imlementation [\#1405](https://github.com/pypeclub/OpenPype/issues/1405)
|
||||
- Blender: publish layout json [\#1346](https://github.com/pypeclub/OpenPype/issues/1346)
|
||||
- Ftrack without autosync in Pype 3 [\#1128](https://github.com/pypeclub/OpenPype/issues/1128)
|
||||
- Launcher: started action indicator [\#1102](https://github.com/pypeclub/OpenPype/issues/1102)
|
||||
- Launch arguments of applications [\#1094](https://github.com/pypeclub/OpenPype/issues/1094)
|
||||
- Publish: instance info [\#724](https://github.com/pypeclub/OpenPype/issues/724)
|
||||
- Review: ability to control review length [\#482](https://github.com/pypeclub/OpenPype/issues/482)
|
||||
- Colorized recognition of creator result [\#394](https://github.com/pypeclub/OpenPype/issues/394)
|
||||
- event assign user to started task [\#49](https://github.com/pypeclub/OpenPype/issues/49)
|
||||
- rebuild containers from reference in maya [\#55](https://github.com/pypeclub/OpenPype/issues/55)
|
||||
- nuke Load metadata [\#66](https://github.com/pypeclub/OpenPype/issues/66)
|
||||
- Maya: Safer handling of expected render output names [\#1496](https://github.com/pypeclub/OpenPype/pull/1496) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- TVPaint: Increment workfile version on successfull publish. [\#1489](https://github.com/pypeclub/OpenPype/pull/1489) ([tokejepsen](https://github.com/tokejepsen))
|
||||
- Use SubsetLoader and multiple contexts for delete\_old\_versions [\#1484](https://github.com/pypeclub/OpenPype/pull/1484) ([tokejepsen](https://github.com/tokejepsen))
|
||||
- Maya: Use of multiple deadline servers [\#1483](https://github.com/pypeclub/OpenPype/pull/1483) ([antirotor](https://github.com/antirotor))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Igniter version resolution doesn't consider it's own version [\#1505](https://github.com/pypeclub/OpenPype/issues/1505)
|
||||
- Maya: Safer handling of expected render output names [\#1159](https://github.com/pypeclub/OpenPype/issues/1159)
|
||||
- Harmony: Invalid render output from non-conventionally named instance [\#871](https://github.com/pypeclub/OpenPype/issues/871)
|
||||
- Existing subsets hints in creator [\#1503](https://github.com/pypeclub/OpenPype/pull/1503) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- nuke: space in node name breaking process [\#1494](https://github.com/pypeclub/OpenPype/pull/1494) ([jezscha](https://github.com/jezscha))
|
||||
- Maya: wrong collection of playblasted frames [\#1517](https://github.com/pypeclub/OpenPype/pull/1517) ([mkolar](https://github.com/mkolar))
|
||||
- Existing subsets hints in creator [\#1502](https://github.com/pypeclub/OpenPype/pull/1502) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Use instance frame start instead of timeline. [\#1486](https://github.com/pypeclub/OpenPype/pull/1486) ([tokejepsen](https://github.com/tokejepsen))
|
||||
- Maya: Redshift - set proper start frame on proxy [\#1480](https://github.com/pypeclub/OpenPype/pull/1480) ([antirotor](https://github.com/antirotor))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Nuke: wrong "star at" value on render load [\#1352](https://github.com/pypeclub/OpenPype/issues/1352)
|
||||
- DV Resolve - loading/updating - image video [\#915](https://github.com/pypeclub/OpenPype/issues/915)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- nuke: fixing start\_at with option gui [\#1507](https://github.com/pypeclub/OpenPype/pull/1507) ([jezscha](https://github.com/jezscha))
|
||||
|
||||
## [CI/3.0.0-rc.4](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.4) (2021-05-12)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.3...CI/3.0.0-rc.4)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Resolve: documentation [\#1490](https://github.com/pypeclub/OpenPype/issues/1490)
|
||||
- Hiero: audio to review [\#1378](https://github.com/pypeclub/OpenPype/issues/1378)
|
||||
- nks color clips after publish [\#44](https://github.com/pypeclub/OpenPype/issues/44)
|
||||
- Store data from modifiable dict as list [\#1504](https://github.com/pypeclub/OpenPype/pull/1504) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Use SubsetLoader and multiple contexts for delete\_old\_versions [\#1497](https://github.com/pypeclub/OpenPype/pull/1497) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Hiero: publish audio and add to review [\#1493](https://github.com/pypeclub/OpenPype/pull/1493) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Resolve: documentation [\#1491](https://github.com/pypeclub/OpenPype/pull/1491) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Change integratenew template profiles setting [\#1487](https://github.com/pypeclub/OpenPype/pull/1487) ([kalisp](https://github.com/kalisp))
|
||||
- Settings tool cleanup [\#1477](https://github.com/pypeclub/OpenPype/pull/1477) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Sorted Applications and Tools in Custom attribute [\#1476](https://github.com/pypeclub/OpenPype/pull/1476) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- PS - group all published instances [\#1416](https://github.com/pypeclub/OpenPype/pull/1416) ([kalisp](https://github.com/kalisp))
|
||||
- OpenPype: Support for Docker [\#1289](https://github.com/pypeclub/OpenPype/pull/1289) ([antirotor](https://github.com/antirotor))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Harmony: palettes publishing [\#1439](https://github.com/pypeclub/OpenPype/issues/1439)
|
||||
- Photoshop: validation for already created images [\#1435](https://github.com/pypeclub/OpenPype/issues/1435)
|
||||
- Nuke Extracts Thumbnail from frame out of shot range [\#963](https://github.com/pypeclub/OpenPype/issues/963)
|
||||
- Instance in same Context repairing [\#390](https://github.com/pypeclub/OpenPype/issues/390)
|
||||
- User Inactivity - Start timers sets wrong time [\#91](https://github.com/pypeclub/OpenPype/issues/91)
|
||||
- Use instance frame start instead of timeline [\#1499](https://github.com/pypeclub/OpenPype/pull/1499) ([mkolar](https://github.com/mkolar))
|
||||
- Various smaller fixes [\#1498](https://github.com/pypeclub/OpenPype/pull/1498) ([mkolar](https://github.com/mkolar))
|
||||
- nuke: space in node name breaking process [\#1495](https://github.com/pypeclub/OpenPype/pull/1495) ([jezscha](https://github.com/jezscha))
|
||||
- Codec determination in extract burnin [\#1492](https://github.com/pypeclub/OpenPype/pull/1492) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Undefined constant in subprocess module [\#1485](https://github.com/pypeclub/OpenPype/pull/1485) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- List entity catch add/remove item changes properly [\#1482](https://github.com/pypeclub/OpenPype/pull/1482) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Resolve: additional fixes of publishing workflow [\#1481](https://github.com/pypeclub/OpenPype/pull/1481) ([jezscha](https://github.com/jezscha))
|
||||
- Photoshop: validation for already created images [\#1436](https://github.com/pypeclub/OpenPype/pull/1436) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Maya: Support for looks on VRay Proxies [\#1443](https://github.com/pypeclub/OpenPype/pull/1443) ([antirotor](https://github.com/antirotor))
|
||||
|
||||
## [2.17.3](https://github.com/pypeclub/openpype/tree/2.17.3) (2021-05-06)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.3...2.17.3)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Nuke: workfile version synced to db version always [\#1479](https://github.com/pypeclub/OpenPype/pull/1479) ([jezscha](https://github.com/jezscha))
|
||||
|
||||
## [CI/3.0.0-rc.3](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.3) (2021-05-05)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.2...CI/3.0.0-rc.3)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Path entity with placeholder [\#1473](https://github.com/pypeclub/OpenPype/pull/1473) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Burnin custom font filepath [\#1472](https://github.com/pypeclub/OpenPype/pull/1472) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Poetry: Move to OpenPype [\#1449](https://github.com/pypeclub/OpenPype/pull/1449) ([antirotor](https://github.com/antirotor))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Mac SSL path needs to be relative to pype\_root [\#1469](https://github.com/pypeclub/OpenPype/issues/1469)
|
||||
- Resolve: fix loading clips to timeline [\#1421](https://github.com/pypeclub/OpenPype/issues/1421)
|
||||
- Wrong handling of slashes when loading on mac [\#1411](https://github.com/pypeclub/OpenPype/issues/1411)
|
||||
- Nuke openpype3 [\#1342](https://github.com/pypeclub/OpenPype/issues/1342)
|
||||
- Houdini launcher [\#1171](https://github.com/pypeclub/OpenPype/issues/1171)
|
||||
- Fix SyncServer get\_enabled\_projects should handle global state [\#1475](https://github.com/pypeclub/OpenPype/pull/1475) ([kalisp](https://github.com/kalisp))
|
||||
- Igniter buttons enable/disable fix [\#1474](https://github.com/pypeclub/OpenPype/pull/1474) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Mac SSL path needs to be relative to pype\_root [\#1470](https://github.com/pypeclub/OpenPype/pull/1470) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Resolve: 17 compatibility issues and load image sequences [\#1422](https://github.com/pypeclub/OpenPype/pull/1422) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
|
||||
## [CI/3.0.0-rc.2](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.2) (2021-05-04)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.2...CI/3.0.0-rc.2)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Extract burnins with sequences [\#1467](https://github.com/pypeclub/OpenPype/pull/1467) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Extract burnins with color setting [\#1466](https://github.com/pypeclub/OpenPype/pull/1466) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix groups check in Python 2 [\#1468](https://github.com/pypeclub/OpenPype/pull/1468) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
|
||||
## [2.17.2](https://github.com/pypeclub/openpype/tree/2.17.2) (2021-05-04)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.1...2.17.2)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Forward/Backward compatible apps and tools with OpenPype 3 [\#1463](https://github.com/pypeclub/OpenPype/pull/1463) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
|
||||
## [CI/3.0.0-rc.1](https://github.com/pypeclub/openpype/tree/CI/3.0.0-rc.1) (2021-05-04)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.1...CI/3.0.0-rc.1)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Only show studio settings to admins [\#1406](https://github.com/pypeclub/OpenPype/issues/1406)
|
||||
- Ftrack specific settings save warning messages [\#1458](https://github.com/pypeclub/OpenPype/pull/1458) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Faster settings actions [\#1446](https://github.com/pypeclub/OpenPype/pull/1446) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Feature/sync server priority [\#1444](https://github.com/pypeclub/OpenPype/pull/1444) ([kalisp](https://github.com/kalisp))
|
||||
- Faster settings UI loading [\#1442](https://github.com/pypeclub/OpenPype/pull/1442) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Igniter re-write [\#1441](https://github.com/pypeclub/OpenPype/pull/1441) ([mkolar](https://github.com/mkolar))
|
||||
- Wrap openpype build into installers [\#1419](https://github.com/pypeclub/OpenPype/pull/1419) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Extract review first documentation [\#1404](https://github.com/pypeclub/OpenPype/pull/1404) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Blender PySide2 install guide [\#1403](https://github.com/pypeclub/OpenPype/pull/1403) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Nuke: deadline submission with gpu [\#1394](https://github.com/pypeclub/OpenPype/pull/1394) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Igniter: Reverse item filter for OpenPype version [\#1349](https://github.com/pypeclub/OpenPype/pull/1349) ([antirotor](https://github.com/antirotor))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- OpenPype Mongo URL definition [\#1450](https://github.com/pypeclub/OpenPype/issues/1450)
|
||||
- Various typos and smaller fixes [\#1464](https://github.com/pypeclub/OpenPype/pull/1464) ([mkolar](https://github.com/mkolar))
|
||||
- Validation of dynamic items in settings [\#1462](https://github.com/pypeclub/OpenPype/pull/1462) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- List can handle new items correctly [\#1459](https://github.com/pypeclub/OpenPype/pull/1459) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Settings actions process fix [\#1457](https://github.com/pypeclub/OpenPype/pull/1457) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Add to overrides actions fix [\#1456](https://github.com/pypeclub/OpenPype/pull/1456) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- OpenPype Mongo URL definition [\#1455](https://github.com/pypeclub/OpenPype/pull/1455) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Global settings save/load out of system settings [\#1447](https://github.com/pypeclub/OpenPype/pull/1447) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Keep metadata on remove overrides [\#1445](https://github.com/pypeclub/OpenPype/pull/1445) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Nuke: fixing undo for loaded mov and sequence [\#1432](https://github.com/pypeclub/OpenPype/pull/1432) ([jezscha](https://github.com/jezscha))
|
||||
- ExtractReview skip empty strings from settings [\#1431](https://github.com/pypeclub/OpenPype/pull/1431) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Bugfix Sync server tweaks [\#1430](https://github.com/pypeclub/OpenPype/pull/1430) ([kalisp](https://github.com/kalisp))
|
||||
- Hiero: missing thumbnail in review [\#1429](https://github.com/pypeclub/OpenPype/pull/1429) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Bugfix Maya in deadline for OpenPype [\#1428](https://github.com/pypeclub/OpenPype/pull/1428) ([kalisp](https://github.com/kalisp))
|
||||
- AE - validation for duration was 1 frame shorter [\#1427](https://github.com/pypeclub/OpenPype/pull/1427) ([kalisp](https://github.com/kalisp))
|
||||
- Houdini menu filename [\#1418](https://github.com/pypeclub/OpenPype/pull/1418) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Fix Avalon plugins attribute overrides [\#1413](https://github.com/pypeclub/OpenPype/pull/1413) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Nuke: submit to Deadline fails [\#1409](https://github.com/pypeclub/OpenPype/pull/1409) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- Validate MongoDB Url on start [\#1407](https://github.com/pypeclub/OpenPype/pull/1407) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Nuke: fix set colorspace with new settings [\#1386](https://github.com/pypeclub/OpenPype/pull/1386) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- MacOs build and install issues [\#1380](https://github.com/pypeclub/OpenPype/pull/1380) ([mkolar](https://github.com/mkolar))
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- test [\#1452](https://github.com/pypeclub/OpenPype/issues/1452)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- TVPaint frame range definition [\#1425](https://github.com/pypeclub/OpenPype/pull/1425) ([iLLiCiTiT](https://github.com/iLLiCiTiT))
|
||||
- Only show studio settings to admins [\#1420](https://github.com/pypeclub/OpenPype/pull/1420) ([create-issue-branch[bot]](https://github.com/apps/create-issue-branch))
|
||||
- TVPaint documentation [\#1305](https://github.com/pypeclub/OpenPype/pull/1305) ([64qam](https://github.com/64qam))
|
||||
|
||||
## [2.17.1](https://github.com/pypeclub/openpype/tree/2.17.1) (2021-04-30)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.0...2.17.1)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Nuke: deadline submission with gpu [\#1414](https://github.com/pypeclub/OpenPype/pull/1414)
|
||||
- TVPaint frame range definition [\#1424](https://github.com/pypeclub/OpenPype/pull/1424)
|
||||
- PS - group all published instances [\#1415](https://github.com/pypeclub/OpenPype/pull/1415)
|
||||
- Add task name to context pop up. [\#1383](https://github.com/pypeclub/OpenPype/pull/1383)
|
||||
- Enhance review letterbox feature. [\#1371](https://github.com/pypeclub/OpenPype/pull/1371)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Houdini menu filename [\#1417](https://github.com/pypeclub/OpenPype/pull/1417)
|
||||
- AE - validation for duration was 1 frame shorter [\#1426](https://github.com/pypeclub/OpenPype/pull/1426)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Maya: Vray - problem getting all file nodes for look publishing [\#1399](https://github.com/pypeclub/OpenPype/pull/1399)
|
||||
- Maya: Support for Redshift proxies [\#1360](https://github.com/pypeclub/OpenPype/pull/1360)
|
||||
|
||||
## [2.17.0](https://github.com/pypeclub/openpype/tree/2.17.0) (2021-04-20)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-beta.2...2.17.0)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Forward compatible ftrack group [\#1243](https://github.com/pypeclub/OpenPype/pull/1243)
|
||||
- Settings in mongo as dict [\#1221](https://github.com/pypeclub/OpenPype/pull/1221)
|
||||
- Maya: Make tx option configurable with presets [\#1328](https://github.com/pypeclub/OpenPype/pull/1328)
|
||||
- TVPaint asset name validation [\#1302](https://github.com/pypeclub/OpenPype/pull/1302)
|
||||
- TV Paint: Set initial project settings. [\#1299](https://github.com/pypeclub/OpenPype/pull/1299)
|
||||
- TV Paint: Validate mark in and out. [\#1298](https://github.com/pypeclub/OpenPype/pull/1298)
|
||||
- Validate project settings [\#1297](https://github.com/pypeclub/OpenPype/pull/1297)
|
||||
- After Effects: added SubsetManager [\#1234](https://github.com/pypeclub/OpenPype/pull/1234)
|
||||
- Show error message in pyblish UI [\#1206](https://github.com/pypeclub/OpenPype/pull/1206)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Hiero: fixing source frame from correct object [\#1362](https://github.com/pypeclub/OpenPype/pull/1362)
|
||||
- Nuke: fix colourspace, prerenders and nuke panes opening [\#1308](https://github.com/pypeclub/OpenPype/pull/1308)
|
||||
- AE remove orphaned instance from workfile - fix self.stub [\#1282](https://github.com/pypeclub/OpenPype/pull/1282)
|
||||
- Nuke: deadline submission with search replaced env values from preset [\#1194](https://github.com/pypeclub/OpenPype/pull/1194)
|
||||
- Ftrack custom attributes in bulks [\#1312](https://github.com/pypeclub/OpenPype/pull/1312)
|
||||
- Ftrack optional pypclub role [\#1303](https://github.com/pypeclub/OpenPype/pull/1303)
|
||||
- After Effects: remove orphaned instances [\#1275](https://github.com/pypeclub/OpenPype/pull/1275)
|
||||
- Avalon schema names [\#1242](https://github.com/pypeclub/OpenPype/pull/1242)
|
||||
- Handle duplication of Task name [\#1226](https://github.com/pypeclub/OpenPype/pull/1226)
|
||||
- Modified path of plugin loads for Harmony and TVPaint [\#1217](https://github.com/pypeclub/OpenPype/pull/1217)
|
||||
- Regex checks in profiles filtering [\#1214](https://github.com/pypeclub/OpenPype/pull/1214)
|
||||
- Bulk mov strict task [\#1204](https://github.com/pypeclub/OpenPype/pull/1204)
|
||||
- Update custom ftrack session attributes [\#1202](https://github.com/pypeclub/OpenPype/pull/1202)
|
||||
- Nuke: write node colorspace ignore `default\(\)` label [\#1199](https://github.com/pypeclub/OpenPype/pull/1199)
|
||||
- Nuke: reverse search to make it more versatile [\#1178](https://github.com/pypeclub/OpenPype/pull/1178)
|
||||
|
||||
|
||||
|
||||
## [2.16.0](https://github.com/pypeclub/pype/tree/2.16.0) (2021-03-22)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.3...2.16.0)
|
||||
|
|
@ -1057,4 +1568,7 @@ A large cleanup release. Most of the change are under the hood.
|
|||
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
||||
|
||||
|
||||
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
||||
|
||||
|
||||
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
||||
|
|
|
|||
15
README.md
15
README.md
|
|
@ -2,7 +2,7 @@
|
|||
OpenPype
|
||||
====
|
||||
|
||||
[](https://github.com/pypeclub/pype/actions/workflows/documentation.yml)  
|
||||
[](https://github.com/pypeclub/pype/actions/workflows/documentation.yml) 
|
||||
|
||||
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ The main things you will need to run and build OpenPype are:
|
|||
- PowerShell 5.0+ (Windows)
|
||||
- Bash (Linux)
|
||||
- [**Python 3.7.8**](#python) or higher
|
||||
- [**MongoDB**](#database)
|
||||
- [**MongoDB**](#database) (needed only for local development)
|
||||
|
||||
|
||||
It can be built and ran on all common platforms. We develop and test on the following:
|
||||
|
|
@ -126,6 +126,16 @@ pyenv local 3.7.9
|
|||
|
||||
### Linux
|
||||
|
||||
#### Docker
|
||||
Easiest way to build OpenPype on Linux is using [Docker](https://www.docker.com/). Just run:
|
||||
|
||||
```sh
|
||||
sudo ./tools/docker_build.sh
|
||||
```
|
||||
|
||||
If all is successful, you'll find built OpenPype in `./build/` folder.
|
||||
|
||||
#### Manual build
|
||||
You will need [Python 3.7](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads). You'll also need [curl](https://curl.se) on systems that doesn't have one preinstalled.
|
||||
|
||||
To build Python related stuff, you need Python header files installed (`python3-dev` on Ubuntu for example).
|
||||
|
|
@ -133,7 +143,6 @@ To build Python related stuff, you need Python header files installed (`python3-
|
|||
You'll need also other tools to build
|
||||
some OpenPype dependencies like [CMake](https://cmake.org/). Python 3 should be part of all modern distributions. You can use your package manager to install **git** and **cmake**.
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Details for Ubuntu</summary>
|
||||
Install git, cmake and curl
|
||||
|
|
|
|||
93
igniter/Poppins/OFL.txt
Normal file
93
igniter/Poppins/OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
igniter/Poppins/Poppins-Black.ttf
Normal file
BIN
igniter/Poppins/Poppins-Black.ttf
Normal file
Binary file not shown.
BIN
igniter/Poppins/Poppins-BlackItalic.ttf
Normal file
BIN
igniter/Poppins/Poppins-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
igniter/Poppins/Poppins-Bold.ttf
Normal file
BIN
igniter/Poppins/Poppins-Bold.ttf
Normal file
Binary file not shown.
BIN
igniter/Poppins/Poppins-BoldItalic.ttf
Normal file
BIN
igniter/Poppins/Poppins-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
igniter/Poppins/Poppins-ExtraBold.ttf
Normal file
BIN
igniter/Poppins/Poppins-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
igniter/Poppins/Poppins-ExtraBoldItalic.ttf
Normal file
BIN
igniter/Poppins/Poppins-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
igniter/Poppins/Poppins-ExtraLight.ttf
Normal file
BIN
igniter/Poppins/Poppins-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
igniter/Poppins/Poppins-ExtraLightItalic.ttf
Normal file
BIN
igniter/Poppins/Poppins-ExtraLightItalic.ttf
Normal file
Binary file not shown.
BIN
igniter/Poppins/Poppins-Italic.ttf
Normal file
BIN
igniter/Poppins/Poppins-Italic.ttf
Normal file
Binary file not shown.
BIN
igniter/Poppins/Poppins-Light.ttf
Normal file
BIN
igniter/Poppins/Poppins-Light.ttf
Normal file
Binary file not shown.
BIN
igniter/Poppins/Poppins-LightItalic.ttf
Normal file
BIN
igniter/Poppins/Poppins-LightItalic.ttf
Normal file
Binary file not shown.
BIN
igniter/Poppins/Poppins-Medium.ttf
Normal file
BIN
igniter/Poppins/Poppins-Medium.ttf
Normal file
Binary file not shown.
BIN
igniter/Poppins/Poppins-MediumItalic.ttf
Normal file
BIN
igniter/Poppins/Poppins-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
igniter/Poppins/Poppins-Regular.ttf
Normal file
BIN
igniter/Poppins/Poppins-Regular.ttf
Normal file
Binary file not shown.
BIN
igniter/Poppins/Poppins-SemiBold.ttf
Normal file
BIN
igniter/Poppins/Poppins-SemiBold.ttf
Normal file
Binary file not shown.
BIN
igniter/Poppins/Poppins-SemiBoldItalic.ttf
Normal file
BIN
igniter/Poppins/Poppins-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
igniter/Poppins/Poppins-Thin.ttf
Normal file
BIN
igniter/Poppins/Poppins-Thin.ttf
Normal file
Binary file not shown.
BIN
igniter/Poppins/Poppins-ThinItalic.ttf
Normal file
BIN
igniter/Poppins/Poppins-ThinItalic.ttf
Normal file
Binary file not shown.
|
|
@ -10,33 +10,52 @@ from .bootstrap_repos import BootstrapRepos
|
|||
from .version import __version__ as version
|
||||
|
||||
|
||||
RESULT = 0
|
||||
|
||||
|
||||
def get_result(res: int):
|
||||
"""Sets result returned from dialog."""
|
||||
global RESULT
|
||||
RESULT = res
|
||||
|
||||
|
||||
def open_dialog():
|
||||
"""Show Igniter dialog."""
|
||||
from Qt import QtWidgets
|
||||
if os.getenv("OPENPYPE_HEADLESS_MODE"):
|
||||
print("!!! Can't open dialog in headless mode. Exiting.")
|
||||
sys.exit(1)
|
||||
from Qt import QtWidgets, QtCore
|
||||
from .install_dialog import InstallDialog
|
||||
|
||||
scale_attr = getattr(QtCore.Qt, "AA_EnableHighDpiScaling", None)
|
||||
if scale_attr is not None:
|
||||
QtWidgets.QApplication.setAttribute(scale_attr)
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
|
||||
d = InstallDialog()
|
||||
d.finished.connect(get_result)
|
||||
d.open()
|
||||
|
||||
app.exec()
|
||||
app.exec_()
|
||||
return d.result()
|
||||
|
||||
return RESULT
|
||||
|
||||
def open_update_window(openpype_version):
|
||||
"""Open update window."""
|
||||
if os.getenv("OPENPYPE_HEADLESS_MODE"):
|
||||
print("!!! Can't open dialog in headless mode. Exiting.")
|
||||
sys.exit(1)
|
||||
from Qt import QtWidgets, QtCore
|
||||
from .update_window import UpdateWindow
|
||||
|
||||
scale_attr = getattr(QtCore.Qt, "AA_EnableHighDpiScaling", None)
|
||||
if scale_attr is not None:
|
||||
QtWidgets.QApplication.setAttribute(scale_attr)
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
|
||||
d = UpdateWindow(version=openpype_version)
|
||||
d.open()
|
||||
|
||||
app.exec_()
|
||||
version_path = d.get_version_path()
|
||||
return version_path
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BootstrapRepos",
|
||||
"open_dialog",
|
||||
"open_update_window",
|
||||
"version"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Bootstrap OpenPype repositories."""
|
||||
import functools
|
||||
from __future__ import annotations
|
||||
import logging as log
|
||||
import os
|
||||
import re
|
||||
|
|
@ -9,10 +9,13 @@ import sys
|
|||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Union, Callable, List, Tuple
|
||||
import hashlib
|
||||
|
||||
from zipfile import ZipFile, BadZipFile
|
||||
|
||||
from appdirs import user_data_dir
|
||||
from speedcopy import copyfile
|
||||
import semver
|
||||
|
||||
from .user_settings import (
|
||||
OpenPypeSecureRegistry,
|
||||
|
|
@ -26,159 +29,157 @@ LOG_WARNING = 1
|
|||
LOG_ERROR = 3
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class OpenPypeVersion:
|
||||
def sha256sum(filename):
|
||||
"""Calculate sha256 for content of the file.
|
||||
|
||||
Args:
|
||||
filename (str): Path to file.
|
||||
|
||||
Returns:
|
||||
str: hex encoded sha256
|
||||
|
||||
"""
|
||||
h = hashlib.sha256()
|
||||
b = bytearray(128 * 1024)
|
||||
mv = memoryview(b)
|
||||
with open(filename, 'rb', buffering=0) as f:
|
||||
for n in iter(lambda: f.readinto(mv), 0):
|
||||
h.update(mv[:n])
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
class OpenPypeVersion(semver.VersionInfo):
|
||||
"""Class for storing information about OpenPype version.
|
||||
|
||||
Attributes:
|
||||
major (int): [1].2.3-client-variant
|
||||
minor (int): 1.[2].3-client-variant
|
||||
subversion (int): 1.2.[3]-client-variant
|
||||
client (str): 1.2.3-[client]-variant
|
||||
variant (str): 1.2.3-client-[variant]
|
||||
staging (bool): True if it is staging version
|
||||
path (str): path to OpenPype
|
||||
|
||||
"""
|
||||
major = 0
|
||||
minor = 0
|
||||
subversion = 0
|
||||
variant = ""
|
||||
client = None
|
||||
staging = False
|
||||
path = None
|
||||
_VERSION_REGEX = re.compile(r"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$") # noqa: E501
|
||||
|
||||
_version_regex = re.compile(
|
||||
r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<sub>\d+)(-(?P<var1>staging)|-(?P<client>.+)(-(?P<var2>staging)))?") # noqa: E501
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Create OpenPype version.
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
"""return formatted version string."""
|
||||
return self._compose_version()
|
||||
.. deprecated:: 3.0.0-rc.2
|
||||
`client` and `variant` are removed.
|
||||
|
||||
@version.setter
|
||||
def version(self, val):
|
||||
decomposed = self._decompose_version(val)
|
||||
self.major = decomposed[0]
|
||||
self.minor = decomposed[1]
|
||||
self.subversion = decomposed[2]
|
||||
self.variant = decomposed[3]
|
||||
self.client = decomposed[4]
|
||||
|
||||
def __init__(self, major: int = None, minor: int = None,
|
||||
subversion: int = None, version: str = None,
|
||||
variant: str = "", client: str = None,
|
||||
path: Path = None):
|
||||
self.path = path
|
||||
Args:
|
||||
major (int): version when you make incompatible API changes.
|
||||
minor (int): version when you add functionality in a
|
||||
backwards-compatible manner.
|
||||
patch (int): version when you make backwards-compatible bug fixes.
|
||||
prerelease (str): an optional prerelease string
|
||||
build (str): an optional build string
|
||||
version (str): if set, it will be parsed and will override
|
||||
parameters like `major`, `minor` and so on.
|
||||
staging (bool): set to True if version is staging.
|
||||
path (Path): path to version location.
|
||||
|
||||
if (
|
||||
major is None or minor is None or subversion is None
|
||||
) and version is None:
|
||||
raise ValueError("Need version specified in some way.")
|
||||
if version:
|
||||
values = self._decompose_version(version)
|
||||
self.major = values[0]
|
||||
self.minor = values[1]
|
||||
self.subversion = values[2]
|
||||
self.variant = values[3]
|
||||
self.client = values[4]
|
||||
else:
|
||||
self.major = major
|
||||
self.minor = minor
|
||||
self.subversion = subversion
|
||||
# variant is set only if it is "staging", otherwise "production" is
|
||||
# implied and no need to mention it in version string.
|
||||
if variant == "staging":
|
||||
self.variant = variant
|
||||
self.client = client
|
||||
"""
|
||||
self.path = None
|
||||
self.staging = False
|
||||
|
||||
def _compose_version(self):
|
||||
version = "{}.{}.{}".format(self.major, self.minor, self.subversion)
|
||||
if "version" in kwargs.keys():
|
||||
if not kwargs.get("version"):
|
||||
raise ValueError("Invalid version specified")
|
||||
v = OpenPypeVersion.parse(kwargs.get("version"))
|
||||
kwargs["major"] = v.major
|
||||
kwargs["minor"] = v.minor
|
||||
kwargs["patch"] = v.patch
|
||||
kwargs["prerelease"] = v.prerelease
|
||||
kwargs["build"] = v.build
|
||||
kwargs.pop("version")
|
||||
|
||||
if self.client:
|
||||
version = "{}-{}".format(version, self.client)
|
||||
if kwargs.get("path"):
|
||||
if isinstance(kwargs.get("path"), str):
|
||||
self.path = Path(kwargs.get("path"))
|
||||
elif isinstance(kwargs.get("path"), Path):
|
||||
self.path = kwargs.get("path")
|
||||
else:
|
||||
raise TypeError("Path must be str or Path")
|
||||
kwargs.pop("path")
|
||||
|
||||
if self.variant == "staging":
|
||||
version = "{}-{}".format(version, self.variant)
|
||||
if "path" in kwargs.keys():
|
||||
kwargs.pop("path")
|
||||
|
||||
return version
|
||||
if kwargs.get("staging"):
|
||||
self.staging = kwargs.get("staging", False)
|
||||
kwargs.pop("staging")
|
||||
|
||||
@classmethod
|
||||
def _decompose_version(cls, version_string: str) -> tuple:
|
||||
m = re.search(cls._version_regex, version_string)
|
||||
if not m:
|
||||
raise ValueError(
|
||||
"Cannot parse version string: {}".format(version_string))
|
||||
if "staging" in kwargs.keys():
|
||||
kwargs.pop("staging")
|
||||
|
||||
variant = None
|
||||
if m.group("var1") == "staging" or m.group("var2") == "staging":
|
||||
variant = "staging"
|
||||
if self.staging:
|
||||
if kwargs.get("build"):
|
||||
if "staging" not in kwargs.get("build"):
|
||||
kwargs["build"] = "{}-staging".format(kwargs.get("build"))
|
||||
else:
|
||||
kwargs["build"] = "staging"
|
||||
|
||||
client = m.group("client")
|
||||
if kwargs.get("build") and "staging" in kwargs.get("build", ""):
|
||||
self.staging = True
|
||||
|
||||
return (int(m.group("major")), int(m.group("minor")),
|
||||
int(m.group("sub")), variant, client)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
return self.version == other.version
|
||||
|
||||
def __str__(self):
|
||||
return self.version
|
||||
result = super().__eq__(other)
|
||||
return bool(result and self.staging == other.staging)
|
||||
|
||||
def __repr__(self):
|
||||
return "{}, {}: {}".format(
|
||||
self.__class__.__name__, self.version, self.path)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.version)
|
||||
|
||||
def __lt__(self, other):
|
||||
if (self.major, self.minor, self.subversion) < \
|
||||
(other.major, other.minor, other.subversion):
|
||||
return True
|
||||
|
||||
# 1.2.3-staging < 1.2.3-client-staging
|
||||
if self.get_main_version() == other.get_main_version() and \
|
||||
not self.client and self.variant and \
|
||||
other.client and other.variant:
|
||||
return True
|
||||
|
||||
# 1.2.3 < 1.2.3-staging
|
||||
if self.get_main_version() == other.get_main_version() and \
|
||||
not self.client and self.variant and \
|
||||
not other.client and not other.variant:
|
||||
return True
|
||||
|
||||
# 1.2.3 < 1.2.3-client
|
||||
if self.get_main_version() == other.get_main_version() and \
|
||||
not self.client and not self.variant and \
|
||||
other.client and not other.variant:
|
||||
return True
|
||||
|
||||
# 1.2.3 < 1.2.3-client-staging
|
||||
if self.get_main_version() == other.get_main_version() and \
|
||||
not self.client and not self.variant and other.client:
|
||||
return True
|
||||
|
||||
# 1.2.3-client-staging < 1.2.3-client
|
||||
if self.get_main_version() == other.get_main_version() and \
|
||||
self.client and self.variant and \
|
||||
other.client and not other.variant:
|
||||
return True
|
||||
return "<{}: {} - path={}>".format(
|
||||
self.__class__.__name__, str(self), self.path)
|
||||
|
||||
def __lt__(self, other: OpenPypeVersion):
|
||||
result = super().__lt__(other)
|
||||
# prefer path over no path
|
||||
if self.version == other.version and \
|
||||
not self.path and other.path:
|
||||
if self == other and not self.path and other.path:
|
||||
return True
|
||||
|
||||
# prefer path with dir over path with file
|
||||
return self.version == other.version and self.path and \
|
||||
other.path and self.path.is_file() and \
|
||||
other.path.is_dir()
|
||||
if self == other and self.path and other.path and \
|
||||
other.path.is_dir() and self.path.is_file():
|
||||
return True
|
||||
|
||||
if self.finalize_version() == other.finalize_version() and \
|
||||
self.prerelease == other.prerelease and \
|
||||
self.is_staging() and not other.is_staging():
|
||||
return True
|
||||
|
||||
return result
|
||||
|
||||
def set_staging(self) -> OpenPypeVersion:
|
||||
"""Set version as staging and return it.
|
||||
|
||||
This will preserve current one.
|
||||
|
||||
Returns:
|
||||
OpenPypeVersion: Set as staging.
|
||||
|
||||
"""
|
||||
if self.staging:
|
||||
return self
|
||||
return self.replace(parts={"build": f"{self.build}-staging"})
|
||||
|
||||
def set_production(self) -> OpenPypeVersion:
|
||||
"""Set version as production and return it.
|
||||
|
||||
This will preserve current one.
|
||||
|
||||
Returns:
|
||||
OpenPypeVersion: Set as production.
|
||||
|
||||
"""
|
||||
if not self.staging:
|
||||
return self
|
||||
return self.replace(
|
||||
parts={"build": self.build.replace("-staging", "")})
|
||||
|
||||
def is_staging(self) -> bool:
|
||||
"""Test if current version is staging one."""
|
||||
return self.variant == "staging"
|
||||
return self.staging
|
||||
|
||||
def get_main_version(self) -> str:
|
||||
"""Return main version component.
|
||||
|
|
@ -186,11 +187,13 @@ class OpenPypeVersion:
|
|||
This returns x.x.x part of version from possibly more complex one
|
||||
like x.x.x-foo-bar.
|
||||
|
||||
.. deprecated:: 3.0.0-rc.2
|
||||
use `finalize_version()` instead.
|
||||
Returns:
|
||||
str: main version component
|
||||
|
||||
"""
|
||||
return "{}.{}.{}".format(self.major, self.minor, self.subversion)
|
||||
return str(self.finalize_version())
|
||||
|
||||
@staticmethod
|
||||
def version_in_str(string: str) -> Tuple:
|
||||
|
|
@ -203,15 +206,28 @@ class OpenPypeVersion:
|
|||
tuple: True/False and OpenPypeVersion if found.
|
||||
|
||||
"""
|
||||
try:
|
||||
result = OpenPypeVersion._decompose_version(string)
|
||||
except ValueError:
|
||||
m = re.search(OpenPypeVersion._VERSION_REGEX, string)
|
||||
if not m:
|
||||
return False, None
|
||||
return True, OpenPypeVersion(major=result[0],
|
||||
minor=result[1],
|
||||
subversion=result[2],
|
||||
variant=result[3],
|
||||
client=result[4])
|
||||
version = OpenPypeVersion.parse(string[m.start():m.end()])
|
||||
return True, version
|
||||
|
||||
@classmethod
|
||||
def parse(cls, version):
|
||||
"""Extends parse to handle ta handle staging variant."""
|
||||
v = super().parse(version)
|
||||
openpype_version = cls(major=v.major, minor=v.minor,
|
||||
patch=v.patch, prerelease=v.prerelease,
|
||||
build=v.build)
|
||||
if v.build and "staging" in v.build:
|
||||
openpype_version.staging = True
|
||||
return openpype_version
|
||||
|
||||
def __hash__(self):
|
||||
if self.path:
|
||||
return hash(self.path)
|
||||
else:
|
||||
return hash(str(self))
|
||||
|
||||
|
||||
class BootstrapRepos:
|
||||
|
|
@ -223,7 +239,7 @@ class BootstrapRepos:
|
|||
otherwise `None`.
|
||||
registry (OpenPypeSettingsRegistry): OpenPype registry object.
|
||||
zip_filter (list): List of files to exclude from zip
|
||||
openpype_filter (list): list of top level directories not to
|
||||
openpype_filter (list): list of top level directories to
|
||||
include in zip in OpenPype repository.
|
||||
|
||||
"""
|
||||
|
|
@ -246,7 +262,7 @@ class BootstrapRepos:
|
|||
self.registry = OpenPypeSettingsRegistry()
|
||||
self.zip_filter = [".pyc", "__pycache__"]
|
||||
self.openpype_filter = [
|
||||
"build", "docs", "tests", "tools", "venv", "coverage"
|
||||
"openpype", "repos", "schema", "LICENSE"
|
||||
]
|
||||
self._message = message
|
||||
|
||||
|
|
@ -265,11 +281,12 @@ class BootstrapRepos:
|
|||
self.live_repo_dir = Path(Path(__file__).parent / ".." / "repos")
|
||||
|
||||
@staticmethod
|
||||
def get_version_path_from_list(version: str, version_list: list) -> Path:
|
||||
def get_version_path_from_list(
|
||||
version: str, version_list: list) -> Union[Path, None]:
|
||||
"""Get path for specific version in list of OpenPype versions.
|
||||
|
||||
Args:
|
||||
version (str): Version string to look for (1.2.4-staging)
|
||||
version (str): Version string to look for (1.2.4+staging)
|
||||
version_list (list of OpenPypeVersion): list of version to search.
|
||||
|
||||
Returns:
|
||||
|
|
@ -279,13 +296,14 @@ class BootstrapRepos:
|
|||
for v in version_list:
|
||||
if str(v) == version:
|
||||
return v.path
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_local_live_version() -> str:
|
||||
"""Get version of local OpenPype."""
|
||||
|
||||
version = {}
|
||||
path = Path(os.path.dirname(__file__)).parent / "openpype" / "version.py"
|
||||
path = Path(os.environ["OPENPYPE_ROOT"]) / "openpype" / "version.py"
|
||||
with open(path, "r") as fp:
|
||||
exec(fp.read(), version)
|
||||
return version["__version__"]
|
||||
|
|
@ -423,18 +441,13 @@ class BootstrapRepos:
|
|||
"""
|
||||
frozen_root = Path(sys.executable).parent
|
||||
|
||||
# from frozen code we need igniter, openpype, schema vendor
|
||||
openpype_list = self._filter_dir(
|
||||
frozen_root / "openpype", self.zip_filter)
|
||||
openpype_list += self._filter_dir(
|
||||
frozen_root / "igniter", self.zip_filter)
|
||||
openpype_list += self._filter_dir(
|
||||
frozen_root / "repos", self.zip_filter)
|
||||
openpype_list += self._filter_dir(
|
||||
frozen_root / "schema", self.zip_filter)
|
||||
openpype_list += self._filter_dir(
|
||||
frozen_root / "vendor", self.zip_filter)
|
||||
openpype_list.append(frozen_root / "LICENSE")
|
||||
openpype_list = []
|
||||
for f in self.openpype_filter:
|
||||
if (frozen_root / f).is_dir():
|
||||
openpype_list += self._filter_dir(
|
||||
frozen_root / f, self.zip_filter)
|
||||
else:
|
||||
openpype_list.append(frozen_root / f)
|
||||
|
||||
version = self.get_version(frozen_root)
|
||||
|
||||
|
|
@ -477,11 +490,16 @@ class BootstrapRepos:
|
|||
openpype_path (Path): Path to OpenPype sources.
|
||||
|
||||
"""
|
||||
openpype_list = []
|
||||
openpype_inc = 0
|
||||
|
||||
# get filtered list of file in Pype repository
|
||||
openpype_list = self._filter_dir(openpype_path, self.zip_filter)
|
||||
# openpype_list = self._filter_dir(openpype_path, self.zip_filter)
|
||||
openpype_list = []
|
||||
for f in self.openpype_filter:
|
||||
if (openpype_path / f).is_dir():
|
||||
openpype_list += self._filter_dir(
|
||||
openpype_path / f, self.zip_filter)
|
||||
else:
|
||||
openpype_list.append(openpype_path / f)
|
||||
|
||||
openpype_files = len(openpype_list)
|
||||
|
||||
openpype_inc = 98.0 / float(openpype_files)
|
||||
|
|
@ -491,6 +509,7 @@ class BootstrapRepos:
|
|||
openpype_root = openpype_path.resolve()
|
||||
# generate list of filtered paths
|
||||
dir_filter = [openpype_root / f for f in self.openpype_filter]
|
||||
checksums = []
|
||||
|
||||
file: Path
|
||||
for file in openpype_list:
|
||||
|
|
@ -506,18 +525,125 @@ class BootstrapRepos:
|
|||
except ValueError:
|
||||
pass
|
||||
|
||||
if is_inside:
|
||||
if not is_inside:
|
||||
continue
|
||||
|
||||
processed_path = file
|
||||
self._print(f"- processing {processed_path}")
|
||||
|
||||
zip_file.write(file, file.relative_to(openpype_root))
|
||||
checksums.append(
|
||||
(
|
||||
sha256sum(file.as_posix()),
|
||||
file.resolve().relative_to(openpype_root)
|
||||
)
|
||||
)
|
||||
zip_file.write(
|
||||
file, file.resolve().relative_to(openpype_root))
|
||||
|
||||
checksums_str = ""
|
||||
for c in checksums:
|
||||
checksums_str += "{}:{}\n".format(c[0], c[1])
|
||||
zip_file.writestr("checksums", checksums_str)
|
||||
# test if zip is ok
|
||||
zip_file.testzip()
|
||||
self._progress_callback(100)
|
||||
|
||||
def validate_openpype_version(self, path: Path) -> tuple:
|
||||
"""Validate version directory or zip file.
|
||||
|
||||
This will load `checksums` file if present, calculate checksums
|
||||
of existing files in given path and compare. It will also compare
|
||||
lists of files together for missing files.
|
||||
|
||||
Args:
|
||||
path (Path): Path to OpenPype version to validate.
|
||||
|
||||
Returns:
|
||||
tuple(bool, str): with version validity as first item
|
||||
and string with reason as second.
|
||||
|
||||
"""
|
||||
if not path.exists():
|
||||
return False, "Path doesn't exist"
|
||||
|
||||
if path.is_file():
|
||||
return self._validate_zip(path)
|
||||
return self._validate_dir(path)
|
||||
|
||||
@staticmethod
|
||||
def _validate_zip(path: Path) -> tuple:
|
||||
"""Validate content of zip file."""
|
||||
with ZipFile(path, "r") as zip_file:
|
||||
# read checksums
|
||||
try:
|
||||
checksums_data = str(zip_file.read("checksums"))
|
||||
except IOError:
|
||||
# FIXME: This should be set to False sometimes in the future
|
||||
return True, "Cannot read checksums for archive."
|
||||
|
||||
# split it to the list of tuples
|
||||
checksums = [
|
||||
tuple(line.split(":"))
|
||||
for line in checksums_data.split("\n") if line
|
||||
]
|
||||
|
||||
# calculate and compare checksums in the zip file
|
||||
for file in checksums:
|
||||
h = hashlib.sha256()
|
||||
try:
|
||||
h.update(zip_file.read(file[1]))
|
||||
except FileNotFoundError:
|
||||
return False, f"Missing file [ {file[1]} ]"
|
||||
if h.hexdigest() != file[0]:
|
||||
return False, f"Invalid checksum on {file[1]}"
|
||||
|
||||
# get list of files in zip minus `checksums` file itself
|
||||
# and turn in to set to compare against list of files
|
||||
# from checksum file. If difference exists, something is
|
||||
# wrong
|
||||
files_in_zip = zip_file.namelist()
|
||||
files_in_zip.remove("checksums")
|
||||
files_in_zip = set(files_in_zip)
|
||||
files_in_checksum = set([file[1] for file in checksums])
|
||||
diff = files_in_zip.difference(files_in_checksum)
|
||||
if diff:
|
||||
return False, f"Missing files {diff}"
|
||||
|
||||
return True, "All ok"
|
||||
|
||||
@staticmethod
|
||||
def _validate_dir(path: Path) -> tuple:
|
||||
checksums_file = Path(path / "checksums")
|
||||
if not checksums_file.exists():
|
||||
# FIXME: This should be set to False sometimes in the future
|
||||
return True, "Cannot read checksums for archive."
|
||||
checksums_data = checksums_file.read_text()
|
||||
checksums = [
|
||||
tuple(line.split(":"))
|
||||
for line in checksums_data.split("\n") if line
|
||||
]
|
||||
files_in_dir = [
|
||||
file.relative_to(path).as_posix()
|
||||
for file in path.iterdir() if file.is_file()
|
||||
]
|
||||
files_in_dir.remove("checksums")
|
||||
files_in_dir = set(files_in_dir)
|
||||
files_in_checksum = set([file[1] for file in checksums])
|
||||
|
||||
for file in checksums:
|
||||
try:
|
||||
current = sha256sum((path / file[1]).as_posix())
|
||||
except FileNotFoundError:
|
||||
return False, f"Missing file [ {file[1]} ]"
|
||||
|
||||
if file[0] != current:
|
||||
return False, f"Invalid checksum on {file[1]}"
|
||||
diff = files_in_dir.difference(files_in_checksum)
|
||||
if diff:
|
||||
return False, f"Missing files {diff}"
|
||||
|
||||
return True, "All ok"
|
||||
|
||||
@staticmethod
|
||||
def add_paths_from_archive(archive: Path) -> None:
|
||||
"""Add first-level directory and 'repos' as paths to :mod:`sys.path`.
|
||||
|
|
@ -575,7 +701,7 @@ class BootstrapRepos:
|
|||
|
||||
"""
|
||||
sys.path.insert(0, directory.as_posix())
|
||||
directory = directory / "repos"
|
||||
directory /= "repos"
|
||||
if not directory.exists() and not directory.is_dir():
|
||||
raise ValueError("directory is invalid")
|
||||
|
||||
|
|
@ -632,7 +758,7 @@ class BootstrapRepos:
|
|||
" not implemented yet."))
|
||||
|
||||
dir_to_search = self.data_dir
|
||||
|
||||
user_versions = self.get_openpype_versions(self.data_dir, staging)
|
||||
# if we have openpype_path specified, search only there.
|
||||
if openpype_path:
|
||||
dir_to_search = openpype_path
|
||||
|
|
@ -652,6 +778,7 @@ class BootstrapRepos:
|
|||
pass
|
||||
|
||||
openpype_versions = self.get_openpype_versions(dir_to_search, staging)
|
||||
openpype_versions += user_versions
|
||||
|
||||
# remove zip file version if needed.
|
||||
if not include_zips:
|
||||
|
|
@ -659,6 +786,9 @@ class BootstrapRepos:
|
|||
v for v in openpype_versions if v.path.suffix != ".zip"
|
||||
]
|
||||
|
||||
# remove duplicates
|
||||
openpype_versions = sorted(list(set(openpype_versions)))
|
||||
|
||||
return openpype_versions
|
||||
|
||||
def process_entered_location(self, location: str) -> Union[Path, None]:
|
||||
|
|
@ -681,7 +811,7 @@ class BootstrapRepos:
|
|||
openpype_path = None
|
||||
# try to get OpenPype path from mongo.
|
||||
if location.startswith("mongodb"):
|
||||
pype_path = get_openpype_path_from_db(location)
|
||||
openpype_path = get_openpype_path_from_db(location)
|
||||
if not openpype_path:
|
||||
self._print("cannot find OPENPYPE_PATH in settings.")
|
||||
return None
|
||||
|
|
@ -764,12 +894,13 @@ class BootstrapRepos:
|
|||
|
||||
destination = self.data_dir / version.path.stem
|
||||
if destination.exists():
|
||||
assert destination.is_dir()
|
||||
try:
|
||||
destination.unlink()
|
||||
except OSError:
|
||||
shutil.rmtree(destination)
|
||||
except OSError as e:
|
||||
msg = f"!!! Cannot remove already existing {destination}"
|
||||
self._print(msg, LOG_ERROR, exc_info=True)
|
||||
return None
|
||||
raise e
|
||||
|
||||
destination.mkdir(parents=True)
|
||||
|
||||
|
|
@ -808,7 +939,7 @@ class BootstrapRepos:
|
|||
"""Install OpenPype version to user data directory.
|
||||
|
||||
Args:
|
||||
oepnpype_version (OpenPypeVersion): OpenPype version to install.
|
||||
openpype_version (OpenPypeVersion): OpenPype version to install.
|
||||
force (bool, optional): Force overwrite existing version.
|
||||
|
||||
Returns:
|
||||
|
|
@ -821,7 +952,6 @@ class BootstrapRepos:
|
|||
OpenPypeVersionIOError: If copying or zipping fail.
|
||||
|
||||
"""
|
||||
|
||||
if self.is_inside_user_data(openpype_version.path) and not openpype_version.path.is_file(): # noqa
|
||||
raise OpenPypeVersionExists(
|
||||
"OpenPype already inside user data dir")
|
||||
|
|
@ -837,6 +967,7 @@ class BootstrapRepos:
|
|||
|
||||
# test if destination directory already exist, if so lets delete it.
|
||||
if destination.exists() and force:
|
||||
self._print("removing existing directory")
|
||||
try:
|
||||
shutil.rmtree(destination)
|
||||
except OSError as e:
|
||||
|
|
@ -846,6 +977,7 @@ class BootstrapRepos:
|
|||
raise OpenPypeVersionIOError(
|
||||
f"cannot remove existing {destination}") from e
|
||||
elif destination.exists() and not force:
|
||||
self._print("destination directory already exists")
|
||||
raise OpenPypeVersionExists(f"{destination} already exist.")
|
||||
else:
|
||||
# create destination parent directories even if they don't exist.
|
||||
|
|
@ -855,6 +987,7 @@ class BootstrapRepos:
|
|||
if openpype_version.path.is_dir():
|
||||
# create zip inside temporary directory.
|
||||
self._print("Creating zip from directory ...")
|
||||
self._progress_callback(0)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_zip = \
|
||||
Path(temp_dir) / f"openpype-v{openpype_version}.zip"
|
||||
|
|
@ -868,34 +1001,48 @@ class BootstrapRepos:
|
|||
# set zip as version source
|
||||
openpype_version.path = temp_zip
|
||||
|
||||
if self.is_inside_user_data(openpype_version.path):
|
||||
raise OpenPypeVersionInvalid(
|
||||
"Version is in user data dir.")
|
||||
openpype_version.path = self._copy_zip(
|
||||
openpype_version.path, destination)
|
||||
|
||||
elif openpype_version.path.is_file():
|
||||
# check if file is zip (by extension)
|
||||
if openpype_version.path.suffix.lower() != ".zip":
|
||||
raise OpenPypeVersionInvalid("Invalid file format")
|
||||
|
||||
if not self.is_inside_user_data(openpype_version.path):
|
||||
try:
|
||||
# copy file to destination
|
||||
self._print("Copying zip to destination ...")
|
||||
_destination_zip = destination.parent / openpype_version.path.name # noqa: E501
|
||||
copyfile(
|
||||
openpype_version.path.as_posix(),
|
||||
_destination_zip.as_posix())
|
||||
except OSError as e:
|
||||
self._print(
|
||||
"cannot copy version to user data directory", LOG_ERROR,
|
||||
exc_info=True)
|
||||
raise OpenPypeVersionIOError((
|
||||
f"can't copy version {openpype_version.path.as_posix()} "
|
||||
f"to destination {destination.parent.as_posix()}")) from e
|
||||
if not self.is_inside_user_data(openpype_version.path):
|
||||
self._progress_callback(35)
|
||||
openpype_version.path = self._copy_zip(
|
||||
openpype_version.path, destination)
|
||||
|
||||
# extract zip there
|
||||
self._print("extracting zip to destination ...")
|
||||
with ZipFile(openpype_version.path, "r") as zip_ref:
|
||||
self._progress_callback(75)
|
||||
zip_ref.extractall(destination)
|
||||
self._progress_callback(100)
|
||||
|
||||
return destination
|
||||
|
||||
def _copy_zip(self, source: Path, destination: Path) -> Path:
|
||||
try:
|
||||
# copy file to destination
|
||||
self._print("Copying zip to destination ...")
|
||||
_destination_zip = destination.parent / source.name # noqa: E501
|
||||
copyfile(
|
||||
source.as_posix(),
|
||||
_destination_zip.as_posix())
|
||||
except OSError as e:
|
||||
self._print(
|
||||
"cannot copy version to user data directory", LOG_ERROR,
|
||||
exc_info=True)
|
||||
raise OpenPypeVersionIOError((
|
||||
f"can't copy version {source.as_posix()} "
|
||||
f"to destination {destination.parent.as_posix()}")) from e
|
||||
return _destination_zip
|
||||
|
||||
def _is_openpype_in_dir(self,
|
||||
dir_item: Path,
|
||||
detected_version: OpenPypeVersion) -> bool:
|
||||
|
|
@ -961,8 +1108,12 @@ class BootstrapRepos:
|
|||
"openpype/version.py") as version_file:
|
||||
zip_version = {}
|
||||
exec(version_file.read(), zip_version)
|
||||
version_check = OpenPypeVersion(
|
||||
version=zip_version["__version__"])
|
||||
try:
|
||||
version_check = OpenPypeVersion(
|
||||
version=zip_version["__version__"])
|
||||
except ValueError as e:
|
||||
self._print(str(e), True)
|
||||
return False
|
||||
|
||||
version_main = version_check.get_main_version() # noqa: E501
|
||||
detected_main = detected_version.get_main_version() # noqa: E501
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -17,12 +17,6 @@ from .bootstrap_repos import (
|
|||
from .tools import validate_mongo_connection
|
||||
|
||||
|
||||
class InstallResult(QObject):
|
||||
"""Used to pass results back."""
|
||||
def __init__(self, value):
|
||||
self.status = value
|
||||
|
||||
|
||||
class InstallThread(QThread):
|
||||
"""Install Worker thread.
|
||||
|
||||
|
|
@ -36,15 +30,22 @@ class InstallThread(QThread):
|
|||
"""
|
||||
progress = Signal(int)
|
||||
message = Signal((str, bool))
|
||||
finished = Signal(object)
|
||||
|
||||
def __init__(self, callback, parent=None,):
|
||||
def __init__(self, parent=None,):
|
||||
self._mongo = None
|
||||
self._path = None
|
||||
self.result_callback = callback
|
||||
self._result = None
|
||||
|
||||
QThread.__init__(self, parent)
|
||||
self.finished.connect(callback)
|
||||
|
||||
def result(self):
|
||||
"""Result of finished installation."""
|
||||
return self._result
|
||||
|
||||
def _set_result(self, value):
|
||||
if self._result is not None:
|
||||
raise AssertionError("BUG: Result was set more than once!")
|
||||
self._result = value
|
||||
|
||||
def run(self):
|
||||
"""Thread entry point.
|
||||
|
|
@ -76,7 +77,7 @@ class InstallThread(QThread):
|
|||
except ValueError:
|
||||
self.message.emit(
|
||||
"!!! We need MongoDB URL to proceed.", True)
|
||||
self.finished.emit(InstallResult(-1))
|
||||
self._set_result(-1)
|
||||
return
|
||||
else:
|
||||
self._mongo = os.getenv("OPENPYPE_MONGO")
|
||||
|
|
@ -101,7 +102,7 @@ class InstallThread(QThread):
|
|||
self.message.emit("Skipping OpenPype install ...", False)
|
||||
if detected[-1].path.suffix.lower() == ".zip":
|
||||
bs.extract_openpype(detected[-1])
|
||||
self.finished.emit(InstallResult(0))
|
||||
self._set_result(0)
|
||||
return
|
||||
|
||||
if OpenPypeVersion(version=local_version).get_main_version() == detected[-1].get_main_version(): # noqa
|
||||
|
|
@ -110,7 +111,7 @@ class InstallThread(QThread):
|
|||
f"currently running {local_version}"
|
||||
), False)
|
||||
self.message.emit("Skipping OpenPype install ...", False)
|
||||
self.finished.emit(InstallResult(0))
|
||||
self._set_result(0)
|
||||
return
|
||||
|
||||
self.message.emit((
|
||||
|
|
@ -126,13 +127,13 @@ class InstallThread(QThread):
|
|||
if not openpype_version:
|
||||
self.message.emit(
|
||||
f"!!! Install failed - {openpype_version}", True)
|
||||
self.finished.emit(InstallResult(-1))
|
||||
self._set_result(-1)
|
||||
return
|
||||
self.message.emit(f"Using: {openpype_version}", False)
|
||||
bs.install_version(openpype_version)
|
||||
self.message.emit(f"Installed as {openpype_version}", False)
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(InstallResult(1))
|
||||
self._set_result(1)
|
||||
return
|
||||
else:
|
||||
self.message.emit("None detected.", False)
|
||||
|
|
@ -144,7 +145,7 @@ class InstallThread(QThread):
|
|||
if not local_openpype:
|
||||
self.message.emit(
|
||||
f"!!! Install failed - {local_openpype}", True)
|
||||
self.finished.emit(InstallResult(-1))
|
||||
self._set_result(-1)
|
||||
return
|
||||
|
||||
try:
|
||||
|
|
@ -154,11 +155,12 @@ class InstallThread(QThread):
|
|||
OpenPypeVersionIOError) as e:
|
||||
self.message.emit(f"Installed failed: ", True)
|
||||
self.message.emit(str(e), True)
|
||||
self.finished.emit(InstallResult(-1))
|
||||
self._set_result(-1)
|
||||
return
|
||||
|
||||
self.message.emit(f"Installed as {local_openpype}", False)
|
||||
self.progress.emit(100)
|
||||
self._set_result(1)
|
||||
return
|
||||
else:
|
||||
# if we have mongo connection string, validate it, set it to
|
||||
|
|
@ -167,7 +169,7 @@ class InstallThread(QThread):
|
|||
if not validate_mongo_connection(self._mongo):
|
||||
self.message.emit(
|
||||
f"!!! invalid mongo url {self._mongo}", True)
|
||||
self.finished.emit(InstallResult(-1))
|
||||
self._set_result(-1)
|
||||
return
|
||||
bs.secure_registry.set_item("openPypeMongo", self._mongo)
|
||||
os.environ["OPENPYPE_MONGO"] = self._mongo
|
||||
|
|
@ -177,11 +179,11 @@ class InstallThread(QThread):
|
|||
|
||||
if not repo_file:
|
||||
self.message.emit("!!! Cannot install", True)
|
||||
self.finished.emit(InstallResult(-1))
|
||||
self._set_result(-1)
|
||||
return
|
||||
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(InstallResult(1))
|
||||
self._set_result(1)
|
||||
return
|
||||
|
||||
def set_path(self, path: str) -> None:
|
||||
|
|
|
|||
20
igniter/nice_progress_bar.py
Normal file
20
igniter/nice_progress_bar.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from Qt import QtCore, QtGui, QtWidgets # noqa
|
||||
|
||||
|
||||
class NiceProgressBar(QtWidgets.QProgressBar):
|
||||
def __init__(self, parent=None):
|
||||
super(NiceProgressBar, self).__init__(parent)
|
||||
self._real_value = 0
|
||||
|
||||
def setValue(self, value):
|
||||
self._real_value = value
|
||||
if value != 0 and value < 11:
|
||||
value = 11
|
||||
|
||||
super(NiceProgressBar, self).setValue(value)
|
||||
|
||||
def value(self):
|
||||
return self._real_value
|
||||
|
||||
def text(self):
|
||||
return "{} %".format(self._real_value)
|
||||
BIN
igniter/openpype.icns
Normal file
BIN
igniter/openpype.icns
Normal file
Binary file not shown.
280
igniter/stylesheet.css
Normal file
280
igniter/stylesheet.css
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
*{
|
||||
font-size: 10pt;
|
||||
font-family: "Poppins";
|
||||
}
|
||||
|
||||
QWidget {
|
||||
color: #bfccd6;
|
||||
background-color: #282C34;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
QMenu {
|
||||
border: 1px solid #555555;
|
||||
background-color: #21252B;
|
||||
}
|
||||
|
||||
QMenu::item {
|
||||
padding: 5px 10px 5px 10px;
|
||||
border-left: 5px solid #313741;;
|
||||
}
|
||||
|
||||
QMenu::item:selected {
|
||||
border-left-color: rgb(84, 209, 178);
|
||||
background-color: #222d37;
|
||||
}
|
||||
|
||||
QLineEdit, QPlainTextEdit {
|
||||
border: 1px solid #464b54;
|
||||
border-radius: 3px;
|
||||
background-color: #21252B;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
QLineEdit[state="valid"] {
|
||||
background-color: rgb(19, 19, 19);
|
||||
color: rgb(64, 230, 132);
|
||||
border-color: rgb(32, 64, 32);
|
||||
}
|
||||
|
||||
QLineEdit[state="invalid"] {
|
||||
background-color: rgb(32, 19, 19);
|
||||
color: rgb(255, 69, 0);
|
||||
border-color: rgb(64, 32, 32);
|
||||
}
|
||||
|
||||
QLabel {
|
||||
background: transparent;
|
||||
color: #969b9e;
|
||||
}
|
||||
|
||||
QLabel:hover {color: #b8c1c5;}
|
||||
|
||||
QPushButton {
|
||||
border: 1px solid #aaaaaa;
|
||||
border-radius: 3px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #333840;
|
||||
border: 1px solid #fff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
QTableView {
|
||||
border: 1px solid #444;
|
||||
gridline-color: #6c6c6c;
|
||||
background-color: #201F1F;
|
||||
alternate-background-color:#21252B;
|
||||
}
|
||||
|
||||
QTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed {
|
||||
background: #78879b;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
QTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active {
|
||||
background: #3d8ec9;
|
||||
}
|
||||
|
||||
QProgressBar {
|
||||
border: 1px solid grey;
|
||||
border-radius: 10px;
|
||||
color: #222222;
|
||||
font-weight: bold;
|
||||
}
|
||||
QProgressBar:horizontal {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
border-radius: 10px;
|
||||
background-color: qlineargradient(
|
||||
x1: 0,
|
||||
y1: 0.5,
|
||||
x2: 1,
|
||||
y2: 0.5,
|
||||
stop: 0 rgb(72, 200, 150),
|
||||
stop: 1 rgb(82, 172, 215)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
QScrollBar:horizontal {
|
||||
height: 15px;
|
||||
margin: 3px 15px 3px 15px;
|
||||
border: 1px transparent #21252B;
|
||||
border-radius: 4px;
|
||||
background-color: #21252B;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal {
|
||||
background-color: #4B5362;
|
||||
min-width: 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QScrollBar::add-line:horizontal {
|
||||
margin: 0px 3px 0px 3px;
|
||||
border-image: url(:/qss_icons/rc/right_arrow_disabled.png);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
subcontrol-position: right;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
|
||||
QScrollBar::sub-line:horizontal {
|
||||
margin: 0px 3px 0px 3px;
|
||||
border-image: url(:/qss_icons/rc/left_arrow_disabled.png);
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
subcontrol-position: left;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
|
||||
QScrollBar::add-line:horizontal:hover,QScrollBar::add-line:horizontal:on {
|
||||
border-image: url(:/qss_icons/rc/right_arrow.png);
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
subcontrol-position: right;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
|
||||
QScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on {
|
||||
border-image: url(:/qss_icons/rc/left_arrow.png);
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
subcontrol-position: left;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
|
||||
QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal {
|
||||
background: none;
|
||||
}
|
||||
|
||||
QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
|
||||
background: none;
|
||||
}
|
||||
|
||||
QScrollBar:vertical {
|
||||
background-color: #21252B;
|
||||
width: 15px;
|
||||
margin: 15px 3px 15px 3px;
|
||||
border: 1px transparent #21252B;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical {
|
||||
background-color: #4B5362;
|
||||
min-height: 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QScrollBar::sub-line:vertical {
|
||||
margin: 3px 0px 3px 0px;
|
||||
border-image: url(:/qss_icons/rc/up_arrow_disabled.png);
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
subcontrol-position: top;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
|
||||
QScrollBar::add-line:vertical {
|
||||
margin: 3px 0px 3px 0px;
|
||||
border-image: url(:/qss_icons/rc/down_arrow_disabled.png);
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
subcontrol-position: bottom;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
|
||||
QScrollBar::sub-line:vertical:hover,QScrollBar::sub-line:vertical:on {
|
||||
|
||||
border-image: url(:/qss_icons/rc/up_arrow.png);
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
subcontrol-position: top;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
|
||||
|
||||
QScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on {
|
||||
border-image: url(:/qss_icons/rc/down_arrow.png);
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
subcontrol-position: bottom;
|
||||
subcontrol-origin: margin;
|
||||
}
|
||||
|
||||
QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {
|
||||
background: none;
|
||||
}
|
||||
|
||||
|
||||
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
||||
background: none;
|
||||
}
|
||||
|
||||
#MainLabel {
|
||||
color: rgb(200, 200, 200);
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
#Console {
|
||||
background-color: #21252B;
|
||||
color: rgb(72, 200, 150);
|
||||
font-family: "Roboto Mono";
|
||||
font-size: 8pt;
|
||||
}
|
||||
|
||||
#ExitBtn {
|
||||
/* `border` must be set to background of flat button is painted .*/
|
||||
border: none;
|
||||
color: rgb(39, 39, 39);
|
||||
background-color: #828a97;
|
||||
padding: 0.5em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
#ExitBtn:hover{
|
||||
background-color: #b2bece
|
||||
}
|
||||
#ExitBtn:disabled {
|
||||
background-color: rgba(185, 185, 185, 31);
|
||||
color: rgba(64, 64, 64, 63);
|
||||
}
|
||||
|
||||
#ButtonWithOptions QPushButton{
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
border: none;
|
||||
background-color: rgb(84, 209, 178);
|
||||
color: rgb(39, 39, 39);
|
||||
font-weight: 400;
|
||||
padding: 0.5em;
|
||||
}
|
||||
#ButtonWithOptions QPushButton:hover{
|
||||
background-color: rgb(85, 224, 189)
|
||||
}
|
||||
#ButtonWithOptions QPushButton:disabled {
|
||||
background-color: rgba(72, 200, 150, 31);
|
||||
color: rgba(64, 64, 64, 63);
|
||||
}
|
||||
|
||||
#ButtonWithOptions QToolButton{
|
||||
border: none;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
background-color: rgb(84, 209, 178);
|
||||
color: rgb(39, 39, 39);
|
||||
}
|
||||
#ButtonWithOptions QToolButton:hover{
|
||||
background-color: rgb(85, 224, 189)
|
||||
}
|
||||
#ButtonWithOptions QToolButton:disabled {
|
||||
background-color: rgba(72, 200, 150, 31);
|
||||
color: rgba(64, 64, 64, 63);
|
||||
}
|
||||
|
|
@ -14,7 +14,12 @@ from pathlib import Path
|
|||
import platform
|
||||
|
||||
from pymongo import MongoClient
|
||||
from pymongo.errors import ServerSelectionTimeoutError, InvalidURI
|
||||
from pymongo.errors import (
|
||||
ServerSelectionTimeoutError,
|
||||
InvalidURI,
|
||||
ConfigurationError,
|
||||
OperationFailure
|
||||
)
|
||||
|
||||
|
||||
def decompose_url(url: str) -> Dict:
|
||||
|
|
@ -115,30 +120,20 @@ def validate_mongo_connection(cnx: str) -> (bool, str):
|
|||
parsed = urlparse(cnx)
|
||||
if parsed.scheme not in ["mongodb", "mongodb+srv"]:
|
||||
return False, "Not mongodb schema"
|
||||
# we have mongo connection string. Let's try if we can connect.
|
||||
try:
|
||||
components = decompose_url(cnx)
|
||||
except RuntimeError:
|
||||
return False, f"Invalid port specified."
|
||||
|
||||
mongo_args = {
|
||||
"host": compose_url(**components),
|
||||
"serverSelectionTimeoutMS": 2000
|
||||
}
|
||||
port = components.get("port")
|
||||
if port is not None:
|
||||
mongo_args["port"] = int(port)
|
||||
|
||||
try:
|
||||
client = MongoClient(**mongo_args)
|
||||
client = MongoClient(
|
||||
cnx,
|
||||
serverSelectionTimeoutMS=2000
|
||||
)
|
||||
client.server_info()
|
||||
client.close()
|
||||
except ServerSelectionTimeoutError as e:
|
||||
return False, f"Cannot connect to server {cnx} - {e}"
|
||||
except ValueError:
|
||||
return False, f"Invalid port specified {parsed.port}"
|
||||
except InvalidURI as e:
|
||||
return False, str(e)
|
||||
except (ConfigurationError, OperationFailure, InvalidURI) as exc:
|
||||
return False, str(exc)
|
||||
else:
|
||||
return True, "Connection is successful"
|
||||
|
||||
|
|
@ -253,3 +248,15 @@ def get_openpype_path_from_db(url: str) -> Union[str, None]:
|
|||
if os.path.exists(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def load_stylesheet() -> str:
|
||||
"""Load css style sheet.
|
||||
|
||||
Returns:
|
||||
str: content of the stylesheet
|
||||
|
||||
"""
|
||||
stylesheet_path = Path(__file__).parent.resolve() / "stylesheet.css"
|
||||
|
||||
return stylesheet_path.read_text()
|
||||
|
|
|
|||
61
igniter/update_thread.py
Normal file
61
igniter/update_thread.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Working thread for update."""
|
||||
from Qt.QtCore import QThread, Signal, QObject # noqa
|
||||
|
||||
from .bootstrap_repos import (
|
||||
BootstrapRepos,
|
||||
OpenPypeVersion
|
||||
)
|
||||
|
||||
|
||||
class UpdateThread(QThread):
|
||||
"""Install Worker thread.
|
||||
|
||||
This class takes care of finding OpenPype version on user entered path
|
||||
(or loading this path from database). If nothing is entered by user,
|
||||
OpenPype will create its zip files from repositories that comes with it.
|
||||
|
||||
If path contains plain repositories, they are zipped and installed to
|
||||
user data dir.
|
||||
|
||||
"""
|
||||
progress = Signal(int)
|
||||
message = Signal((str, bool))
|
||||
|
||||
def __init__(self, parent=None):
|
||||
self._result = None
|
||||
self._openpype_version = None
|
||||
QThread.__init__(self, parent)
|
||||
|
||||
def set_version(self, openpype_version: OpenPypeVersion):
|
||||
self._openpype_version = openpype_version
|
||||
|
||||
def result(self):
|
||||
"""Result of finished installation."""
|
||||
return self._result
|
||||
|
||||
def _set_result(self, value):
|
||||
if self._result is not None:
|
||||
raise AssertionError("BUG: Result was set more than once!")
|
||||
self._result = value
|
||||
|
||||
def run(self):
|
||||
"""Thread entry point.
|
||||
|
||||
Using :class:`BootstrapRepos` to either install OpenPype as zip files
|
||||
or copy them from location specified by user or retrieved from
|
||||
database.
|
||||
"""
|
||||
bs = BootstrapRepos(
|
||||
progress_callback=self.set_progress, message=self.message)
|
||||
version_path = bs.install_version(self._openpype_version)
|
||||
self._set_result(version_path)
|
||||
|
||||
def set_progress(self, progress: int) -> None:
|
||||
"""Helper to set progress bar.
|
||||
|
||||
Args:
|
||||
progress (int): Progress in percents.
|
||||
|
||||
"""
|
||||
self.progress.emit(progress)
|
||||
136
igniter/update_window.py
Normal file
136
igniter/update_window.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Progress window to show when OpenPype is updating/installing locally."""
|
||||
import os
|
||||
from .update_thread import UpdateThread
|
||||
from Qt import QtCore, QtGui, QtWidgets # noqa
|
||||
from .bootstrap_repos import OpenPypeVersion
|
||||
from .nice_progress_bar import NiceProgressBar
|
||||
from .tools import load_stylesheet
|
||||
|
||||
|
||||
class UpdateWindow(QtWidgets.QDialog):
|
||||
"""OpenPype update window."""
|
||||
|
||||
_width = 500
|
||||
_height = 100
|
||||
|
||||
def __init__(self, version: OpenPypeVersion, parent=None):
|
||||
super(UpdateWindow, self).__init__(parent)
|
||||
self._openpype_version = version
|
||||
self._result_version_path = None
|
||||
|
||||
self.setWindowTitle(
|
||||
f"OpenPype is updating ..."
|
||||
)
|
||||
self.setModal(True)
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowMinimizeButtonHint
|
||||
)
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
roboto_font_path = os.path.join(current_dir, "RobotoMono-Regular.ttf")
|
||||
poppins_font_path = os.path.join(current_dir, "Poppins")
|
||||
icon_path = os.path.join(current_dir, "openpype_icon.png")
|
||||
|
||||
# Install roboto font
|
||||
QtGui.QFontDatabase.addApplicationFont(roboto_font_path)
|
||||
for filename in os.listdir(poppins_font_path):
|
||||
if os.path.splitext(filename)[1] == ".ttf":
|
||||
QtGui.QFontDatabase.addApplicationFont(filename)
|
||||
|
||||
# Load logo
|
||||
pixmap_openpype_logo = QtGui.QPixmap(icon_path)
|
||||
# Set logo as icon of window
|
||||
self.setWindowIcon(QtGui.QIcon(pixmap_openpype_logo))
|
||||
|
||||
self._pixmap_openpype_logo = pixmap_openpype_logo
|
||||
|
||||
self._update_thread = None
|
||||
|
||||
self.resize(QtCore.QSize(self._width, self._height))
|
||||
self._init_ui()
|
||||
|
||||
# Set stylesheet
|
||||
self.setStyleSheet(load_stylesheet())
|
||||
self._run_update()
|
||||
|
||||
def _init_ui(self):
|
||||
|
||||
# Main info
|
||||
# --------------------------------------------------------------------
|
||||
main_label = QtWidgets.QLabel(
|
||||
f"<b>OpenPype</b> is updating to {self._openpype_version}", self)
|
||||
main_label.setWordWrap(True)
|
||||
main_label.setObjectName("MainLabel")
|
||||
|
||||
# Progress bar
|
||||
# --------------------------------------------------------------------
|
||||
progress_bar = NiceProgressBar(self)
|
||||
progress_bar.setAlignment(QtCore.Qt.AlignCenter)
|
||||
progress_bar.setTextVisible(False)
|
||||
|
||||
# add all to main
|
||||
main = QtWidgets.QVBoxLayout(self)
|
||||
main.addSpacing(15)
|
||||
main.addWidget(main_label, 0)
|
||||
main.addSpacing(15)
|
||||
main.addWidget(progress_bar, 0)
|
||||
main.addSpacing(15)
|
||||
|
||||
self._progress_bar = progress_bar
|
||||
|
||||
def _run_update(self):
|
||||
"""Start install process.
|
||||
|
||||
This will once again validate entered path and mongo if ok, start
|
||||
working thread that will do actual job.
|
||||
"""
|
||||
# Check if install thread is not already running
|
||||
if self._update_thread and self._update_thread.isRunning():
|
||||
return
|
||||
self._progress_bar.setRange(0, 0)
|
||||
update_thread = UpdateThread(self)
|
||||
update_thread.set_version(self._openpype_version)
|
||||
update_thread.message.connect(self.update_console)
|
||||
update_thread.progress.connect(self._update_progress)
|
||||
update_thread.finished.connect(self._installation_finished)
|
||||
|
||||
self._update_thread = update_thread
|
||||
|
||||
update_thread.start()
|
||||
|
||||
def get_version_path(self):
|
||||
return self._result_version_path
|
||||
|
||||
def _installation_finished(self):
|
||||
status = self._update_thread.result()
|
||||
self._result_version_path = status
|
||||
self._progress_bar.setRange(0, 1)
|
||||
self._update_progress(100)
|
||||
QtWidgets.QApplication.processEvents()
|
||||
self.done(0)
|
||||
|
||||
def _update_progress(self, progress: int):
|
||||
# not updating progress as we are not able to determine it
|
||||
# correctly now. Progress bar is set to un-deterministic mode
|
||||
# until we are able to get progress in better way.
|
||||
"""
|
||||
self._progress_bar.setRange(0, 0)
|
||||
self._progress_bar.setValue(progress)
|
||||
text_visible = self._progress_bar.isTextVisible()
|
||||
if progress == 0:
|
||||
if text_visible:
|
||||
self._progress_bar.setTextVisible(False)
|
||||
elif not text_visible:
|
||||
self._progress_bar.setTextVisible(True)
|
||||
"""
|
||||
return
|
||||
|
||||
def update_console(self, msg: str, error: bool = False) -> None:
|
||||
"""Display message in console.
|
||||
|
||||
Args:
|
||||
msg (str): message.
|
||||
error (bool): if True, print it red.
|
||||
"""
|
||||
print(msg)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Definition of Igniter version."""
|
||||
|
||||
__version__ = "1.0.0-beta"
|
||||
__version__ = "1.0.1"
|
||||
|
|
|
|||
50
inno_setup.iss
Normal file
50
inno_setup.iss
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
; Script generated by the Inno Setup Script Wizard.
|
||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
|
||||
|
||||
#define MyAppName "OpenPype"
|
||||
#define Build GetEnv("BUILD_DIR")
|
||||
#define AppVer GetEnv("BUILD_VERSION")
|
||||
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||
AppId={{B9E9DF6A-5BDA-42DD-9F35-C09D564C4D93}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#AppVer}
|
||||
AppVerName={#MyAppName} version {#AppVer}
|
||||
AppPublisher=Orbi Tools s.r.o
|
||||
AppPublisherURL=http://pype.club
|
||||
AppSupportURL=http://pype.club
|
||||
AppUpdatesURL=http://pype.club
|
||||
DefaultDirName={autopf}\{#MyAppName}
|
||||
DisableProgramGroupPage=yes
|
||||
OutputBaseFilename={#MyAppName}-{#AppVer}-install
|
||||
AllowCancelDuringInstall=yes
|
||||
; Uncomment the following line to run in non administrative install mode (install for current user only.)
|
||||
;PrivilegesRequired=lowest
|
||||
PrivilegesRequiredOverridesAllowed=dialog
|
||||
SetupIconFile=igniter\openpype.ico
|
||||
OutputDir=build\
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
|
||||
[Files]
|
||||
Source: "build\{#build}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\openpype_gui.exe"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\openpype_gui.exe"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\openpype_gui.exe"; Description: "{cm:LaunchProgram,OpenPype}"; Flags: nowait postinstall skipifsilent
|
||||
|
||||
|
|
@ -9,6 +9,7 @@ from .settings import get_project_settings
|
|||
from .lib import (
|
||||
Anatomy,
|
||||
filter_pyblish_plugins,
|
||||
set_plugin_attributes_from_settings,
|
||||
change_timer_to_current_context
|
||||
)
|
||||
|
||||
|
|
@ -58,44 +59,27 @@ def patched_discover(superclass):
|
|||
# run original discover and get plugins
|
||||
plugins = _original_discover(superclass)
|
||||
|
||||
# determine host application to use for finding presets
|
||||
if avalon.registered_host() is None:
|
||||
return plugins
|
||||
host = avalon.registered_host().__name__.split(".")[-1]
|
||||
set_plugin_attributes_from_settings(plugins, superclass)
|
||||
|
||||
# map plugin superclass to preset json. Currenly suppoted is load and
|
||||
# create (avalon.api.Loader and avalon.api.Creator)
|
||||
plugin_type = "undefined"
|
||||
if superclass.__name__.split(".")[-1] == "Loader":
|
||||
plugin_type = "load"
|
||||
elif superclass.__name__.split(".")[-1] == "Creator":
|
||||
plugin_type = "create"
|
||||
|
||||
print(">>> Finding presets for {}:{} ...".format(host, plugin_type))
|
||||
try:
|
||||
settings = (
|
||||
get_project_settings(os.environ['AVALON_PROJECT'])
|
||||
[host][plugin_type]
|
||||
)
|
||||
except KeyError:
|
||||
print("*** no presets found.")
|
||||
else:
|
||||
for plugin in plugins:
|
||||
if plugin.__name__ in settings:
|
||||
print(">>> We have preset for {}".format(plugin.__name__))
|
||||
for option, value in settings[plugin.__name__].items():
|
||||
if option == "enabled" and value is False:
|
||||
setattr(plugin, "active", False)
|
||||
print(" - is disabled by preset")
|
||||
else:
|
||||
setattr(plugin, option, value)
|
||||
print(" - setting `{}`: `{}`".format(option, value))
|
||||
return plugins
|
||||
|
||||
|
||||
@import_wrapper
|
||||
def install():
|
||||
"""Install Pype to Avalon."""
|
||||
from pyblish.lib import MessageHandler
|
||||
from openpype.modules import load_modules
|
||||
|
||||
# Make sure modules are loaded
|
||||
load_modules()
|
||||
|
||||
def modified_emit(obj, record):
|
||||
"""Method replacing `emit` in Pyblish's MessageHandler."""
|
||||
record.msg = record.getMessage()
|
||||
obj.records.append(record)
|
||||
|
||||
MessageHandler.emit = modified_emit
|
||||
|
||||
log.info("Registering global plug-ins..")
|
||||
pyblish.register_plugin_path(PUBLISH_PATH)
|
||||
pyblish.register_discovery_filter(filter_pyblish_plugins)
|
||||
|
|
@ -118,6 +102,11 @@ def install():
|
|||
.get(platform_name)
|
||||
) or []
|
||||
for path in project_plugins:
|
||||
try:
|
||||
path = str(path.format(**os.environ))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if not path or not os.path.exists(path):
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ from .lib import (
|
|||
get_latest_version,
|
||||
get_global_environments,
|
||||
get_local_site_id,
|
||||
change_openpype_mongo_url
|
||||
change_openpype_mongo_url,
|
||||
create_project_folders,
|
||||
get_project_basic_paths
|
||||
)
|
||||
|
||||
from .lib.mongo import (
|
||||
|
|
@ -72,6 +74,7 @@ __all__ = [
|
|||
"get_current_project_settings",
|
||||
"get_anatomy_settings",
|
||||
"get_environments",
|
||||
"get_project_basic_paths",
|
||||
|
||||
"SystemSettings",
|
||||
|
||||
|
|
@ -120,5 +123,9 @@ __all__ = [
|
|||
"get_global_environments",
|
||||
|
||||
"get_local_site_id",
|
||||
"change_openpype_mongo_url"
|
||||
"change_openpype_mongo_url",
|
||||
|
||||
"get_project_basic_paths",
|
||||
"create_project_folders"
|
||||
|
||||
]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ from .pype_commands import PypeCommands
|
|||
expose_value=False, help="use specified version")
|
||||
@click.option("--use-staging", is_flag=True,
|
||||
expose_value=False, help="use staging variants")
|
||||
@click.option("--list-versions", is_flag=True, expose_value=False,
|
||||
help=("list all detected versions. Use With `--use-staging "
|
||||
"to list staging versions."))
|
||||
@click.option("--validate-version", expose_value=False,
|
||||
help="validate given version integrity")
|
||||
def main(ctx):
|
||||
"""Pype is main command serving as entry point to pipeline system.
|
||||
|
||||
|
|
@ -26,7 +31,7 @@ def main(ctx):
|
|||
|
||||
@main.command()
|
||||
@click.option("-d", "--dev", is_flag=True, help="Settings in Dev mode")
|
||||
def settings(dev=False):
|
||||
def settings(dev):
|
||||
"""Show Pype Settings UI."""
|
||||
PypeCommands().launch_settings_gui(dev)
|
||||
|
||||
|
|
@ -60,13 +65,6 @@ def tray(debug=False):
|
|||
help="Ftrack api user")
|
||||
@click.option("--ftrack-api-key", envvar="FTRACK_API_KEY",
|
||||
help="Ftrack api key")
|
||||
@click.option("--ftrack-events-path",
|
||||
envvar="FTRACK_EVENTS_PATH",
|
||||
help=("path to ftrack event handlers"))
|
||||
@click.option("--no-stored-credentials", is_flag=True,
|
||||
help="don't use stored credentials")
|
||||
@click.option("--store-credentials", is_flag=True,
|
||||
help="store provided credentials")
|
||||
@click.option("--legacy", is_flag=True,
|
||||
help="run event server without mongo storing")
|
||||
@click.option("--clockify-api-key", envvar="CLOCKIFY_API_KEY",
|
||||
|
|
@ -77,9 +75,6 @@ def eventserver(debug,
|
|||
ftrack_url,
|
||||
ftrack_user,
|
||||
ftrack_api_key,
|
||||
ftrack_events_path,
|
||||
no_stored_credentials,
|
||||
store_credentials,
|
||||
legacy,
|
||||
clockify_api_key,
|
||||
clockify_workspace):
|
||||
|
|
@ -87,10 +82,6 @@ def eventserver(debug,
|
|||
|
||||
This should be ideally used by system service (such us systemd or upstart
|
||||
on linux and window service).
|
||||
|
||||
You have to set either proper environment variables to provide URL and
|
||||
credentials or use option to specify them. If you use --store_credentials
|
||||
provided credentials will be stored for later use.
|
||||
"""
|
||||
if debug:
|
||||
os.environ['OPENPYPE_DEBUG'] = "3"
|
||||
|
|
@ -99,15 +90,37 @@ def eventserver(debug,
|
|||
ftrack_url,
|
||||
ftrack_user,
|
||||
ftrack_api_key,
|
||||
ftrack_events_path,
|
||||
no_stored_credentials,
|
||||
store_credentials,
|
||||
legacy,
|
||||
clockify_api_key,
|
||||
clockify_workspace
|
||||
)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option("-d", "--debug", is_flag=True, help="Print debug messages")
|
||||
@click.option("-h", "--host", help="Host", default=None)
|
||||
@click.option("-p", "--port", help="Port", default=None)
|
||||
@click.option("-e", "--executable", help="Executable")
|
||||
@click.option("-u", "--upload_dir", help="Upload dir")
|
||||
def webpublisherwebserver(debug, executable, upload_dir, host=None, port=None):
|
||||
"""Starts webserver for communication with Webpublish FR via command line
|
||||
|
||||
OP must be congigured on a machine, eg. OPENPYPE_MONGO filled AND
|
||||
FTRACK_BOT_API_KEY provided with api key from Ftrack.
|
||||
|
||||
Expect "pype.club" user created on Ftrack.
|
||||
"""
|
||||
if debug:
|
||||
os.environ['OPENPYPE_DEBUG'] = "3"
|
||||
|
||||
PypeCommands().launch_webpublisher_webservercli(
|
||||
upload_dir=upload_dir,
|
||||
executable=executable,
|
||||
host=host,
|
||||
port=port
|
||||
)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument("output_json_path")
|
||||
@click.option("--project", help="Project name", default=None)
|
||||
|
|
@ -132,7 +145,9 @@ def extractenvironments(output_json_path, project, asset, task, app):
|
|||
@main.command()
|
||||
@click.argument("paths", nargs=-1)
|
||||
@click.option("-d", "--debug", is_flag=True, help="Print debug messages")
|
||||
def publish(debug, paths):
|
||||
@click.option("-t", "--targets", help="Targets module", default=None,
|
||||
multiple=True)
|
||||
def publish(debug, paths, targets):
|
||||
"""Start CLI publishing.
|
||||
|
||||
Publish collects json from paths provided as an argument.
|
||||
|
|
@ -140,7 +155,26 @@ def publish(debug, paths):
|
|||
"""
|
||||
if debug:
|
||||
os.environ['OPENPYPE_DEBUG'] = '3'
|
||||
PypeCommands.publish(list(paths))
|
||||
PypeCommands.publish(list(paths), targets)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument("path")
|
||||
@click.option("-d", "--debug", is_flag=True, help="Print debug messages")
|
||||
@click.option("-h", "--host", help="Host")
|
||||
@click.option("-u", "--user", help="User email address")
|
||||
@click.option("-p", "--project", help="Project")
|
||||
@click.option("-t", "--targets", help="Targets", default=None,
|
||||
multiple=True)
|
||||
def remotepublish(debug, project, path, host, targets=None, user=None):
|
||||
"""Start CLI publishing.
|
||||
|
||||
Publish collects json from paths provided as an argument.
|
||||
More than one path is allowed.
|
||||
"""
|
||||
if debug:
|
||||
os.environ['OPENPYPE_DEBUG'] = '3'
|
||||
PypeCommands.remotepublish(project, path, host, user, targets=targets)
|
||||
|
||||
|
||||
@main.command()
|
||||
|
|
@ -224,15 +258,9 @@ def launch(app, project, asset, task,
|
|||
PypeCommands().run_application(app, project, asset, task, tools, arguments)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option("-p", "--path", help="Path to zip file", default=None)
|
||||
def generate_zip(path):
|
||||
"""Generate Pype zip from current sources.
|
||||
|
||||
If PATH is not provided, it will create zip file in user data dir.
|
||||
|
||||
"""
|
||||
PypeCommands().generate_zip(path)
|
||||
@main.command(context_settings={"ignore_unknown_options": True})
|
||||
def projectmanager():
|
||||
PypeCommands().launch_project_manager()
|
||||
|
||||
|
||||
@main.command(
|
||||
|
|
|
|||
|
|
@ -8,8 +8,19 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
|
|||
This is not possible to do for all applications the same way.
|
||||
"""
|
||||
|
||||
order = 0
|
||||
app_groups = ["maya", "nuke", "nukex", "hiero", "nukestudio"]
|
||||
# Execute after workfile template copy
|
||||
order = 10
|
||||
app_groups = [
|
||||
"maya",
|
||||
"nuke",
|
||||
"nukex",
|
||||
"hiero",
|
||||
"nukestudio",
|
||||
"blender",
|
||||
"photoshop",
|
||||
"tvpaint",
|
||||
"afftereffects"
|
||||
]
|
||||
|
||||
def execute(self):
|
||||
if not self.data.get("start_last_workfile"):
|
||||
|
|
|
|||
127
openpype/hooks/pre_copy_template_workfile.py
Normal file
127
openpype/hooks/pre_copy_template_workfile.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import os
|
||||
import shutil
|
||||
from openpype.lib import (
|
||||
PreLaunchHook,
|
||||
get_custom_workfile_template_by_context,
|
||||
get_custom_workfile_template_by_string_context
|
||||
)
|
||||
from openpype.settings import get_project_settings
|
||||
|
||||
|
||||
class CopyTemplateWorkfile(PreLaunchHook):
|
||||
"""Copy workfile template.
|
||||
|
||||
This is not possible to do for all applications the same way.
|
||||
|
||||
Prelaunch hook works only if last workfile leads to not existing file.
|
||||
- That is possible only if it's first version.
|
||||
"""
|
||||
|
||||
# Before `AddLastWorkfileToLaunchArgs`
|
||||
order = 0
|
||||
app_groups = ["blender", "photoshop", "tvpaint", "afftereffects"]
|
||||
|
||||
def execute(self):
|
||||
"""Check if can copy template for context and do it if possible.
|
||||
|
||||
First check if host for current project should create first workfile.
|
||||
Second check is if template is reachable and can be copied.
|
||||
|
||||
Args:
|
||||
last_workfile(str): Path where template will be copied.
|
||||
|
||||
Returns:
|
||||
None: This is a void method.
|
||||
"""
|
||||
|
||||
last_workfile = self.data.get("last_workfile_path")
|
||||
if not last_workfile:
|
||||
self.log.warning((
|
||||
"Last workfile was not collected."
|
||||
" Can't add it to launch arguments or determine if should"
|
||||
" copy template."
|
||||
))
|
||||
return
|
||||
|
||||
if os.path.exists(last_workfile):
|
||||
self.log.debug("Last workfile exits. Skipping {} process.".format(
|
||||
self.__class__.__name__
|
||||
))
|
||||
return
|
||||
|
||||
self.log.info("Last workfile does not exist.")
|
||||
|
||||
project_name = self.data["project_name"]
|
||||
asset_name = self.data["asset_name"]
|
||||
task_name = self.data["task_name"]
|
||||
|
||||
project_settings = get_project_settings(project_name)
|
||||
host_settings = project_settings[self.application.host_name]
|
||||
|
||||
workfile_builder_settings = host_settings.get("workfile_builder")
|
||||
if not workfile_builder_settings:
|
||||
# TODO remove warning when deprecated
|
||||
self.log.warning((
|
||||
"Seems like old version of settings is used."
|
||||
" Can't access custom templates in host \"{}\"."
|
||||
).format(self.application.full_label))
|
||||
return
|
||||
|
||||
if not workfile_builder_settings["create_first_version"]:
|
||||
self.log.info((
|
||||
"Project \"{}\" has turned off to create first workfile for"
|
||||
" application \"{}\""
|
||||
).format(project_name, self.application.full_label))
|
||||
return
|
||||
|
||||
# Backwards compatibility
|
||||
template_profiles = workfile_builder_settings.get("custom_templates")
|
||||
if not template_profiles:
|
||||
self.log.info(
|
||||
"Custom templates are not filled. Skipping template copy."
|
||||
)
|
||||
return
|
||||
|
||||
project_doc = self.data.get("project_doc")
|
||||
asset_doc = self.data.get("asset_doc")
|
||||
anatomy = self.data.get("anatomy")
|
||||
if project_doc and asset_doc:
|
||||
self.log.debug("Started filtering of custom template paths.")
|
||||
template_path = get_custom_workfile_template_by_context(
|
||||
template_profiles, project_doc, asset_doc, task_name, anatomy
|
||||
)
|
||||
|
||||
else:
|
||||
self.log.warning((
|
||||
"Global data collection probably did not execute."
|
||||
" Using backup solution."
|
||||
))
|
||||
dbcon = self.data.get("dbcon")
|
||||
template_path = get_custom_workfile_template_by_string_context(
|
||||
template_profiles, project_name, asset_name, task_name,
|
||||
dbcon, anatomy
|
||||
)
|
||||
|
||||
if not template_path:
|
||||
self.log.info(
|
||||
"Registered custom templates didn't match current context."
|
||||
)
|
||||
return
|
||||
|
||||
if not os.path.exists(template_path):
|
||||
self.log.warning(
|
||||
"Couldn't find workfile template file \"{}\"".format(
|
||||
template_path
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
self.log.info(
|
||||
f"Creating workfile from template: \"{template_path}\""
|
||||
)
|
||||
|
||||
# Copy template workfile to new destinantion
|
||||
shutil.copy2(
|
||||
os.path.normpath(template_path),
|
||||
os.path.normpath(last_workfile)
|
||||
)
|
||||
28
openpype/hooks/pre_foundry_apps.py
Normal file
28
openpype/hooks/pre_foundry_apps.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import subprocess
|
||||
from openpype.lib import PreLaunchHook
|
||||
|
||||
|
||||
class LaunchFoundryAppsWindows(PreLaunchHook):
|
||||
"""Foundry applications have specific way how to launch them.
|
||||
|
||||
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
|
||||
and stderr handlers so they should not be redirected to DEVNULL.
|
||||
"""
|
||||
|
||||
# Should be as last hook because must change launch arguments to string
|
||||
order = 1000
|
||||
app_groups = ["nuke", "nukex", "hiero", "nukestudio"]
|
||||
platforms = ["windows"]
|
||||
|
||||
def execute(self):
|
||||
# Change `creationflags` to CREATE_NEW_CONSOLE
|
||||
# - on Windows will nuke create new window using it's 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({
|
||||
"creationflags": subprocess.CREATE_NEW_CONSOLE,
|
||||
"stdout": None,
|
||||
"stderr": None
|
||||
})
|
||||
34
openpype/hooks/pre_mac_launch.py
Normal file
34
openpype/hooks/pre_mac_launch.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import os
|
||||
from openpype.lib import PreLaunchHook
|
||||
|
||||
|
||||
class LaunchWithTerminal(PreLaunchHook):
|
||||
"""Mac specific pre arguments for application.
|
||||
|
||||
Mac applications should be launched using "open" argument which is internal
|
||||
callbacks to open executable. We also add argument "-a" to tell it's
|
||||
application open. This is used only for executables ending with ".app". It
|
||||
is expected that these executables lead to app packages.
|
||||
"""
|
||||
order = 1000
|
||||
|
||||
platforms = ["darwin"]
|
||||
|
||||
def execute(self):
|
||||
executable = str(self.launch_context.executable)
|
||||
# Skip executables not ending with ".app" or that are not folder
|
||||
if not executable.endswith(".app") or not os.path.isdir(executable):
|
||||
return
|
||||
|
||||
# Check if first argument match executable path
|
||||
# - Few applications are not executed directly but through OpenPype
|
||||
# process (Photoshop, AfterEffects, Harmony, ...). These should not
|
||||
# use `open`.
|
||||
if self.launch_context.launch_args[0] != executable:
|
||||
return
|
||||
|
||||
# Tell `open` to pass arguments if there are any
|
||||
if len(self.launch_context.launch_args) > 1:
|
||||
self.launch_context.launch_args.insert(1, "--args")
|
||||
# Prepend open arguments
|
||||
self.launch_context.launch_args.insert(0, ["open", "-na"])
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import subprocess
|
||||
|
||||
from openpype.lib import (
|
||||
PreLaunchHook,
|
||||
|
|
@ -17,6 +18,8 @@ class NonPythonHostHook(PreLaunchHook):
|
|||
"""
|
||||
app_groups = ["harmony", "photoshop", "aftereffects"]
|
||||
|
||||
order = 20
|
||||
|
||||
def execute(self):
|
||||
# Pop executable
|
||||
executable_path = self.launch_context.launch_args.pop(0)
|
||||
|
|
@ -45,3 +48,8 @@ class NonPythonHostHook(PreLaunchHook):
|
|||
|
||||
if remainders:
|
||||
self.launch_context.launch_args.extend(remainders)
|
||||
|
||||
# This must be set otherwise it wouldn't be possible to catch output
|
||||
# when build OpenPype is used.
|
||||
self.launch_context.kwargs["stdout"] = subprocess.DEVNULL
|
||||
self.launch_context.kwargs["stderr"] = subprocess.DEVNULL
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ from openpype.lib import PreLaunchHook
|
|||
|
||||
class PrePython2Vendor(PreLaunchHook):
|
||||
"""Prepend python 2 dependencies for py2 hosts."""
|
||||
# WARNING This hook will probably be deprecated in OpenPype 3 - kept for
|
||||
# test
|
||||
order = 10
|
||||
app_groups = ["hiero", "nuke", "nukex", "maya", "houdini"]
|
||||
|
||||
def execute(self):
|
||||
if not self.application.use_python_2:
|
||||
return
|
||||
|
||||
# Prepare vendor dir path
|
||||
self.log.info("adding global python 2 vendor")
|
||||
pype_root = os.getenv("OPENPYPE_REPOS_ROOT")
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
import os
|
||||
import subprocess
|
||||
from openpype.lib import PreLaunchHook
|
||||
|
||||
|
||||
class LaunchWithWindowsShell(PreLaunchHook):
|
||||
"""Add shell command before executable.
|
||||
|
||||
Some hosts have issues when are launched directly from python in that case
|
||||
it is possible to prepend shell executable which will trigger process
|
||||
instead.
|
||||
"""
|
||||
|
||||
# Should be as last hook becuase must change launch arguments to string
|
||||
order = 1000
|
||||
app_groups = ["nuke", "nukex", "hiero", "nukestudio"]
|
||||
platforms = ["windows"]
|
||||
|
||||
def execute(self):
|
||||
new_args = [
|
||||
# Get comspec which is cmd.exe in most cases.
|
||||
os.environ.get("COMSPEC", "cmd.exe"),
|
||||
# NOTE change to "/k" if want to keep console opened
|
||||
"/c",
|
||||
# Convert arguments to command line arguments (as string)
|
||||
"\"{}\"".format(
|
||||
subprocess.list2cmdline(self.launch_context.launch_args)
|
||||
)
|
||||
]
|
||||
# Convert list to string
|
||||
# WARNING this only works if is used as string
|
||||
args_string = " ".join(new_args)
|
||||
self.log.info((
|
||||
"Modified launch arguments to be launched with shell \"{}\"."
|
||||
).format(args_string))
|
||||
|
||||
# Replace launch args with new one
|
||||
self.launch_context.launch_args = args_string
|
||||
# Change `creationflags` to CREATE_NEW_CONSOLE
|
||||
self.launch_context.kwargs["creationflags"] = (
|
||||
subprocess.CREATE_NEW_CONSOLE
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
def add_implementation_envs(env, _app):
|
||||
"""Modify environments to contain all required for implementation."""
|
||||
defaults = {
|
||||
"OPENPYPE_LOG_NO_COLORS": "True",
|
||||
"WEBSOCKET_URL": "ws://localhost:8097/ws/"
|
||||
}
|
||||
for key, value in defaults.items():
|
||||
if not env.get(key):
|
||||
env[key] = value
|
||||
|
|
@ -5,7 +5,7 @@ import logging
|
|||
from avalon import io
|
||||
from avalon import api as avalon
|
||||
from avalon.vendor import Qt
|
||||
from openpype import lib
|
||||
from openpype import lib, api
|
||||
import pyblish.api as pyblish
|
||||
import openpype.hosts.aftereffects
|
||||
|
||||
|
|
@ -81,3 +81,35 @@ def uninstall():
|
|||
def on_pyblish_instance_toggled(instance, old_value, new_value):
|
||||
"""Toggle layer visibility on instance toggles."""
|
||||
instance[0].Visible = new_value
|
||||
|
||||
|
||||
def get_asset_settings():
|
||||
"""Get settings on current asset from database.
|
||||
|
||||
Returns:
|
||||
dict: Scene data.
|
||||
|
||||
"""
|
||||
asset_data = lib.get_asset()["data"]
|
||||
fps = asset_data.get("fps")
|
||||
frame_start = asset_data.get("frameStart")
|
||||
frame_end = asset_data.get("frameEnd")
|
||||
handle_start = asset_data.get("handleStart")
|
||||
handle_end = asset_data.get("handleEnd")
|
||||
resolution_width = asset_data.get("resolutionWidth")
|
||||
resolution_height = asset_data.get("resolutionHeight")
|
||||
duration = (frame_end - frame_start + 1) + handle_start + handle_end
|
||||
entity_type = asset_data.get("entityType")
|
||||
|
||||
scene_data = {
|
||||
"fps": fps,
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"resolutionWidth": resolution_width,
|
||||
"resolutionHeight": resolution_height,
|
||||
"duration": duration
|
||||
}
|
||||
|
||||
return scene_data
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
from openpype.hosts.aftereffects.plugins.create import create_render
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CreateLocalRender(create_render.CreateRender):
|
||||
""" Creator to render locally.
|
||||
|
||||
Created only after default render on farm. So family 'render.local' is
|
||||
used for backward compatibility.
|
||||
"""
|
||||
|
||||
name = "renderDefault"
|
||||
label = "Render Locally"
|
||||
family = "renderLocal"
|
||||
|
|
@ -47,6 +47,10 @@ class CreateRender(openpype.api.Creator):
|
|||
|
||||
self.data["members"] = [item.id]
|
||||
self.data["uuid"] = item.id # for SubsetManager
|
||||
self.data["subset"] = self.data["subset"]\
|
||||
.replace(stub.PUBLISH_ICON, '')\
|
||||
.replace(stub.LOADED_ICON, '')
|
||||
|
||||
stub.imprint(item, self.data)
|
||||
stub.set_label_color(item.id, 14) # Cyan options 0 - 16
|
||||
stub.rename_item(item.id, stub.PUBLISH_ICON + self.data["subset"])
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
from openpype.lib import abstract_collect_render
|
||||
from openpype.lib.abstract_collect_render import RenderInstance
|
||||
import pyblish.api
|
||||
import attr
|
||||
import os
|
||||
import re
|
||||
import attr
|
||||
import tempfile
|
||||
|
||||
from avalon import aftereffects
|
||||
import pyblish.api
|
||||
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.lib import abstract_collect_render
|
||||
from openpype.lib.abstract_collect_render import RenderInstance
|
||||
|
||||
|
||||
@attr.s
|
||||
|
|
@ -12,6 +16,9 @@ class AERenderInstance(RenderInstance):
|
|||
# extend generic, composition name is needed
|
||||
comp_name = attr.ib(default=None)
|
||||
comp_id = attr.ib(default=None)
|
||||
fps = attr.ib(default=None)
|
||||
projectEntity = attr.ib(default=None)
|
||||
stagingDir = attr.ib(default=None)
|
||||
|
||||
|
||||
class CollectAERender(abstract_collect_render.AbstractCollectRender):
|
||||
|
|
@ -20,6 +27,11 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
|
|||
label = "Collect After Effects Render Layers"
|
||||
hosts = ["aftereffects"]
|
||||
|
||||
# internal
|
||||
family_remapping = {
|
||||
"render": ("render.farm", "farm"), # (family, label)
|
||||
"renderLocal": ("render", "local")
|
||||
}
|
||||
padding_width = 6
|
||||
rendered_extension = 'png'
|
||||
|
||||
|
|
@ -45,6 +57,7 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
|
|||
raise ValueError("Couldn't find id, unable to publish. " +
|
||||
"Please recreate instance.")
|
||||
item_id = inst["members"][0]
|
||||
|
||||
work_area_info = self.stub.get_work_area(int(item_id))
|
||||
|
||||
if not work_area_info:
|
||||
|
|
@ -57,15 +70,19 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
|
|||
frameEnd = round(work_area_info.workAreaStart +
|
||||
float(work_area_info.workAreaDuration) *
|
||||
float(work_area_info.frameRate)) - 1
|
||||
fps = work_area_info.frameRate
|
||||
# TODO add resolution when supported by extension
|
||||
|
||||
if inst["family"] == "render" and inst["active"]:
|
||||
if inst["family"] in self.family_remapping.keys() \
|
||||
and inst["active"]:
|
||||
remapped_family = self.family_remapping[inst["family"]]
|
||||
instance = AERenderInstance(
|
||||
family="render.farm", # other way integrate would catch it
|
||||
families=["render.farm"],
|
||||
family=remapped_family[0],
|
||||
families=[remapped_family[0]],
|
||||
version=version,
|
||||
time="",
|
||||
source=current_file,
|
||||
label="{} - farm".format(inst["subset"]),
|
||||
label="{} - {}".format(inst["subset"], remapped_family[1]),
|
||||
subset=inst["subset"],
|
||||
asset=context.data["assetEntity"]["name"],
|
||||
attachTo=False,
|
||||
|
|
@ -86,7 +103,8 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
|
|||
frameStart=frameStart,
|
||||
frameEnd=frameEnd,
|
||||
frameStep=1,
|
||||
toBeRenderedOn='deadline'
|
||||
toBeRenderedOn='deadline',
|
||||
fps=fps
|
||||
)
|
||||
|
||||
comp = compositions_by_id.get(int(item_id))
|
||||
|
|
@ -100,9 +118,32 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
|
|||
|
||||
instance.outputDir = self._get_output_dir(instance)
|
||||
|
||||
settings = get_project_settings(os.getenv("AVALON_PROJECT"))
|
||||
reviewable_subset_filter = \
|
||||
(settings["deadline"]
|
||||
["publish"]
|
||||
["ProcessSubmittedJobOnFarm"]
|
||||
["aov_filter"])
|
||||
|
||||
if inst["family"] == "renderLocal":
|
||||
# for local renders
|
||||
instance.anatomyData["version"] = instance.version
|
||||
instance.anatomyData["subset"] = instance.subset
|
||||
instance.stagingDir = tempfile.mkdtemp()
|
||||
instance.projectEntity = project_entity
|
||||
|
||||
if self.hosts[0] in reviewable_subset_filter.keys():
|
||||
for aov_pattern in \
|
||||
reviewable_subset_filter[self.hosts[0]]:
|
||||
if re.match(aov_pattern, instance.subset):
|
||||
instance.families.append("review")
|
||||
instance.review = True
|
||||
break
|
||||
|
||||
self.log.info("New instance:: {}".format(instance))
|
||||
|
||||
instances.append(instance)
|
||||
|
||||
self.log.debug("instances::{}".format(instances))
|
||||
return instances
|
||||
|
||||
def get_expected_files(self, render_instance):
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
|
|||
"subset": subset,
|
||||
"label": scene_file,
|
||||
"family": family,
|
||||
"families": [family, "ftrack"],
|
||||
"families": [family],
|
||||
"representations": list()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
import os
|
||||
import six
|
||||
import sys
|
||||
|
||||
import openpype.api
|
||||
from avalon import aftereffects
|
||||
|
||||
|
||||
class ExtractLocalRender(openpype.api.Extractor):
|
||||
"""Render RenderQueue locally."""
|
||||
|
||||
order = openpype.api.Extractor.order - 0.47
|
||||
label = "Extract Local Render"
|
||||
hosts = ["aftereffects"]
|
||||
families = ["render"]
|
||||
|
||||
def process(self, instance):
|
||||
stub = aftereffects.stub()
|
||||
staging_dir = instance.data["stagingDir"]
|
||||
self.log.info("staging_dir::{}".format(staging_dir))
|
||||
|
||||
stub.render(staging_dir)
|
||||
|
||||
# pull file name from Render Queue Output module
|
||||
render_q = stub.get_render_info()
|
||||
if not render_q:
|
||||
raise ValueError("No file extension set in Render Queue")
|
||||
_, ext = os.path.splitext(os.path.basename(render_q.file_name))
|
||||
ext = ext[1:]
|
||||
|
||||
first_file_path = None
|
||||
files = []
|
||||
self.log.info("files::{}".format(os.listdir(staging_dir)))
|
||||
for file_name in os.listdir(staging_dir):
|
||||
files.append(file_name)
|
||||
if first_file_path is None:
|
||||
first_file_path = os.path.join(staging_dir,
|
||||
file_name)
|
||||
|
||||
resulting_files = files
|
||||
if len(files) == 1:
|
||||
resulting_files = files[0]
|
||||
|
||||
repre_data = {
|
||||
"frameStart": instance.data["frameStart"],
|
||||
"frameEnd": instance.data["frameEnd"],
|
||||
"name": ext,
|
||||
"ext": ext,
|
||||
"files": resulting_files,
|
||||
"stagingDir": staging_dir
|
||||
}
|
||||
if instance.data["review"]:
|
||||
repre_data["tags"] = ["review"]
|
||||
|
||||
instance.data["representations"] = [repre_data]
|
||||
|
||||
ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg")
|
||||
# Generate thumbnail.
|
||||
thumbnail_path = os.path.join(staging_dir,
|
||||
"thumbnail.jpg")
|
||||
|
||||
args = [
|
||||
ffmpeg_path, "-y",
|
||||
"-i", first_file_path,
|
||||
"-vf", "scale=300:-1",
|
||||
"-vframes", "1",
|
||||
thumbnail_path
|
||||
]
|
||||
self.log.debug("Thumbnail args:: {}".format(args))
|
||||
try:
|
||||
output = openpype.lib.run_subprocess(args)
|
||||
except TypeError:
|
||||
self.log.warning("Error in creating thumbnail")
|
||||
six.reraise(*sys.exc_info())
|
||||
|
||||
instance.data["representations"].append({
|
||||
"name": "thumbnail",
|
||||
"ext": "jpg",
|
||||
"files": os.path.basename(thumbnail_path),
|
||||
"stagingDir": staging_dir,
|
||||
"tags": ["thumbnail"]
|
||||
})
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
from avalon import api
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
from avalon import aftereffects
|
||||
|
||||
|
||||
class ValidateInstanceAssetRepair(pyblish.api.Action):
|
||||
"""Repair the instance asset with value from Context."""
|
||||
|
||||
label = "Repair"
|
||||
icon = "wrench"
|
||||
on = "failed"
|
||||
|
||||
def process(self, context, plugin):
|
||||
|
||||
# Get the errored instances
|
||||
failed = []
|
||||
for result in context.data["results"]:
|
||||
if (result["error"] is not None and result["instance"] is not None
|
||||
and result["instance"] not in failed):
|
||||
failed.append(result["instance"])
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(failed, plugin)
|
||||
stub = aftereffects.stub()
|
||||
for instance in instances:
|
||||
data = stub.read(instance[0])
|
||||
|
||||
data["asset"] = api.Session["AVALON_ASSET"]
|
||||
stub.imprint(instance[0], data)
|
||||
|
||||
|
||||
class ValidateInstanceAsset(pyblish.api.InstancePlugin):
|
||||
"""Validate the instance asset is the current selected context asset.
|
||||
|
||||
As it might happen that multiple worfiles are opened at same time,
|
||||
switching between them would mess with selected context. (From Launcher
|
||||
or Ftrack).
|
||||
|
||||
In that case outputs might be output under wrong asset!
|
||||
|
||||
Repair action will use Context asset value (from Workfiles or Launcher)
|
||||
Closing and reopening with Workfiles will refresh Context value.
|
||||
"""
|
||||
|
||||
label = "Validate Instance Asset"
|
||||
hosts = ["aftereffects"]
|
||||
actions = [ValidateInstanceAssetRepair]
|
||||
order = openpype.api.ValidateContentsOrder
|
||||
|
||||
def process(self, instance):
|
||||
instance_asset = instance.data["asset"]
|
||||
current_asset = api.Session["AVALON_ASSET"]
|
||||
msg = (
|
||||
f"Instance asset {instance_asset} is not the same "
|
||||
f"as current context {current_asset}. PLEASE DO:\n"
|
||||
f"Repair with 'A' action to use '{current_asset}'.\n"
|
||||
f"If that's not correct value, close workfile and "
|
||||
f"reopen via Workfiles!"
|
||||
)
|
||||
assert instance_asset == current_asset, msg
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validate scene settings."""
|
||||
import os
|
||||
import re
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from avalon import aftereffects
|
||||
|
||||
import openpype.hosts.aftereffects.api as api
|
||||
|
||||
stub = aftereffects.stub()
|
||||
|
||||
|
||||
class ValidateSceneSettings(pyblish.api.InstancePlugin):
|
||||
"""
|
||||
Ensures that Composition Settings (right mouse on comp) are same as
|
||||
in FTrack on task.
|
||||
|
||||
By default checks only duration - how many frames should be rendered.
|
||||
Compares:
|
||||
Frame start - Frame end + 1 from FTrack
|
||||
against
|
||||
Duration in Composition Settings.
|
||||
|
||||
If this complains:
|
||||
Check error message where is discrepancy.
|
||||
Check FTrack task 'pype' section of task attributes for expected
|
||||
values.
|
||||
Check/modify rendered Composition Settings.
|
||||
|
||||
If you know what you are doing run publishing again, uncheck this
|
||||
validation before Validation phase.
|
||||
"""
|
||||
|
||||
"""
|
||||
Dev docu:
|
||||
Could be configured by 'presets/plugins/aftereffects/publish'
|
||||
|
||||
skip_timelines_check - fill task name for which skip validation of
|
||||
frameStart
|
||||
frameEnd
|
||||
fps
|
||||
handleStart
|
||||
handleEnd
|
||||
skip_resolution_check - fill entity type ('asset') to skip validation
|
||||
resolutionWidth
|
||||
resolutionHeight
|
||||
TODO support in extension is missing for now
|
||||
|
||||
By defaults validates duration (how many frames should be published)
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Scene Settings"
|
||||
families = ["render.farm", "render"]
|
||||
hosts = ["aftereffects"]
|
||||
optional = True
|
||||
|
||||
skip_timelines_check = [".*"] # * >> skip for all
|
||||
skip_resolution_check = [".*"]
|
||||
|
||||
def process(self, instance):
|
||||
"""Plugin entry point."""
|
||||
expected_settings = api.get_asset_settings()
|
||||
self.log.info("config from DB::{}".format(expected_settings))
|
||||
|
||||
if any(re.search(pattern, os.getenv('AVALON_TASK'))
|
||||
for pattern in self.skip_resolution_check):
|
||||
expected_settings.pop("resolutionWidth")
|
||||
expected_settings.pop("resolutionHeight")
|
||||
|
||||
if any(re.search(pattern, os.getenv('AVALON_TASK'))
|
||||
for pattern in self.skip_timelines_check):
|
||||
expected_settings.pop('fps', None)
|
||||
expected_settings.pop('frameStart', None)
|
||||
expected_settings.pop('frameEnd', None)
|
||||
expected_settings.pop('handleStart', None)
|
||||
expected_settings.pop('handleEnd', None)
|
||||
|
||||
# handle case where ftrack uses only two decimal places
|
||||
# 23.976023976023978 vs. 23.98
|
||||
fps = instance.data.get("fps")
|
||||
if fps:
|
||||
if isinstance(fps, float):
|
||||
fps = float(
|
||||
"{:.2f}".format(fps))
|
||||
expected_settings["fps"] = fps
|
||||
|
||||
duration = instance.data.get("frameEndHandle") - \
|
||||
instance.data.get("frameStartHandle") + 1
|
||||
|
||||
self.log.debug("filtered config::{}".format(expected_settings))
|
||||
|
||||
current_settings = {
|
||||
"fps": fps,
|
||||
"frameStartHandle": instance.data.get("frameStartHandle"),
|
||||
"frameEndHandle": instance.data.get("frameEndHandle"),
|
||||
"resolutionWidth": instance.data.get("resolutionWidth"),
|
||||
"resolutionHeight": instance.data.get("resolutionHeight"),
|
||||
"duration": duration
|
||||
}
|
||||
self.log.info("current_settings:: {}".format(current_settings))
|
||||
|
||||
invalid_settings = []
|
||||
for key, value in expected_settings.items():
|
||||
if value != current_settings[key]:
|
||||
invalid_settings.append(
|
||||
"{} expected: {} found: {}".format(key, value,
|
||||
current_settings[key])
|
||||
)
|
||||
|
||||
if ((expected_settings.get("handleStart")
|
||||
or expected_settings.get("handleEnd"))
|
||||
and invalid_settings):
|
||||
msg = "Handles included in calculation. Remove handles in DB " +\
|
||||
"or extend frame range in Composition Setting."
|
||||
invalid_settings[-1]["reason"] = msg
|
||||
|
||||
msg = "Found invalid settings:\n{}".format(
|
||||
"\n".join(invalid_settings)
|
||||
)
|
||||
assert not invalid_settings, msg
|
||||
assert os.path.exists(instance.data.get("source")), (
|
||||
"Scene file not found (saved under wrong name)"
|
||||
)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import os
|
||||
|
||||
|
||||
def add_implementation_envs(env, _app):
|
||||
"""Modify environments to contain all required for implementation."""
|
||||
# Prepare path to implementation script
|
||||
implementation_user_script_path = os.path.join(
|
||||
os.environ["OPENPYPE_REPOS_ROOT"],
|
||||
"repos",
|
||||
"avalon-core",
|
||||
"setup",
|
||||
"blender"
|
||||
)
|
||||
|
||||
# Add blender implementation script path to PYTHONPATH
|
||||
python_path = env.get("PYTHONPATH") or ""
|
||||
python_path_parts = [
|
||||
path
|
||||
for path in python_path.split(os.pathsep)
|
||||
if path
|
||||
]
|
||||
python_path_parts.insert(0, implementation_user_script_path)
|
||||
env["PYTHONPATH"] = os.pathsep.join(python_path_parts)
|
||||
|
||||
# Modify Blender user scripts path
|
||||
previous_user_scripts = set()
|
||||
# Implementation path is added to set for easier paths check inside loops
|
||||
# - will be removed at the end
|
||||
previous_user_scripts.add(implementation_user_script_path)
|
||||
|
||||
openpype_blender_user_scripts = (
|
||||
env.get("OPENPYPE_BLENDER_USER_SCRIPTS") or ""
|
||||
)
|
||||
for path in openpype_blender_user_scripts.split(os.pathsep):
|
||||
if path and os.path.exists(path):
|
||||
previous_user_scripts.add(os.path.normpath(path))
|
||||
|
||||
blender_user_scripts = env.get("BLENDER_USER_SCRIPTS") or ""
|
||||
for path in blender_user_scripts.split(os.pathsep):
|
||||
if path and os.path.exists(path):
|
||||
previous_user_scripts.add(os.path.normpath(path))
|
||||
|
||||
# Remove implementation path from user script paths as is set to
|
||||
# `BLENDER_USER_SCRIPTS`
|
||||
previous_user_scripts.remove(implementation_user_script_path)
|
||||
env["BLENDER_USER_SCRIPTS"] = implementation_user_script_path
|
||||
|
||||
# Set custom user scripts env
|
||||
env["OPENPYPE_BLENDER_USER_SCRIPTS"] = os.pathsep.join(
|
||||
previous_user_scripts
|
||||
)
|
||||
|
||||
# Define Qt binding if not defined
|
||||
if not env.get("QT_PREFERRED_BINDING"):
|
||||
env["QT_PREFERRED_BINDING"] = "PySide2"
|
||||
|
|
@ -4,6 +4,8 @@ import traceback
|
|||
|
||||
import bpy
|
||||
|
||||
from .lib import append_user_scripts
|
||||
|
||||
from avalon import api as avalon
|
||||
from pyblish import api as pyblish
|
||||
|
||||
|
|
@ -29,7 +31,7 @@ def install():
|
|||
pyblish.register_plugin_path(str(PUBLISH_PATH))
|
||||
avalon.register_plugin_path(avalon.Loader, str(LOAD_PATH))
|
||||
avalon.register_plugin_path(avalon.Creator, str(CREATE_PATH))
|
||||
|
||||
append_user_scripts()
|
||||
avalon.on("new", on_new)
|
||||
avalon.on("open", on_open)
|
||||
|
||||
|
|
|
|||
127
openpype/hosts/blender/api/lib.py
Normal file
127
openpype/hosts/blender/api/lib.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import os
|
||||
import traceback
|
||||
import importlib
|
||||
|
||||
import bpy
|
||||
import addon_utils
|
||||
|
||||
|
||||
def load_scripts(paths):
|
||||
"""Copy of `load_scripts` from Blender's implementation.
|
||||
|
||||
It is possible that whis function will be changed in future and usage will
|
||||
be based on Blender version.
|
||||
"""
|
||||
import bpy_types
|
||||
|
||||
loaded_modules = set()
|
||||
|
||||
previous_classes = [
|
||||
cls
|
||||
for cls in bpy.types.bpy_struct.__subclasses__()
|
||||
]
|
||||
|
||||
def register_module_call(mod):
|
||||
register = getattr(mod, "register", None)
|
||||
if register:
|
||||
try:
|
||||
register()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
else:
|
||||
print("\nWarning! '%s' has no register function, "
|
||||
"this is now a requirement for registerable scripts" %
|
||||
mod.__file__)
|
||||
|
||||
def unregister_module_call(mod):
|
||||
unregister = getattr(mod, "unregister", None)
|
||||
if unregister:
|
||||
try:
|
||||
unregister()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
def test_reload(mod):
|
||||
# reloading this causes internal errors
|
||||
# because the classes from this module are stored internally
|
||||
# possibly to refresh internal references too but for now, best not to.
|
||||
if mod == bpy_types:
|
||||
return mod
|
||||
|
||||
try:
|
||||
return importlib.reload(mod)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
def test_register(mod):
|
||||
if mod:
|
||||
register_module_call(mod)
|
||||
bpy.utils._global_loaded_modules.append(mod.__name__)
|
||||
|
||||
from bpy_restrict_state import RestrictBlend
|
||||
|
||||
with RestrictBlend():
|
||||
for base_path in paths:
|
||||
for path_subdir in bpy.utils._script_module_dirs:
|
||||
path = os.path.join(base_path, path_subdir)
|
||||
if not os.path.isdir(path):
|
||||
continue
|
||||
|
||||
bpy.utils._sys_path_ensure_prepend(path)
|
||||
|
||||
# Only add to 'sys.modules' unless this is 'startup'.
|
||||
if path_subdir != "startup":
|
||||
continue
|
||||
for mod in bpy.utils.modules_from_path(path, loaded_modules):
|
||||
test_register(mod)
|
||||
|
||||
addons_paths = []
|
||||
for base_path in paths:
|
||||
addons_path = os.path.join(base_path, "addons")
|
||||
if not os.path.exists(addons_path):
|
||||
continue
|
||||
addons_paths.append(addons_path)
|
||||
addons_module_path = os.path.join(addons_path, "modules")
|
||||
if os.path.exists(addons_module_path):
|
||||
bpy.utils._sys_path_ensure_prepend(addons_module_path)
|
||||
|
||||
if addons_paths:
|
||||
# Fake addons
|
||||
origin_paths = addon_utils.paths
|
||||
|
||||
def new_paths():
|
||||
paths = origin_paths() + addons_paths
|
||||
return paths
|
||||
|
||||
addon_utils.paths = new_paths
|
||||
addon_utils.modules_refresh()
|
||||
|
||||
# load template (if set)
|
||||
if any(bpy.utils.app_template_paths()):
|
||||
import bl_app_template_utils
|
||||
bl_app_template_utils.reset(reload_scripts=False)
|
||||
del bl_app_template_utils
|
||||
|
||||
for cls in bpy.types.bpy_struct.__subclasses__():
|
||||
if cls in previous_classes:
|
||||
continue
|
||||
if not getattr(cls, "is_registered", False):
|
||||
continue
|
||||
for subcls in cls.__subclasses__():
|
||||
if not subcls.is_registered:
|
||||
print(
|
||||
"Warning, unregistered class: %s(%s)" %
|
||||
(subcls.__name__, cls.__name__)
|
||||
)
|
||||
|
||||
|
||||
def append_user_scripts():
|
||||
user_scripts = os.environ.get("OPENPYPE_BLENDER_USER_SCRIPTS")
|
||||
if not user_scripts:
|
||||
return
|
||||
|
||||
try:
|
||||
load_scripts(user_scripts.split(os.pathsep))
|
||||
except Exception:
|
||||
print("Couldn't load user scripts \"{}\"".format(user_scripts))
|
||||
traceback.print_exc()
|
||||
|
|
@ -5,11 +5,12 @@ from typing import Dict, List, Optional
|
|||
|
||||
import bpy
|
||||
|
||||
from avalon import api
|
||||
import avalon.blender
|
||||
from avalon import api, blender
|
||||
from avalon.blender import ops
|
||||
from avalon.blender.pipeline import AVALON_CONTAINERS
|
||||
from openpype.api import PypeCreatorMixin
|
||||
|
||||
VALID_EXTENSIONS = [".blend", ".json"]
|
||||
VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx"]
|
||||
|
||||
|
||||
def asset_name(
|
||||
|
|
@ -27,32 +28,24 @@ def get_unique_number(
|
|||
asset: str, subset: str
|
||||
) -> str:
|
||||
"""Return a unique number based on the asset name."""
|
||||
avalon_containers = [
|
||||
c for c in bpy.data.collections
|
||||
if c.name == 'AVALON_CONTAINERS'
|
||||
]
|
||||
containers = []
|
||||
# First, add the children of avalon containers
|
||||
for c in avalon_containers:
|
||||
containers.extend(c.children)
|
||||
# then keep looping to include all the children
|
||||
for c in containers:
|
||||
containers.extend(c.children)
|
||||
container_names = [
|
||||
c.name for c in containers
|
||||
]
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
if not avalon_container:
|
||||
return "01"
|
||||
asset_groups = avalon_container.all_objects
|
||||
|
||||
container_names = [c.name for c in asset_groups if c.type == 'EMPTY']
|
||||
count = 1
|
||||
name = f"{asset}_{count:0>2}_{subset}_CON"
|
||||
name = f"{asset}_{count:0>2}_{subset}"
|
||||
while name in container_names:
|
||||
count += 1
|
||||
name = f"{asset}_{count:0>2}_{subset}_CON"
|
||||
name = f"{asset}_{count:0>2}_{subset}"
|
||||
return f"{count:0>2}"
|
||||
|
||||
|
||||
def prepare_data(data, container_name):
|
||||
name = data.name
|
||||
local_data = data.make_local()
|
||||
local_data.name = f"{name}:{container_name}"
|
||||
local_data.name = f"{container_name}:{name}"
|
||||
return local_data
|
||||
|
||||
|
||||
|
|
@ -102,7 +95,7 @@ def get_local_collection_with_name(name):
|
|||
return None
|
||||
|
||||
|
||||
class Creator(PypeCreatorMixin, avalon.blender.Creator):
|
||||
class Creator(PypeCreatorMixin, blender.Creator):
|
||||
pass
|
||||
|
||||
|
||||
|
|
@ -173,6 +166,16 @@ class AssetLoader(api.Loader):
|
|||
name: Optional[str] = None,
|
||||
namespace: Optional[str] = None,
|
||||
options: Optional[Dict] = None) -> Optional[bpy.types.Collection]:
|
||||
""" Run the loader on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._load, context, name, namespace, options)
|
||||
ops.execute_in_main_thread(mti)
|
||||
|
||||
def _load(self,
|
||||
context: dict,
|
||||
name: Optional[str] = None,
|
||||
namespace: Optional[str] = None,
|
||||
options: Optional[Dict] = None
|
||||
) -> Optional[bpy.types.Collection]:
|
||||
"""Load asset via database
|
||||
|
||||
Arguments:
|
||||
|
|
@ -218,16 +221,26 @@ class AssetLoader(api.Loader):
|
|||
# loader=self.__class__.__name__,
|
||||
# )
|
||||
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
instance_name = asset_name(asset, subset, unique_number) + '_CON'
|
||||
# asset = context["asset"]["name"]
|
||||
# subset = context["subset"]["name"]
|
||||
# instance_name = asset_name(asset, subset, unique_number) + '_CON'
|
||||
|
||||
return self._get_instance_collection(instance_name, nodes)
|
||||
# return self._get_instance_collection(instance_name, nodes)
|
||||
|
||||
def exec_update(self, container: Dict, representation: Dict):
|
||||
"""Must be implemented by a sub-class"""
|
||||
raise NotImplementedError("Must be implemented by a sub-class")
|
||||
|
||||
def update(self, container: Dict, representation: Dict):
|
||||
""" Run the update on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self.exec_update, container, representation)
|
||||
ops.execute_in_main_thread(mti)
|
||||
|
||||
def exec_remove(self, container: Dict) -> bool:
|
||||
"""Must be implemented by a sub-class"""
|
||||
raise NotImplementedError("Must be implemented by a sub-class")
|
||||
|
||||
def remove(self, container: Dict) -> bool:
|
||||
"""Must be implemented by a sub-class"""
|
||||
raise NotImplementedError("Must be implemented by a sub-class")
|
||||
""" Run the remove on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self.exec_remove, container)
|
||||
ops.execute_in_main_thread(mti)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from openpype.lib import PreLaunchHook
|
||||
|
||||
|
|
@ -31,10 +32,46 @@ class InstallPySideToBlender(PreLaunchHook):
|
|||
|
||||
def inner_execute(self):
|
||||
# Get blender's python directory
|
||||
version_regex = re.compile(r"^2\.[0-9]{2}$")
|
||||
|
||||
executable = self.launch_context.executable.executable_path
|
||||
# Blender installation contain subfolder named with it's version where
|
||||
# python binaries are stored.
|
||||
version_subfolder = self.launch_context.app_name.split("_")[1]
|
||||
if os.path.basename(executable).lower() != "blender.exe":
|
||||
self.log.info((
|
||||
"Executable does not lead to blender.exe file. Can't determine"
|
||||
" blender's python to check/install PySide2."
|
||||
))
|
||||
return
|
||||
|
||||
executable_dir = os.path.dirname(executable)
|
||||
version_subfolders = []
|
||||
for name in os.listdir(executable_dir):
|
||||
fullpath = os.path.join(name, executable_dir)
|
||||
if not os.path.isdir(fullpath):
|
||||
continue
|
||||
|
||||
if not version_regex.match(name):
|
||||
continue
|
||||
|
||||
version_subfolders.append(name)
|
||||
|
||||
if not version_subfolders:
|
||||
self.log.info(
|
||||
"Didn't find version subfolder next to Blender executable"
|
||||
)
|
||||
return
|
||||
|
||||
if len(version_subfolders) > 1:
|
||||
self.log.info((
|
||||
"Found more than one version subfolder next"
|
||||
" to blender executable. {}"
|
||||
).format(", ".join([
|
||||
'"./{}"'.format(name)
|
||||
for name in version_subfolders
|
||||
])))
|
||||
return
|
||||
|
||||
version_subfolder = version_subfolders[0]
|
||||
|
||||
pythond_dir = os.path.join(
|
||||
os.path.dirname(executable),
|
||||
version_subfolder,
|
||||
|
|
@ -65,6 +102,7 @@ class InstallPySideToBlender(PreLaunchHook):
|
|||
|
||||
# Check if PySide2 is installed and skip if yes
|
||||
if self.is_pyside_installed(python_executable):
|
||||
self.log.debug("Blender has already installed PySide2.")
|
||||
return
|
||||
|
||||
# Install PySide2 in blender's python
|
||||
|
|
|
|||
28
openpype/hosts/blender/hooks/pre_windows_console.py
Normal file
28
openpype/hosts/blender/hooks/pre_windows_console.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import subprocess
|
||||
from openpype.lib import PreLaunchHook
|
||||
|
||||
|
||||
class BlenderConsoleWindows(PreLaunchHook):
|
||||
"""Foundry applications have specific way how to launch them.
|
||||
|
||||
Blender 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
|
||||
and stderr handlers so they should not be redirected to DEVNULL.
|
||||
"""
|
||||
|
||||
# Should be as last hook because must change launch arguments to string
|
||||
order = 1000
|
||||
app_groups = ["blender"]
|
||||
platforms = ["windows"]
|
||||
|
||||
def execute(self):
|
||||
# Change `creationflags` to CREATE_NEW_CONSOLE
|
||||
# - on Windows will blender create new window using it's 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({
|
||||
"creationflags": subprocess.CREATE_NEW_CONSOLE,
|
||||
"stdout": None,
|
||||
"stderr": None
|
||||
})
|
||||
|
|
@ -2,11 +2,13 @@
|
|||
|
||||
import bpy
|
||||
|
||||
from avalon import api, blender
|
||||
import openpype.hosts.blender.api.plugin
|
||||
from avalon import api
|
||||
from avalon.blender import lib, ops
|
||||
from avalon.blender.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
class CreateAnimation(openpype.hosts.blender.api.plugin.Creator):
|
||||
class CreateAnimation(plugin.Creator):
|
||||
"""Animation output for character rigs"""
|
||||
|
||||
name = "animationMain"
|
||||
|
|
@ -15,16 +17,36 @@ class CreateAnimation(openpype.hosts.blender.api.plugin.Creator):
|
|||
icon = "male"
|
||||
|
||||
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 Containter 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
|
||||
# name = self.name
|
||||
# if not name:
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
|
||||
collection = bpy.data.collections.new(name=name)
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
name = plugin.asset_name(asset, subset)
|
||||
# asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
# asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
asset_group = bpy.data.collections.new(name=name)
|
||||
instances.children.link(asset_group)
|
||||
self.data['task'] = api.Session.get('AVALON_TASK')
|
||||
blender.lib.imprint(collection, self.data)
|
||||
lib.imprint(asset_group, self.data)
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
for obj in blender.lib.get_selection():
|
||||
collection.objects.link(obj)
|
||||
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 collection
|
||||
return asset_group
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@
|
|||
import bpy
|
||||
|
||||
from avalon import api
|
||||
from avalon.blender import lib
|
||||
import openpype.hosts.blender.api.plugin
|
||||
from avalon.blender import lib, ops
|
||||
from avalon.blender.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
class CreateLayout(openpype.hosts.blender.api.plugin.Creator):
|
||||
class CreateLayout(plugin.Creator):
|
||||
"""Layout output for character rigs"""
|
||||
|
||||
name = "layoutMain"
|
||||
|
|
@ -16,25 +17,34 @@ class CreateLayout(openpype.hosts.blender.api.plugin.Creator):
|
|||
icon = "cubes"
|
||||
|
||||
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 Containter 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 = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
|
||||
collection = bpy.data.collections.new(name=name)
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(asset_group)
|
||||
self.data['task'] = api.Session.get('AVALON_TASK')
|
||||
lib.imprint(collection, self.data)
|
||||
|
||||
# Add the rig object and all the children meshes to
|
||||
# a set and link them all at the end to avoid duplicates.
|
||||
# Blender crashes if trying to link an object that is already linked.
|
||||
# This links automatically the children meshes if they were not
|
||||
# selected, and doesn't link them twice if they, insted,
|
||||
# were manually selected by the user.
|
||||
objects_to_link = set()
|
||||
lib.imprint(asset_group, self.data)
|
||||
|
||||
# Add selected objects to instance
|
||||
if (self.options or {}).get("useSelection"):
|
||||
for obj in lib.get_selection():
|
||||
collection.children.link(obj.users_collection[0])
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
obj.select_set(True)
|
||||
selected.append(asset_group)
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
|
||||
return collection
|
||||
return asset_group
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@
|
|||
import bpy
|
||||
|
||||
from avalon import api
|
||||
from avalon.blender import lib
|
||||
import openpype.hosts.blender.api.plugin
|
||||
from avalon.blender import lib, ops
|
||||
from avalon.blender.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
class CreateModel(openpype.hosts.blender.api.plugin.Creator):
|
||||
class CreateModel(plugin.Creator):
|
||||
"""Polygonal static geometry"""
|
||||
|
||||
name = "modelMain"
|
||||
|
|
@ -16,17 +17,34 @@ class CreateModel(openpype.hosts.blender.api.plugin.Creator):
|
|||
icon = "cube"
|
||||
|
||||
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 Containter 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 = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
|
||||
collection = bpy.data.collections.new(name=name)
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(asset_group)
|
||||
self.data['task'] = api.Session.get('AVALON_TASK')
|
||||
lib.imprint(collection, self.data)
|
||||
lib.imprint(asset_group, self.data)
|
||||
|
||||
# Add selected objects to instance
|
||||
if (self.options or {}).get("useSelection"):
|
||||
for obj in lib.get_selection():
|
||||
collection.objects.link(obj)
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
obj.select_set(True)
|
||||
selected.append(asset_group)
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
|
||||
return collection
|
||||
return asset_group
|
||||
|
|
|
|||
35
openpype/hosts/blender/plugins/create/create_pointcache.py
Normal file
35
openpype/hosts/blender/plugins/create/create_pointcache.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
"""Create a pointcache asset."""
|
||||
|
||||
import bpy
|
||||
|
||||
from avalon import api
|
||||
from avalon.blender import lib
|
||||
import openpype.hosts.blender.api.plugin
|
||||
|
||||
|
||||
class CreatePointcache(openpype.hosts.blender.api.plugin.Creator):
|
||||
"""Polygonal static geometry"""
|
||||
|
||||
name = "pointcacheMain"
|
||||
label = "Point Cache"
|
||||
family = "pointcache"
|
||||
icon = "gears"
|
||||
|
||||
def process(self):
|
||||
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
|
||||
collection = bpy.data.collections.new(name=name)
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
self.data['task'] = api.Session.get('AVALON_TASK')
|
||||
lib.imprint(collection, self.data)
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
objects = lib.get_selection()
|
||||
for obj in objects:
|
||||
collection.objects.link(obj)
|
||||
if obj.type == 'EMPTY':
|
||||
objects.extend(obj.children)
|
||||
|
||||
return collection
|
||||
|
|
@ -3,11 +3,12 @@
|
|||
import bpy
|
||||
|
||||
from avalon import api
|
||||
from avalon.blender import lib
|
||||
import openpype.hosts.blender.api.plugin
|
||||
from avalon.blender import lib, ops
|
||||
from avalon.blender.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
class CreateRig(openpype.hosts.blender.api.plugin.Creator):
|
||||
class CreateRig(plugin.Creator):
|
||||
"""Artist-friendly rig with controls to direct motion"""
|
||||
|
||||
name = "rigMain"
|
||||
|
|
@ -16,26 +17,34 @@ class CreateRig(openpype.hosts.blender.api.plugin.Creator):
|
|||
icon = "wheelchair"
|
||||
|
||||
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 Containter 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 = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
|
||||
collection = bpy.data.collections.new(name=name)
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(asset_group)
|
||||
self.data['task'] = api.Session.get('AVALON_TASK')
|
||||
lib.imprint(collection, self.data)
|
||||
|
||||
# Add the rig object and all the children meshes to
|
||||
# a set and link them all at the end to avoid duplicates.
|
||||
# Blender crashes if trying to link an object that is already linked.
|
||||
# This links automatically the children meshes if they were not
|
||||
# selected, and doesn't link them twice if they, insted,
|
||||
# were manually selected by the user.
|
||||
lib.imprint(asset_group, self.data)
|
||||
|
||||
# Add selected objects to instance
|
||||
if (self.options or {}).get("useSelection"):
|
||||
for obj in lib.get_selection():
|
||||
for child in obj.users_collection[0].children:
|
||||
collection.children.link(child)
|
||||
collection.objects.link(obj)
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
obj.select_set(True)
|
||||
selected.append(asset_group)
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
|
||||
return collection
|
||||
return asset_group
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
import bpy
|
||||
|
||||
from avalon import api, blender
|
||||
import openpype.hosts.blender.api.plugin
|
||||
|
||||
|
||||
class CreateSetDress(openpype.hosts.blender.api.plugin.Creator):
|
||||
"""A grouped package of loaded content"""
|
||||
|
||||
name = "setdressMain"
|
||||
label = "Set Dress"
|
||||
family = "setdress"
|
||||
icon = "cubes"
|
||||
defaults = ["Main", "Anim"]
|
||||
|
||||
def process(self):
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
|
||||
collection = bpy.data.collections.new(name=name)
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
self.data['task'] = api.Session.get('AVALON_TASK')
|
||||
blender.lib.imprint(collection, self.data)
|
||||
|
||||
return collection
|
||||
252
openpype/hosts/blender/plugins/load/load_abc.py
Normal file
252
openpype/hosts/blender/plugins/load/load_abc.py
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
"""Load an asset in Blender from an Alembic file."""
|
||||
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import bpy
|
||||
|
||||
from avalon import api
|
||||
from avalon.blender import lib
|
||||
from avalon.blender.pipeline import AVALON_CONTAINERS
|
||||
from avalon.blender.pipeline import AVALON_CONTAINER_ID
|
||||
from avalon.blender.pipeline import AVALON_PROPERTY
|
||||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
class CacheModelLoader(plugin.AssetLoader):
|
||||
"""Load cache models.
|
||||
|
||||
Stores the imported asset in a collection named after the asset.
|
||||
|
||||
Note:
|
||||
At least for now it only supports Alembic files.
|
||||
"""
|
||||
|
||||
families = ["model", "pointcache"]
|
||||
representations = ["abc"]
|
||||
|
||||
label = "Load Alembic"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def _remove(self, asset_group):
|
||||
objects = list(asset_group.children)
|
||||
empties = []
|
||||
|
||||
for obj in objects:
|
||||
if obj.type == 'MESH':
|
||||
for material_slot in list(obj.material_slots):
|
||||
bpy.data.materials.remove(material_slot.material)
|
||||
bpy.data.meshes.remove(obj.data)
|
||||
elif obj.type == 'EMPTY':
|
||||
objects.extend(obj.children)
|
||||
empties.append(obj)
|
||||
|
||||
for empty in empties:
|
||||
bpy.data.objects.remove(empty)
|
||||
|
||||
def _process(self, libpath, asset_group, group_name):
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
collection = bpy.context.view_layer.active_layer_collection.collection
|
||||
|
||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||
bpy.ops.wm.alembic_import(
|
||||
filepath=libpath,
|
||||
relative_path=relative
|
||||
)
|
||||
|
||||
parent = bpy.context.scene.collection
|
||||
|
||||
imported = lib.get_selection()
|
||||
|
||||
empties = [obj for obj in imported if obj.type == 'EMPTY']
|
||||
|
||||
container = None
|
||||
|
||||
for empty in empties:
|
||||
if not empty.parent:
|
||||
container = empty
|
||||
break
|
||||
|
||||
assert container, "No asset group found"
|
||||
|
||||
# Children must be linked before parents,
|
||||
# otherwise the hierarchy will break
|
||||
objects = []
|
||||
nodes = list(container.children)
|
||||
|
||||
for obj in nodes:
|
||||
obj.parent = asset_group
|
||||
|
||||
bpy.data.objects.remove(container)
|
||||
|
||||
for obj in nodes:
|
||||
objects.append(obj)
|
||||
nodes.extend(list(obj.children))
|
||||
|
||||
objects.reverse()
|
||||
|
||||
for obj in objects:
|
||||
parent.objects.link(obj)
|
||||
collection.objects.unlink(obj)
|
||||
|
||||
for obj in objects:
|
||||
name = obj.name
|
||||
obj.name = f"{group_name}:{name}"
|
||||
if obj.type != 'EMPTY':
|
||||
name_data = obj.data.name
|
||||
obj.data.name = f"{group_name}:{name_data}"
|
||||
|
||||
for material_slot in obj.material_slots:
|
||||
name_mat = material_slot.material.name
|
||||
material_slot.material.name = f"{group_name}:{name_mat}"
|
||||
|
||||
if not obj.get(AVALON_PROPERTY):
|
||||
obj[AVALON_PROPERTY] = dict()
|
||||
|
||||
avalon_info = obj[AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": group_name})
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
return objects
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
options: Optional[Dict] = None
|
||||
) -> Optional[List]:
|
||||
"""
|
||||
Arguments:
|
||||
name: Use pre-defined name
|
||||
namespace: Use pre-defined namespace
|
||||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
|
||||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
if not avalon_container:
|
||||
avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
|
||||
bpy.context.scene.collection.children.link(avalon_container)
|
||||
|
||||
asset_group = bpy.data.objects.new(group_name, object_data=None)
|
||||
avalon_container.objects.link(asset_group)
|
||||
|
||||
objects = self._process(libpath, asset_group, group_name)
|
||||
|
||||
bpy.context.scene.collection.objects.link(asset_group)
|
||||
|
||||
asset_group[AVALON_PROPERTY] = {
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": AVALON_CONTAINER_ID,
|
||||
"name": name,
|
||||
"namespace": namespace or '',
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": str(context["representation"]["_id"]),
|
||||
"libpath": libpath,
|
||||
"asset_name": asset_name,
|
||||
"parent": str(context["representation"]["parent"]),
|
||||
"family": context["representation"]["context"]["family"],
|
||||
"objectName": group_name
|
||||
}
|
||||
|
||||
self[:] = objects
|
||||
return objects
|
||||
|
||||
def exec_update(self, container: Dict, representation: Dict):
|
||||
"""Update the loaded asset.
|
||||
|
||||
This will remove all objects of the current collection, load the new
|
||||
ones and add them to the collection.
|
||||
If the objects of the collection are used in another collection they
|
||||
will not be removed, only unlinked. Normally this should not be the
|
||||
case though.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
object_name = container["objectName"]
|
||||
asset_group = bpy.data.objects.get(object_name)
|
||||
libpath = Path(api.get_representation_path(representation))
|
||||
extension = libpath.suffix.lower()
|
||||
|
||||
self.log.info(
|
||||
"Container: %s\nRepresentation: %s",
|
||||
pformat(container, indent=2),
|
||||
pformat(representation, indent=2),
|
||||
)
|
||||
|
||||
assert asset_group, (
|
||||
f"The asset is not loaded: {container['objectName']}"
|
||||
)
|
||||
assert libpath, (
|
||||
"No existing library file found for {container['objectName']}"
|
||||
)
|
||||
assert libpath.is_file(), (
|
||||
f"The file doesn't exist: {libpath}"
|
||||
)
|
||||
assert extension in plugin.VALID_EXTENSIONS, (
|
||||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
metadata = asset_group.get(AVALON_PROPERTY)
|
||||
group_libpath = metadata["libpath"]
|
||||
|
||||
normalized_group_libpath = (
|
||||
str(Path(bpy.path.abspath(group_libpath)).resolve())
|
||||
)
|
||||
normalized_libpath = (
|
||||
str(Path(bpy.path.abspath(str(libpath))).resolve())
|
||||
)
|
||||
self.log.debug(
|
||||
"normalized_group_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_group_libpath,
|
||||
normalized_libpath,
|
||||
)
|
||||
if normalized_group_libpath == normalized_libpath:
|
||||
self.log.info("Library already loaded, not updating...")
|
||||
return
|
||||
|
||||
mat = asset_group.matrix_basis.copy()
|
||||
self._remove(asset_group)
|
||||
|
||||
self._process(str(libpath), asset_group, object_name)
|
||||
asset_group.matrix_basis = mat
|
||||
|
||||
metadata["libpath"] = str(libpath)
|
||||
metadata["representation"] = str(representation["_id"])
|
||||
|
||||
def exec_remove(self, container: Dict) -> bool:
|
||||
"""Remove an existing container from a Blender scene.
|
||||
|
||||
Arguments:
|
||||
container (openpype:container-1.0): Container to remove,
|
||||
from `host.ls()`.
|
||||
|
||||
Returns:
|
||||
bool: Whether the container was deleted.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
object_name = container["objectName"]
|
||||
asset_group = bpy.data.objects.get(object_name)
|
||||
|
||||
if not asset_group:
|
||||
return False
|
||||
|
||||
self._remove(asset_group)
|
||||
|
||||
bpy.data.objects.remove(asset_group)
|
||||
|
||||
return True
|
||||
|
|
@ -1,20 +1,19 @@
|
|||
"""Load an animation in Blender."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from avalon import api, blender
|
||||
import bpy
|
||||
import openpype.hosts.blender.api.plugin
|
||||
|
||||
from avalon.blender.pipeline import AVALON_PROPERTY
|
||||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
logger = logging.getLogger("openpype").getChild(
|
||||
"blender").getChild("load_animation")
|
||||
|
||||
|
||||
class BlendAnimationLoader(openpype.hosts.blender.api.plugin.AssetLoader):
|
||||
class BlendAnimationLoader(plugin.AssetLoader):
|
||||
"""Load animations from a .blend file.
|
||||
|
||||
Warning:
|
||||
|
|
@ -29,67 +28,6 @@ class BlendAnimationLoader(openpype.hosts.blender.api.plugin.AssetLoader):
|
|||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def _remove(self, objects, lib_container):
|
||||
for obj in list(objects):
|
||||
if obj.type == 'ARMATURE':
|
||||
bpy.data.armatures.remove(obj.data)
|
||||
elif obj.type == 'MESH':
|
||||
bpy.data.meshes.remove(obj.data)
|
||||
|
||||
bpy.data.collections.remove(bpy.data.collections[lib_container])
|
||||
|
||||
def _process(self, libpath, lib_container, container_name):
|
||||
|
||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||
with bpy.data.libraries.load(
|
||||
libpath, link=True, relative=relative
|
||||
) as (_, data_to):
|
||||
data_to.collections = [lib_container]
|
||||
|
||||
scene = bpy.context.scene
|
||||
|
||||
scene.collection.children.link(bpy.data.collections[lib_container])
|
||||
|
||||
anim_container = scene.collection.children[lib_container].make_local()
|
||||
|
||||
meshes = [obj for obj in anim_container.objects if obj.type == 'MESH']
|
||||
armatures = [
|
||||
obj for obj in anim_container.objects if obj.type == 'ARMATURE']
|
||||
|
||||
# Should check if there is only an armature?
|
||||
|
||||
objects_list = []
|
||||
|
||||
# Link meshes first, then armatures.
|
||||
# The armature is unparented for all the non-local meshes,
|
||||
# when it is made local.
|
||||
for obj in meshes + armatures:
|
||||
|
||||
obj = obj.make_local()
|
||||
|
||||
obj.data.make_local()
|
||||
|
||||
anim_data = obj.animation_data
|
||||
|
||||
if anim_data is not None and anim_data.action is not None:
|
||||
|
||||
anim_data.action.make_local()
|
||||
|
||||
if not obj.get(blender.pipeline.AVALON_PROPERTY):
|
||||
|
||||
obj[blender.pipeline.AVALON_PROPERTY] = dict()
|
||||
|
||||
avalon_info = obj[blender.pipeline.AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": container_name})
|
||||
|
||||
objects_list.append(obj)
|
||||
|
||||
anim_container.pop(blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
return objects_list
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
options: Optional[Dict] = None
|
||||
|
|
@ -101,148 +39,32 @@ class BlendAnimationLoader(openpype.hosts.blender.api.plugin.AssetLoader):
|
|||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
|
||||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
lib_container = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
|
||||
container_name = openpype.hosts.blender.api.plugin.asset_name(
|
||||
asset, subset, namespace
|
||||
)
|
||||
|
||||
container = bpy.data.collections.new(lib_container)
|
||||
container.name = container_name
|
||||
blender.pipeline.containerise_existing(
|
||||
container,
|
||||
name,
|
||||
namespace,
|
||||
context,
|
||||
self.__class__.__name__,
|
||||
)
|
||||
with bpy.data.libraries.load(
|
||||
libpath, link=True, relative=False
|
||||
) as (data_from, data_to):
|
||||
data_to.objects = data_from.objects
|
||||
data_to.actions = data_from.actions
|
||||
|
||||
container_metadata = container.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
container = data_to.objects[0]
|
||||
|
||||
container_metadata["libpath"] = libpath
|
||||
container_metadata["lib_container"] = lib_container
|
||||
assert container, "No asset group found"
|
||||
|
||||
objects_list = self._process(
|
||||
libpath, lib_container, container_name)
|
||||
target_namespace = container.get(AVALON_PROPERTY).get('namespace')
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
container_metadata["objects"] = objects_list
|
||||
action = data_to.actions[0].make_local().copy()
|
||||
|
||||
nodes = list(container.objects)
|
||||
nodes.append(container)
|
||||
self[:] = nodes
|
||||
return nodes
|
||||
for obj in bpy.data.objects:
|
||||
if obj.get(AVALON_PROPERTY) and obj.get(AVALON_PROPERTY).get(
|
||||
'namespace') == target_namespace:
|
||||
if obj.children[0]:
|
||||
if not obj.children[0].animation_data:
|
||||
obj.children[0].animation_data_create()
|
||||
obj.children[0].animation_data.action = action
|
||||
break
|
||||
|
||||
def update(self, container: Dict, representation: Dict):
|
||||
"""Update the loaded asset.
|
||||
bpy.data.objects.remove(container)
|
||||
|
||||
This will remove all objects of the current collection, load the new
|
||||
ones and add them to the collection.
|
||||
If the objects of the collection are used in another collection they
|
||||
will not be removed, only unlinked. Normally this should not be the
|
||||
case though.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
|
||||
collection = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
|
||||
libpath = Path(api.get_representation_path(representation))
|
||||
extension = libpath.suffix.lower()
|
||||
|
||||
logger.info(
|
||||
"Container: %s\nRepresentation: %s",
|
||||
pformat(container, indent=2),
|
||||
pformat(representation, indent=2),
|
||||
)
|
||||
|
||||
assert collection, (
|
||||
f"The asset is not loaded: {container['objectName']}"
|
||||
)
|
||||
assert not (collection.children), (
|
||||
"Nested collections are not supported."
|
||||
)
|
||||
assert libpath, (
|
||||
"No existing library file found for {container['objectName']}"
|
||||
)
|
||||
assert libpath.is_file(), (
|
||||
f"The file doesn't exist: {libpath}"
|
||||
)
|
||||
assert extension in openpype.hosts.blender.api.plugin.VALID_EXTENSIONS, (
|
||||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
collection_libpath = collection_metadata["libpath"]
|
||||
normalized_collection_libpath = (
|
||||
str(Path(bpy.path.abspath(collection_libpath)).resolve())
|
||||
)
|
||||
normalized_libpath = (
|
||||
str(Path(bpy.path.abspath(str(libpath))).resolve())
|
||||
)
|
||||
logger.debug(
|
||||
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_collection_libpath,
|
||||
normalized_libpath,
|
||||
)
|
||||
if normalized_collection_libpath == normalized_libpath:
|
||||
logger.info("Library already loaded, not updating...")
|
||||
return
|
||||
|
||||
objects = collection_metadata["objects"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
|
||||
self._remove(objects, lib_container)
|
||||
|
||||
objects_list = self._process(
|
||||
str(libpath), lib_container, collection.name)
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
collection_metadata["objects"] = objects_list
|
||||
collection_metadata["libpath"] = str(libpath)
|
||||
collection_metadata["representation"] = str(representation["_id"])
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
def remove(self, container: Dict) -> bool:
|
||||
"""Remove an existing container from a Blender scene.
|
||||
|
||||
Arguments:
|
||||
container (openpype:container-1.0): Container to remove,
|
||||
from `host.ls()`.
|
||||
|
||||
Returns:
|
||||
bool: Whether the container was deleted.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
|
||||
collection = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
if not collection:
|
||||
return False
|
||||
assert not (collection.children), (
|
||||
"Nested collections are not supported."
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
objects = collection_metadata["objects"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
|
||||
self._remove(objects, lib_container)
|
||||
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
return True
|
||||
library = bpy.data.libraries.get(bpy.path.basename(libpath))
|
||||
bpy.data.libraries.remove(library)
|
||||
|
|
|
|||
273
openpype/hosts/blender/plugins/load/load_fbx.py
Normal file
273
openpype/hosts/blender/plugins/load/load_fbx.py
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
"""Load an asset in Blender from an Alembic file."""
|
||||
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import bpy
|
||||
|
||||
from avalon import api
|
||||
from avalon.blender import lib
|
||||
from avalon.blender.pipeline import AVALON_CONTAINERS
|
||||
from avalon.blender.pipeline import AVALON_CONTAINER_ID
|
||||
from avalon.blender.pipeline import AVALON_PROPERTY
|
||||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
class FbxModelLoader(plugin.AssetLoader):
|
||||
"""Load FBX models.
|
||||
|
||||
Stores the imported asset in an empty named after the asset.
|
||||
"""
|
||||
|
||||
families = ["model", "rig"]
|
||||
representations = ["fbx"]
|
||||
|
||||
label = "Load FBX"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def _remove(self, asset_group):
|
||||
objects = list(asset_group.children)
|
||||
|
||||
for obj in objects:
|
||||
if obj.type == 'MESH':
|
||||
for material_slot in list(obj.material_slots):
|
||||
if material_slot.material:
|
||||
bpy.data.materials.remove(material_slot.material)
|
||||
bpy.data.meshes.remove(obj.data)
|
||||
elif obj.type == 'ARMATURE':
|
||||
objects.extend(obj.children)
|
||||
bpy.data.armatures.remove(obj.data)
|
||||
elif obj.type == 'CURVE':
|
||||
bpy.data.curves.remove(obj.data)
|
||||
elif obj.type == 'EMPTY':
|
||||
objects.extend(obj.children)
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
def _process(self, libpath, asset_group, group_name, action):
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
collection = bpy.context.view_layer.active_layer_collection.collection
|
||||
|
||||
bpy.ops.import_scene.fbx(filepath=libpath)
|
||||
|
||||
parent = bpy.context.scene.collection
|
||||
|
||||
imported = lib.get_selection()
|
||||
|
||||
empties = [obj for obj in imported if obj.type == 'EMPTY']
|
||||
|
||||
container = None
|
||||
|
||||
for empty in empties:
|
||||
if not empty.parent:
|
||||
container = empty
|
||||
break
|
||||
|
||||
assert container, "No asset group found"
|
||||
|
||||
# Children must be linked before parents,
|
||||
# otherwise the hierarchy will break
|
||||
objects = []
|
||||
nodes = list(container.children)
|
||||
|
||||
for obj in nodes:
|
||||
obj.parent = asset_group
|
||||
|
||||
bpy.data.objects.remove(container)
|
||||
|
||||
for obj in nodes:
|
||||
objects.append(obj)
|
||||
nodes.extend(list(obj.children))
|
||||
|
||||
objects.reverse()
|
||||
|
||||
for obj in objects:
|
||||
parent.objects.link(obj)
|
||||
collection.objects.unlink(obj)
|
||||
|
||||
for obj in objects:
|
||||
name = obj.name
|
||||
obj.name = f"{group_name}:{name}"
|
||||
if obj.type != 'EMPTY':
|
||||
name_data = obj.data.name
|
||||
obj.data.name = f"{group_name}:{name_data}"
|
||||
|
||||
if obj.type == 'MESH':
|
||||
for material_slot in obj.material_slots:
|
||||
name_mat = material_slot.material.name
|
||||
material_slot.material.name = f"{group_name}:{name_mat}"
|
||||
elif obj.type == 'ARMATURE':
|
||||
anim_data = obj.animation_data
|
||||
if action is not None:
|
||||
anim_data.action = action
|
||||
elif anim_data.action is not None:
|
||||
name_action = anim_data.action.name
|
||||
anim_data.action.name = f"{group_name}:{name_action}"
|
||||
|
||||
if not obj.get(AVALON_PROPERTY):
|
||||
obj[AVALON_PROPERTY] = dict()
|
||||
|
||||
avalon_info = obj[AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": group_name})
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
return objects
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
options: Optional[Dict] = None
|
||||
) -> Optional[List]:
|
||||
"""
|
||||
Arguments:
|
||||
name: Use pre-defined name
|
||||
namespace: Use pre-defined namespace
|
||||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
if not avalon_container:
|
||||
avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
|
||||
bpy.context.scene.collection.children.link(avalon_container)
|
||||
|
||||
asset_group = bpy.data.objects.new(group_name, object_data=None)
|
||||
avalon_container.objects.link(asset_group)
|
||||
|
||||
objects = self._process(libpath, asset_group, group_name, None)
|
||||
|
||||
objects = []
|
||||
nodes = list(asset_group.children)
|
||||
|
||||
for obj in nodes:
|
||||
objects.append(obj)
|
||||
nodes.extend(list(obj.children))
|
||||
|
||||
bpy.context.scene.collection.objects.link(asset_group)
|
||||
|
||||
asset_group[AVALON_PROPERTY] = {
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": AVALON_CONTAINER_ID,
|
||||
"name": name,
|
||||
"namespace": namespace or '',
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": str(context["representation"]["_id"]),
|
||||
"libpath": libpath,
|
||||
"asset_name": asset_name,
|
||||
"parent": str(context["representation"]["parent"]),
|
||||
"family": context["representation"]["context"]["family"],
|
||||
"objectName": group_name
|
||||
}
|
||||
|
||||
self[:] = objects
|
||||
return objects
|
||||
|
||||
def exec_update(self, container: Dict, representation: Dict):
|
||||
"""Update the loaded asset.
|
||||
|
||||
This will remove all objects of the current collection, load the new
|
||||
ones and add them to the collection.
|
||||
If the objects of the collection are used in another collection they
|
||||
will not be removed, only unlinked. Normally this should not be the
|
||||
case though.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
object_name = container["objectName"]
|
||||
asset_group = bpy.data.objects.get(object_name)
|
||||
libpath = Path(api.get_representation_path(representation))
|
||||
extension = libpath.suffix.lower()
|
||||
|
||||
self.log.info(
|
||||
"Container: %s\nRepresentation: %s",
|
||||
pformat(container, indent=2),
|
||||
pformat(representation, indent=2),
|
||||
)
|
||||
|
||||
assert asset_group, (
|
||||
f"The asset is not loaded: {container['objectName']}"
|
||||
)
|
||||
assert libpath, (
|
||||
"No existing library file found for {container['objectName']}"
|
||||
)
|
||||
assert libpath.is_file(), (
|
||||
f"The file doesn't exist: {libpath}"
|
||||
)
|
||||
assert extension in plugin.VALID_EXTENSIONS, (
|
||||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
metadata = asset_group.get(AVALON_PROPERTY)
|
||||
group_libpath = metadata["libpath"]
|
||||
|
||||
normalized_group_libpath = (
|
||||
str(Path(bpy.path.abspath(group_libpath)).resolve())
|
||||
)
|
||||
normalized_libpath = (
|
||||
str(Path(bpy.path.abspath(str(libpath))).resolve())
|
||||
)
|
||||
self.log.debug(
|
||||
"normalized_group_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_group_libpath,
|
||||
normalized_libpath,
|
||||
)
|
||||
if normalized_group_libpath == normalized_libpath:
|
||||
self.log.info("Library already loaded, not updating...")
|
||||
return
|
||||
|
||||
# Get the armature of the rig
|
||||
objects = asset_group.children
|
||||
armatures = [obj for obj in objects if obj.type == 'ARMATURE']
|
||||
action = None
|
||||
|
||||
if armatures:
|
||||
armature = armatures[0]
|
||||
|
||||
if armature.animation_data and armature.animation_data.action:
|
||||
action = armature.animation_data.action
|
||||
|
||||
mat = asset_group.matrix_basis.copy()
|
||||
self._remove(asset_group)
|
||||
|
||||
self._process(str(libpath), asset_group, object_name, action)
|
||||
|
||||
asset_group.matrix_basis = mat
|
||||
|
||||
metadata["libpath"] = str(libpath)
|
||||
metadata["representation"] = str(representation["_id"])
|
||||
|
||||
def exec_remove(self, container: Dict) -> bool:
|
||||
"""Remove an existing container from a Blender scene.
|
||||
|
||||
Arguments:
|
||||
container (openpype:container-1.0): Container to remove,
|
||||
from `host.ls()`.
|
||||
|
||||
Returns:
|
||||
bool: Whether the container was deleted.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
object_name = container["objectName"]
|
||||
asset_group = bpy.data.objects.get(object_name)
|
||||
|
||||
if not asset_group:
|
||||
return False
|
||||
|
||||
self._remove(asset_group)
|
||||
|
||||
bpy.data.objects.remove(asset_group)
|
||||
|
||||
return True
|
||||
|
|
@ -1,677 +0,0 @@
|
|||
"""Load a layout in Blender."""
|
||||
|
||||
import json
|
||||
from logging import log, warning
|
||||
import math
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from avalon import api, blender, pipeline
|
||||
import bpy
|
||||
import openpype.hosts.blender.api.plugin as plugin
|
||||
from openpype.lib import get_creator_by_name
|
||||
|
||||
|
||||
class BlendLayoutLoader(plugin.AssetLoader):
|
||||
"""Load layout from a .blend file."""
|
||||
|
||||
families = ["layout"]
|
||||
representations = ["blend"]
|
||||
|
||||
label = "Link Layout"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
animation_creator_name = "CreateAnimation"
|
||||
setdress_creator_name = "CreateSetDress"
|
||||
|
||||
def _remove(self, objects, obj_container):
|
||||
for obj in list(objects):
|
||||
if obj.type == 'ARMATURE':
|
||||
bpy.data.armatures.remove(obj.data)
|
||||
elif obj.type == 'MESH':
|
||||
bpy.data.meshes.remove(obj.data)
|
||||
elif obj.type == 'CAMERA':
|
||||
bpy.data.cameras.remove(obj.data)
|
||||
elif obj.type == 'CURVE':
|
||||
bpy.data.curves.remove(obj.data)
|
||||
|
||||
for element_container in obj_container.children:
|
||||
for child in element_container.children:
|
||||
bpy.data.collections.remove(child)
|
||||
bpy.data.collections.remove(element_container)
|
||||
|
||||
bpy.data.collections.remove(obj_container)
|
||||
|
||||
def _process(self, libpath, lib_container, container_name, actions):
|
||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||
with bpy.data.libraries.load(
|
||||
libpath, link=True, relative=relative
|
||||
) as (_, data_to):
|
||||
data_to.collections = [lib_container]
|
||||
|
||||
scene = bpy.context.scene
|
||||
|
||||
scene.collection.children.link(bpy.data.collections[lib_container])
|
||||
|
||||
layout_container = scene.collection.children[lib_container].make_local()
|
||||
layout_container.name = container_name
|
||||
|
||||
objects_local_types = ['MESH', 'CAMERA', 'CURVE']
|
||||
|
||||
objects = []
|
||||
armatures = []
|
||||
|
||||
containers = list(layout_container.children)
|
||||
|
||||
for container in layout_container.children:
|
||||
if container.name == blender.pipeline.AVALON_CONTAINERS:
|
||||
containers.remove(container)
|
||||
|
||||
for container in containers:
|
||||
container.make_local()
|
||||
objects.extend([
|
||||
obj for obj in container.objects
|
||||
if obj.type in objects_local_types
|
||||
])
|
||||
armatures.extend([
|
||||
obj for obj in container.objects
|
||||
if obj.type == 'ARMATURE'
|
||||
])
|
||||
containers.extend(list(container.children))
|
||||
|
||||
# Link meshes first, then armatures.
|
||||
# The armature is unparented for all the non-local meshes,
|
||||
# when it is made local.
|
||||
for obj in objects + armatures:
|
||||
local_obj = obj.make_local()
|
||||
if obj.data:
|
||||
obj.data.make_local()
|
||||
|
||||
if not local_obj.get(blender.pipeline.AVALON_PROPERTY):
|
||||
local_obj[blender.pipeline.AVALON_PROPERTY] = dict()
|
||||
|
||||
avalon_info = local_obj[blender.pipeline.AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": container_name})
|
||||
|
||||
action = actions.get(local_obj.name, None)
|
||||
|
||||
if local_obj.type == 'ARMATURE' and action is not None:
|
||||
local_obj.animation_data.action = action
|
||||
|
||||
layout_container.pop(blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
return layout_container
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
options: Optional[Dict] = None
|
||||
) -> Optional[List]:
|
||||
"""
|
||||
Arguments:
|
||||
name: Use pre-defined name
|
||||
namespace: Use pre-defined namespace
|
||||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
|
||||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
lib_container = plugin.asset_name(
|
||||
asset, subset
|
||||
)
|
||||
unique_number = plugin.get_unique_number(
|
||||
asset, subset
|
||||
)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
container_name = plugin.asset_name(
|
||||
asset, subset, unique_number
|
||||
)
|
||||
|
||||
container = bpy.data.collections.new(lib_container)
|
||||
container.name = container_name
|
||||
blender.pipeline.containerise_existing(
|
||||
container,
|
||||
name,
|
||||
namespace,
|
||||
context,
|
||||
self.__class__.__name__,
|
||||
)
|
||||
|
||||
container_metadata = container.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
container_metadata["libpath"] = libpath
|
||||
container_metadata["lib_container"] = lib_container
|
||||
|
||||
obj_container = self._process(
|
||||
libpath, lib_container, container_name, {})
|
||||
|
||||
container_metadata["obj_container"] = obj_container
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
container_metadata["objects"] = obj_container.all_objects
|
||||
|
||||
# nodes = list(container.objects)
|
||||
# nodes.append(container)
|
||||
nodes = [container]
|
||||
self[:] = nodes
|
||||
return nodes
|
||||
|
||||
def update(self, container: Dict, representation: Dict):
|
||||
"""Update the loaded asset.
|
||||
|
||||
This will remove all objects of the current collection, load the new
|
||||
ones and add them to the collection.
|
||||
If the objects of the collection are used in another collection they
|
||||
will not be removed, only unlinked. Normally this should not be the
|
||||
case though.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
collection = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
|
||||
libpath = Path(api.get_representation_path(representation))
|
||||
extension = libpath.suffix.lower()
|
||||
|
||||
self.log.info(
|
||||
"Container: %s\nRepresentation: %s",
|
||||
pformat(container, indent=2),
|
||||
pformat(representation, indent=2),
|
||||
)
|
||||
|
||||
assert collection, (
|
||||
f"The asset is not loaded: {container['objectName']}"
|
||||
)
|
||||
assert not (collection.children), (
|
||||
"Nested collections are not supported."
|
||||
)
|
||||
assert libpath, (
|
||||
"No existing library file found for {container['objectName']}"
|
||||
)
|
||||
assert libpath.is_file(), (
|
||||
f"The file doesn't exist: {libpath}"
|
||||
)
|
||||
assert extension in plugin.VALID_EXTENSIONS, (
|
||||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
collection_libpath = collection_metadata["libpath"]
|
||||
objects = collection_metadata["objects"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
obj_container = collection_metadata["obj_container"]
|
||||
|
||||
normalized_collection_libpath = (
|
||||
str(Path(bpy.path.abspath(collection_libpath)).resolve())
|
||||
)
|
||||
normalized_libpath = (
|
||||
str(Path(bpy.path.abspath(str(libpath))).resolve())
|
||||
)
|
||||
self.log.debug(
|
||||
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_collection_libpath,
|
||||
normalized_libpath,
|
||||
)
|
||||
if normalized_collection_libpath == normalized_libpath:
|
||||
self.log.info("Library already loaded, not updating...")
|
||||
return
|
||||
|
||||
actions = {}
|
||||
|
||||
for obj in objects:
|
||||
if obj.type == 'ARMATURE':
|
||||
if obj.animation_data and obj.animation_data.action:
|
||||
actions[obj.name] = obj.animation_data.action
|
||||
|
||||
self._remove(objects, obj_container)
|
||||
|
||||
obj_container = self._process(
|
||||
str(libpath), lib_container, collection.name, actions)
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
collection_metadata["obj_container"] = obj_container
|
||||
collection_metadata["objects"] = obj_container.all_objects
|
||||
collection_metadata["libpath"] = str(libpath)
|
||||
collection_metadata["representation"] = str(representation["_id"])
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
def remove(self, container: Dict) -> bool:
|
||||
"""Remove an existing container from a Blender scene.
|
||||
|
||||
Arguments:
|
||||
container (openpype:container-1.0): Container to remove,
|
||||
from `host.ls()`.
|
||||
|
||||
Returns:
|
||||
bool: Whether the container was deleted.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
|
||||
collection = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
if not collection:
|
||||
return False
|
||||
assert not (collection.children), (
|
||||
"Nested collections are not supported."
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
objects = collection_metadata["objects"]
|
||||
obj_container = collection_metadata["obj_container"]
|
||||
|
||||
self._remove(objects, obj_container)
|
||||
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class UnrealLayoutLoader(plugin.AssetLoader):
|
||||
"""Load layout published from Unreal."""
|
||||
|
||||
families = ["layout"]
|
||||
representations = ["json"]
|
||||
|
||||
label = "Link Layout"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
animation_creator_name = "CreateAnimation"
|
||||
setdress_creator_name = "CreateSetDress"
|
||||
|
||||
def _remove_objects(self, objects):
|
||||
for obj in list(objects):
|
||||
if obj.type == 'ARMATURE':
|
||||
bpy.data.armatures.remove(obj.data)
|
||||
elif obj.type == 'MESH':
|
||||
bpy.data.meshes.remove(obj.data)
|
||||
elif obj.type == 'CAMERA':
|
||||
bpy.data.cameras.remove(obj.data)
|
||||
elif obj.type == 'CURVE':
|
||||
bpy.data.curves.remove(obj.data)
|
||||
else:
|
||||
self.log.error(
|
||||
f"Object {obj.name} of type {obj.type} not recognized.")
|
||||
|
||||
def _remove_collections(self, collection):
|
||||
if collection.children:
|
||||
for child in collection.children:
|
||||
self._remove_collections(child)
|
||||
bpy.data.collections.remove(child)
|
||||
|
||||
def _remove(self, layout_container):
|
||||
layout_container_metadata = layout_container.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
if layout_container.children:
|
||||
for child in layout_container.children:
|
||||
child_container = child.get(blender.pipeline.AVALON_PROPERTY)
|
||||
child_container['objectName'] = child.name
|
||||
api.remove(child_container)
|
||||
|
||||
for c in bpy.data.collections:
|
||||
metadata = c.get('avalon')
|
||||
if metadata:
|
||||
print("metadata.get('id')")
|
||||
print(metadata.get('id'))
|
||||
if metadata and metadata.get('id') == 'pyblish.avalon.instance':
|
||||
print("metadata.get('dependencies')")
|
||||
print(metadata.get('dependencies'))
|
||||
print("layout_container_metadata.get('representation')")
|
||||
print(layout_container_metadata.get('representation'))
|
||||
if metadata.get('dependencies') == layout_container_metadata.get('representation'):
|
||||
|
||||
for child in c.children:
|
||||
bpy.data.collections.remove(child)
|
||||
bpy.data.collections.remove(c)
|
||||
break
|
||||
|
||||
def _get_loader(self, loaders, family):
|
||||
name = ""
|
||||
if family == 'rig':
|
||||
name = "BlendRigLoader"
|
||||
elif family == 'model':
|
||||
name = "BlendModelLoader"
|
||||
|
||||
if name == "":
|
||||
return None
|
||||
|
||||
for loader in loaders:
|
||||
if loader.__name__ == name:
|
||||
return loader
|
||||
|
||||
return None
|
||||
|
||||
def set_transform(self, obj, transform):
|
||||
location = transform.get('translation')
|
||||
rotation = transform.get('rotation')
|
||||
scale = transform.get('scale')
|
||||
|
||||
# Y position is inverted in sign because Unreal and Blender have the
|
||||
# Y axis mirrored
|
||||
obj.location = (
|
||||
location.get('x'),
|
||||
-location.get('y'),
|
||||
location.get('z')
|
||||
)
|
||||
obj.rotation_euler = (
|
||||
rotation.get('x') + math.pi / 2,
|
||||
-rotation.get('y'),
|
||||
-rotation.get('z')
|
||||
)
|
||||
obj.scale = (
|
||||
scale.get('x'),
|
||||
scale.get('y'),
|
||||
scale.get('z')
|
||||
)
|
||||
|
||||
def _process(
|
||||
self, libpath, layout_container, container_name, representation,
|
||||
actions, parent
|
||||
):
|
||||
with open(libpath, "r") as fp:
|
||||
data = json.load(fp)
|
||||
|
||||
scene = bpy.context.scene
|
||||
layout_collection = bpy.data.collections.new(container_name)
|
||||
scene.collection.children.link(layout_collection)
|
||||
|
||||
all_loaders = api.discover(api.Loader)
|
||||
|
||||
avalon_container = bpy.data.collections.get(
|
||||
blender.pipeline.AVALON_CONTAINERS)
|
||||
|
||||
for element in data:
|
||||
reference = element.get('reference')
|
||||
family = element.get('family')
|
||||
|
||||
loaders = api.loaders_from_representation(all_loaders, reference)
|
||||
loader = self._get_loader(loaders, family)
|
||||
|
||||
if not loader:
|
||||
continue
|
||||
|
||||
instance_name = element.get('instance_name')
|
||||
|
||||
element_container = api.load(
|
||||
loader,
|
||||
reference,
|
||||
namespace=instance_name
|
||||
)
|
||||
|
||||
if not element_container:
|
||||
continue
|
||||
|
||||
avalon_container.children.unlink(element_container)
|
||||
layout_container.children.link(element_container)
|
||||
|
||||
element_metadata = element_container.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
# Unlink the object's collection from the scene collection and
|
||||
# link it in the layout collection
|
||||
element_collection = element_metadata.get('obj_container')
|
||||
scene.collection.children.unlink(element_collection)
|
||||
layout_collection.children.link(element_collection)
|
||||
|
||||
objects = element_metadata.get('objects')
|
||||
element_metadata['instance_name'] = instance_name
|
||||
|
||||
objects_to_transform = []
|
||||
|
||||
creator_plugin = get_creator_by_name(self.animation_creator_name)
|
||||
if not creator_plugin:
|
||||
raise ValueError("Creator plugin \"{}\" was not found.".format(
|
||||
self.animation_creator_name
|
||||
))
|
||||
|
||||
if family == 'rig':
|
||||
for o in objects:
|
||||
if o.type == 'ARMATURE':
|
||||
objects_to_transform.append(o)
|
||||
# Create an animation subset for each rig
|
||||
o.select_set(True)
|
||||
asset = api.Session["AVALON_ASSET"]
|
||||
c = api.create(
|
||||
creator_plugin,
|
||||
name="animation_" + element_collection.name,
|
||||
asset=asset,
|
||||
options={"useSelection": True},
|
||||
data={"dependencies": representation})
|
||||
scene.collection.children.unlink(c)
|
||||
parent.children.link(c)
|
||||
o.select_set(False)
|
||||
break
|
||||
elif family == 'model':
|
||||
objects_to_transform = objects
|
||||
|
||||
for o in objects_to_transform:
|
||||
self.set_transform(o, element.get('transform'))
|
||||
|
||||
if actions:
|
||||
if o.type == 'ARMATURE':
|
||||
action = actions.get(instance_name, None)
|
||||
|
||||
if action:
|
||||
if o.animation_data is None:
|
||||
o.animation_data_create()
|
||||
o.animation_data.action = action
|
||||
|
||||
return layout_collection
|
||||
|
||||
def process_asset(self,
|
||||
context: dict,
|
||||
name: str,
|
||||
namespace: Optional[str] = None,
|
||||
options: Optional[Dict] = None):
|
||||
"""
|
||||
Arguments:
|
||||
name: Use pre-defined name
|
||||
namespace: Use pre-defined namespace
|
||||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
lib_container = plugin.asset_name(
|
||||
asset, subset
|
||||
)
|
||||
unique_number = plugin.get_unique_number(
|
||||
asset, subset
|
||||
)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
container_name = plugin.asset_name(
|
||||
asset, subset, unique_number
|
||||
)
|
||||
|
||||
layout_container = bpy.data.collections.new(container_name)
|
||||
blender.pipeline.containerise_existing(
|
||||
layout_container,
|
||||
name,
|
||||
namespace,
|
||||
context,
|
||||
self.__class__.__name__,
|
||||
)
|
||||
|
||||
container_metadata = layout_container.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
container_metadata["libpath"] = libpath
|
||||
container_metadata["lib_container"] = lib_container
|
||||
|
||||
# Create a setdress subset to contain all the animation for all
|
||||
# the rigs in the layout
|
||||
creator_plugin = get_creator_by_name(self.setdress_creator_name)
|
||||
if not creator_plugin:
|
||||
raise ValueError("Creator plugin \"{}\" was not found.".format(
|
||||
self.setdress_creator_name
|
||||
))
|
||||
parent = api.create(
|
||||
creator_plugin,
|
||||
name="animation",
|
||||
asset=api.Session["AVALON_ASSET"],
|
||||
options={"useSelection": True},
|
||||
data={"dependencies": str(context["representation"]["_id"])})
|
||||
|
||||
layout_collection = self._process(
|
||||
libpath, layout_container, container_name,
|
||||
str(context["representation"]["_id"]), None, parent)
|
||||
|
||||
container_metadata["obj_container"] = layout_collection
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
container_metadata["objects"] = layout_collection.all_objects
|
||||
|
||||
nodes = [layout_container]
|
||||
self[:] = nodes
|
||||
return nodes
|
||||
|
||||
def update(self, container: Dict, representation: Dict):
|
||||
"""Update the loaded asset.
|
||||
|
||||
This will remove all objects of the current collection, load the new
|
||||
ones and add them to the collection.
|
||||
If the objects of the collection are used in another collection they
|
||||
will not be removed, only unlinked. Normally this should not be the
|
||||
case though.
|
||||
"""
|
||||
layout_container = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
if not layout_container:
|
||||
return False
|
||||
|
||||
libpath = Path(api.get_representation_path(representation))
|
||||
extension = libpath.suffix.lower()
|
||||
|
||||
self.log.info(
|
||||
"Container: %s\nRepresentation: %s",
|
||||
pformat(container, indent=2),
|
||||
pformat(representation, indent=2),
|
||||
)
|
||||
|
||||
assert layout_container, (
|
||||
f"The asset is not loaded: {container['objectName']}"
|
||||
)
|
||||
assert libpath, (
|
||||
"No existing library file found for {container['objectName']}"
|
||||
)
|
||||
assert libpath.is_file(), (
|
||||
f"The file doesn't exist: {libpath}"
|
||||
)
|
||||
assert extension in plugin.VALID_EXTENSIONS, (
|
||||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
layout_container_metadata = layout_container.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
collection_libpath = layout_container_metadata["libpath"]
|
||||
lib_container = layout_container_metadata["lib_container"]
|
||||
obj_container = plugin.get_local_collection_with_name(
|
||||
layout_container_metadata["obj_container"].name
|
||||
)
|
||||
objects = obj_container.all_objects
|
||||
|
||||
container_name = obj_container.name
|
||||
|
||||
normalized_collection_libpath = (
|
||||
str(Path(bpy.path.abspath(collection_libpath)).resolve())
|
||||
)
|
||||
normalized_libpath = (
|
||||
str(Path(bpy.path.abspath(str(libpath))).resolve())
|
||||
)
|
||||
self.log.debug(
|
||||
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_collection_libpath,
|
||||
normalized_libpath,
|
||||
)
|
||||
if normalized_collection_libpath == normalized_libpath:
|
||||
self.log.info("Library already loaded, not updating...")
|
||||
return
|
||||
|
||||
actions = {}
|
||||
|
||||
for obj in objects:
|
||||
if obj.type == 'ARMATURE':
|
||||
if obj.animation_data and obj.animation_data.action:
|
||||
obj_cont_name = obj.get(
|
||||
blender.pipeline.AVALON_PROPERTY).get('container_name')
|
||||
obj_cont = plugin.get_local_collection_with_name(
|
||||
obj_cont_name)
|
||||
element_metadata = obj_cont.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
instance_name = element_metadata.get('instance_name')
|
||||
actions[instance_name] = obj.animation_data.action
|
||||
|
||||
self._remove(layout_container)
|
||||
|
||||
bpy.data.collections.remove(obj_container)
|
||||
|
||||
creator_plugin = get_creator_by_name(self.setdress_creator_name)
|
||||
if not creator_plugin:
|
||||
raise ValueError("Creator plugin \"{}\" was not found.".format(
|
||||
self.setdress_creator_name
|
||||
))
|
||||
|
||||
parent = api.create(
|
||||
creator_plugin,
|
||||
name="animation",
|
||||
asset=api.Session["AVALON_ASSET"],
|
||||
options={"useSelection": True},
|
||||
data={"dependencies": str(representation["_id"])})
|
||||
|
||||
layout_collection = self._process(
|
||||
libpath, layout_container, container_name,
|
||||
str(representation["_id"]), actions, parent)
|
||||
|
||||
layout_container_metadata["obj_container"] = layout_collection
|
||||
layout_container_metadata["objects"] = layout_collection.all_objects
|
||||
layout_container_metadata["libpath"] = str(libpath)
|
||||
layout_container_metadata["representation"] = str(
|
||||
representation["_id"])
|
||||
|
||||
def remove(self, container: Dict) -> bool:
|
||||
"""Remove an existing container from a Blender scene.
|
||||
|
||||
Arguments:
|
||||
container (openpype:container-1.0): Container to remove,
|
||||
from `host.ls()`.
|
||||
|
||||
Returns:
|
||||
bool: Whether the container was deleted.
|
||||
"""
|
||||
layout_container = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
if not layout_container:
|
||||
return False
|
||||
|
||||
layout_container_metadata = layout_container.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
obj_container = plugin.get_local_collection_with_name(
|
||||
layout_container_metadata["obj_container"].name
|
||||
)
|
||||
|
||||
self._remove(layout_container)
|
||||
|
||||
bpy.data.collections.remove(obj_container)
|
||||
bpy.data.collections.remove(layout_container)
|
||||
|
||||
return True
|
||||
337
openpype/hosts/blender/plugins/load/load_layout_blend.py
Normal file
337
openpype/hosts/blender/plugins/load/load_layout_blend.py
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
"""Load a layout in Blender."""
|
||||
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import bpy
|
||||
|
||||
from avalon import api
|
||||
from avalon.blender.pipeline import AVALON_CONTAINERS
|
||||
from avalon.blender.pipeline import AVALON_CONTAINER_ID
|
||||
from avalon.blender.pipeline import AVALON_PROPERTY
|
||||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
class BlendLayoutLoader(plugin.AssetLoader):
|
||||
"""Load layout from a .blend file."""
|
||||
|
||||
families = ["layout"]
|
||||
representations = ["blend"]
|
||||
|
||||
label = "Link Layout"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def _remove(self, asset_group):
|
||||
objects = list(asset_group.children)
|
||||
|
||||
for obj in objects:
|
||||
if obj.type == 'MESH':
|
||||
for material_slot in list(obj.material_slots):
|
||||
if material_slot.material:
|
||||
bpy.data.materials.remove(material_slot.material)
|
||||
bpy.data.meshes.remove(obj.data)
|
||||
elif obj.type == 'ARMATURE':
|
||||
objects.extend(obj.children)
|
||||
bpy.data.armatures.remove(obj.data)
|
||||
elif obj.type == 'CURVE':
|
||||
bpy.data.curves.remove(obj.data)
|
||||
elif obj.type == 'EMPTY':
|
||||
objects.extend(obj.children)
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
def _remove_asset_and_library(self, asset_group):
|
||||
libpath = asset_group.get(AVALON_PROPERTY).get('libpath')
|
||||
|
||||
# Check how many assets use the same library
|
||||
count = 0
|
||||
for obj in bpy.data.collections.get(AVALON_CONTAINERS).all_objects:
|
||||
if obj.get(AVALON_PROPERTY).get('libpath') == libpath:
|
||||
count += 1
|
||||
|
||||
self._remove(asset_group)
|
||||
|
||||
bpy.data.objects.remove(asset_group)
|
||||
|
||||
# If it is the last object to use that library, remove it
|
||||
if count == 1:
|
||||
library = bpy.data.libraries.get(bpy.path.basename(libpath))
|
||||
bpy.data.libraries.remove(library)
|
||||
|
||||
def _process(self, libpath, asset_group, group_name, actions):
|
||||
with bpy.data.libraries.load(
|
||||
libpath, link=True, relative=False
|
||||
) as (data_from, data_to):
|
||||
data_to.objects = data_from.objects
|
||||
|
||||
parent = bpy.context.scene.collection
|
||||
|
||||
empties = [obj for obj in data_to.objects if obj.type == 'EMPTY']
|
||||
|
||||
container = None
|
||||
|
||||
for empty in empties:
|
||||
if empty.get(AVALON_PROPERTY):
|
||||
container = empty
|
||||
break
|
||||
|
||||
assert container, "No asset group found"
|
||||
|
||||
# Children must be linked before parents,
|
||||
# otherwise the hierarchy will break
|
||||
objects = []
|
||||
nodes = list(container.children)
|
||||
|
||||
for obj in nodes:
|
||||
obj.parent = asset_group
|
||||
|
||||
for obj in nodes:
|
||||
objects.append(obj)
|
||||
nodes.extend(list(obj.children))
|
||||
|
||||
objects.reverse()
|
||||
|
||||
constraints = []
|
||||
|
||||
armatures = [obj for obj in objects if obj.type == 'ARMATURE']
|
||||
|
||||
for armature in armatures:
|
||||
for bone in armature.pose.bones:
|
||||
for constraint in bone.constraints:
|
||||
if hasattr(constraint, 'target'):
|
||||
constraints.append(constraint)
|
||||
|
||||
for obj in objects:
|
||||
parent.objects.link(obj)
|
||||
|
||||
for obj in objects:
|
||||
local_obj = plugin.prepare_data(obj, group_name)
|
||||
|
||||
action = None
|
||||
|
||||
if actions:
|
||||
action = actions.get(local_obj.name, None)
|
||||
|
||||
if local_obj.type == 'MESH':
|
||||
plugin.prepare_data(local_obj.data, group_name)
|
||||
|
||||
if obj != local_obj:
|
||||
for constraint in constraints:
|
||||
if constraint.target == obj:
|
||||
constraint.target = local_obj
|
||||
|
||||
for material_slot in local_obj.material_slots:
|
||||
if material_slot.material:
|
||||
plugin.prepare_data(material_slot.material, group_name)
|
||||
elif local_obj.type == 'ARMATURE':
|
||||
plugin.prepare_data(local_obj.data, group_name)
|
||||
|
||||
if action is not None:
|
||||
local_obj.animation_data.action = action
|
||||
elif local_obj.animation_data.action is not None:
|
||||
plugin.prepare_data(
|
||||
local_obj.animation_data.action, group_name)
|
||||
|
||||
# Set link the drivers to the local object
|
||||
if local_obj.data.animation_data:
|
||||
for d in local_obj.data.animation_data.drivers:
|
||||
for v in d.driver.variables:
|
||||
for t in v.targets:
|
||||
t.id = local_obj
|
||||
|
||||
if not local_obj.get(AVALON_PROPERTY):
|
||||
local_obj[AVALON_PROPERTY] = dict()
|
||||
|
||||
avalon_info = local_obj[AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": group_name})
|
||||
|
||||
objects.reverse()
|
||||
|
||||
bpy.data.orphans_purge(do_local_ids=False)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
return objects
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
options: Optional[Dict] = None
|
||||
) -> Optional[List]:
|
||||
"""
|
||||
Arguments:
|
||||
name: Use pre-defined name
|
||||
namespace: Use pre-defined namespace
|
||||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
if not avalon_container:
|
||||
avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
|
||||
bpy.context.scene.collection.children.link(avalon_container)
|
||||
|
||||
asset_group = bpy.data.objects.new(group_name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
avalon_container.objects.link(asset_group)
|
||||
|
||||
objects = self._process(libpath, asset_group, group_name, None)
|
||||
|
||||
for child in asset_group.children:
|
||||
if child.get(AVALON_PROPERTY):
|
||||
avalon_container.objects.link(child)
|
||||
|
||||
bpy.context.scene.collection.objects.link(asset_group)
|
||||
|
||||
asset_group[AVALON_PROPERTY] = {
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": AVALON_CONTAINER_ID,
|
||||
"name": name,
|
||||
"namespace": namespace or '',
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": str(context["representation"]["_id"]),
|
||||
"libpath": libpath,
|
||||
"asset_name": asset_name,
|
||||
"parent": str(context["representation"]["parent"]),
|
||||
"family": context["representation"]["context"]["family"],
|
||||
"objectName": group_name
|
||||
}
|
||||
|
||||
self[:] = objects
|
||||
return objects
|
||||
|
||||
def update(self, container: Dict, representation: Dict):
|
||||
"""Update the loaded asset.
|
||||
|
||||
This will remove all objects of the current collection, load the new
|
||||
ones and add them to the collection.
|
||||
If the objects of the collection are used in another collection they
|
||||
will not be removed, only unlinked. Normally this should not be the
|
||||
case though.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
object_name = container["objectName"]
|
||||
asset_group = bpy.data.objects.get(object_name)
|
||||
libpath = Path(api.get_representation_path(representation))
|
||||
extension = libpath.suffix.lower()
|
||||
|
||||
self.log.info(
|
||||
"Container: %s\nRepresentation: %s",
|
||||
pformat(container, indent=2),
|
||||
pformat(representation, indent=2),
|
||||
)
|
||||
|
||||
assert asset_group, (
|
||||
f"The asset is not loaded: {container['objectName']}"
|
||||
)
|
||||
assert libpath, (
|
||||
"No existing library file found for {container['objectName']}"
|
||||
)
|
||||
assert libpath.is_file(), (
|
||||
f"The file doesn't exist: {libpath}"
|
||||
)
|
||||
assert extension in plugin.VALID_EXTENSIONS, (
|
||||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
metadata = asset_group.get(AVALON_PROPERTY)
|
||||
group_libpath = metadata["libpath"]
|
||||
|
||||
normalized_group_libpath = (
|
||||
str(Path(bpy.path.abspath(group_libpath)).resolve())
|
||||
)
|
||||
normalized_libpath = (
|
||||
str(Path(bpy.path.abspath(str(libpath))).resolve())
|
||||
)
|
||||
self.log.debug(
|
||||
"normalized_group_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_group_libpath,
|
||||
normalized_libpath,
|
||||
)
|
||||
if normalized_group_libpath == normalized_libpath:
|
||||
self.log.info("Library already loaded, not updating...")
|
||||
return
|
||||
|
||||
actions = {}
|
||||
|
||||
for obj in asset_group.children:
|
||||
obj_meta = obj.get(AVALON_PROPERTY)
|
||||
if obj_meta.get('family') == 'rig':
|
||||
rig = None
|
||||
for child in obj.children:
|
||||
if child.type == 'ARMATURE':
|
||||
rig = child
|
||||
break
|
||||
if not rig:
|
||||
raise Exception("No armature in the rig asset group.")
|
||||
if rig.animation_data and rig.animation_data.action:
|
||||
instance_name = obj_meta.get('instance_name')
|
||||
actions[instance_name] = rig.animation_data.action
|
||||
|
||||
mat = asset_group.matrix_basis.copy()
|
||||
|
||||
# Remove the children of the asset_group first
|
||||
for child in list(asset_group.children):
|
||||
self._remove_asset_and_library(child)
|
||||
|
||||
# Check how many assets use the same library
|
||||
count = 0
|
||||
for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects:
|
||||
if obj.get(AVALON_PROPERTY).get('libpath') == group_libpath:
|
||||
count += 1
|
||||
|
||||
self._remove(asset_group)
|
||||
|
||||
# If it is the last object to use that library, remove it
|
||||
if count == 1:
|
||||
library = bpy.data.libraries.get(bpy.path.basename(group_libpath))
|
||||
bpy.data.libraries.remove(library)
|
||||
|
||||
self._process(str(libpath), asset_group, object_name, actions)
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
for child in asset_group.children:
|
||||
if child.get(AVALON_PROPERTY):
|
||||
avalon_container.objects.link(child)
|
||||
|
||||
asset_group.matrix_basis = mat
|
||||
|
||||
metadata["libpath"] = str(libpath)
|
||||
metadata["representation"] = str(representation["_id"])
|
||||
|
||||
def exec_remove(self, container: Dict) -> bool:
|
||||
"""Remove an existing container from a Blender scene.
|
||||
|
||||
Arguments:
|
||||
container (openpype:container-1.0): Container to remove,
|
||||
from `host.ls()`.
|
||||
|
||||
Returns:
|
||||
bool: Whether the container was deleted.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
object_name = container["objectName"]
|
||||
asset_group = bpy.data.objects.get(object_name)
|
||||
|
||||
if not asset_group:
|
||||
return False
|
||||
|
||||
# Remove the children of the asset_group first
|
||||
for child in list(asset_group.children):
|
||||
self._remove_asset_and_library(child)
|
||||
|
||||
self._remove_asset_and_library(asset_group)
|
||||
|
||||
return True
|
||||
259
openpype/hosts/blender/plugins/load/load_layout_json.py
Normal file
259
openpype/hosts/blender/plugins/load/load_layout_json.py
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
"""Load a layout in Blender."""
|
||||
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Dict, Optional
|
||||
|
||||
import bpy
|
||||
import json
|
||||
|
||||
from avalon import api
|
||||
from avalon.blender.pipeline import AVALON_CONTAINERS
|
||||
from avalon.blender.pipeline import AVALON_CONTAINER_ID
|
||||
from avalon.blender.pipeline import AVALON_PROPERTY
|
||||
from avalon.blender.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
class JsonLayoutLoader(plugin.AssetLoader):
|
||||
"""Load layout published from Unreal."""
|
||||
|
||||
families = ["layout"]
|
||||
representations = ["json"]
|
||||
|
||||
label = "Load Layout"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
animation_creator_name = "CreateAnimation"
|
||||
|
||||
def _remove(self, asset_group):
|
||||
objects = list(asset_group.children)
|
||||
|
||||
for obj in objects:
|
||||
api.remove(obj.get(AVALON_PROPERTY))
|
||||
|
||||
def _remove_animation_instances(self, asset_group):
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if instances:
|
||||
for obj in list(asset_group.children):
|
||||
anim_collection = instances.children.get(
|
||||
obj.name + "_animation")
|
||||
if anim_collection:
|
||||
bpy.data.collections.remove(anim_collection)
|
||||
|
||||
def _get_loader(self, loaders, family):
|
||||
name = ""
|
||||
if family == 'rig':
|
||||
name = "BlendRigLoader"
|
||||
elif family == 'model':
|
||||
name = "BlendModelLoader"
|
||||
|
||||
if name == "":
|
||||
return None
|
||||
|
||||
for loader in loaders:
|
||||
if loader.__name__ == name:
|
||||
return loader
|
||||
|
||||
return None
|
||||
|
||||
def _process(self, libpath, asset, asset_group, actions):
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
with open(libpath, "r") as fp:
|
||||
data = json.load(fp)
|
||||
|
||||
all_loaders = api.discover(api.Loader)
|
||||
|
||||
for element in data:
|
||||
reference = element.get('reference')
|
||||
family = element.get('family')
|
||||
|
||||
loaders = api.loaders_from_representation(all_loaders, reference)
|
||||
loader = self._get_loader(loaders, family)
|
||||
|
||||
if not loader:
|
||||
continue
|
||||
|
||||
instance_name = element.get('instance_name')
|
||||
|
||||
action = None
|
||||
|
||||
if actions:
|
||||
action = actions.get(instance_name, None)
|
||||
|
||||
options = {
|
||||
'parent': asset_group,
|
||||
'transform': element.get('transform'),
|
||||
'action': action,
|
||||
'create_animation': True if family == 'rig' else False,
|
||||
'animation_asset': asset
|
||||
}
|
||||
|
||||
# This should return the loaded asset, but the load call will be
|
||||
# added to the queue to run in the Blender main thread, so
|
||||
# at this time it will not return anything. The assets will be
|
||||
# loaded in the next Blender cycle, so we use the options to
|
||||
# set the transform, parent and assign the action, if there is one.
|
||||
api.load(
|
||||
loader,
|
||||
reference,
|
||||
namespace=instance_name,
|
||||
options=options
|
||||
)
|
||||
|
||||
def process_asset(self,
|
||||
context: dict,
|
||||
name: str,
|
||||
namespace: Optional[str] = None,
|
||||
options: Optional[Dict] = None):
|
||||
"""
|
||||
Arguments:
|
||||
name: Use pre-defined name
|
||||
namespace: Use pre-defined namespace
|
||||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
if not avalon_container:
|
||||
avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
|
||||
bpy.context.scene.collection.children.link(avalon_container)
|
||||
|
||||
asset_group = bpy.data.objects.new(group_name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
avalon_container.objects.link(asset_group)
|
||||
|
||||
self._process(libpath, asset, asset_group, None)
|
||||
|
||||
bpy.context.scene.collection.objects.link(asset_group)
|
||||
|
||||
asset_group[AVALON_PROPERTY] = {
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": AVALON_CONTAINER_ID,
|
||||
"name": name,
|
||||
"namespace": namespace or '',
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": str(context["representation"]["_id"]),
|
||||
"libpath": libpath,
|
||||
"asset_name": asset_name,
|
||||
"parent": str(context["representation"]["parent"]),
|
||||
"family": context["representation"]["context"]["family"],
|
||||
"objectName": group_name
|
||||
}
|
||||
|
||||
self[:] = asset_group.children
|
||||
return asset_group.children
|
||||
|
||||
def exec_update(self, container: Dict, representation: Dict):
|
||||
"""Update the loaded asset.
|
||||
|
||||
This will remove all objects of the current collection, load the new
|
||||
ones and add them to the collection.
|
||||
If the objects of the collection are used in another collection they
|
||||
will not be removed, only unlinked. Normally this should not be the
|
||||
case though.
|
||||
"""
|
||||
object_name = container["objectName"]
|
||||
asset_group = bpy.data.objects.get(object_name)
|
||||
libpath = Path(api.get_representation_path(representation))
|
||||
extension = libpath.suffix.lower()
|
||||
|
||||
self.log.info(
|
||||
"Container: %s\nRepresentation: %s",
|
||||
pformat(container, indent=2),
|
||||
pformat(representation, indent=2),
|
||||
)
|
||||
|
||||
assert asset_group, (
|
||||
f"The asset is not loaded: {container['objectName']}"
|
||||
)
|
||||
assert libpath, (
|
||||
"No existing library file found for {container['objectName']}"
|
||||
)
|
||||
assert libpath.is_file(), (
|
||||
f"The file doesn't exist: {libpath}"
|
||||
)
|
||||
assert extension in plugin.VALID_EXTENSIONS, (
|
||||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
metadata = asset_group.get(AVALON_PROPERTY)
|
||||
group_libpath = metadata["libpath"]
|
||||
|
||||
normalized_group_libpath = (
|
||||
str(Path(bpy.path.abspath(group_libpath)).resolve())
|
||||
)
|
||||
normalized_libpath = (
|
||||
str(Path(bpy.path.abspath(str(libpath))).resolve())
|
||||
)
|
||||
self.log.debug(
|
||||
"normalized_group_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_group_libpath,
|
||||
normalized_libpath,
|
||||
)
|
||||
if normalized_group_libpath == normalized_libpath:
|
||||
self.log.info("Library already loaded, not updating...")
|
||||
return
|
||||
|
||||
actions = {}
|
||||
|
||||
for obj in asset_group.children:
|
||||
obj_meta = obj.get(AVALON_PROPERTY)
|
||||
if obj_meta.get('family') == 'rig':
|
||||
rig = None
|
||||
for child in obj.children:
|
||||
if child.type == 'ARMATURE':
|
||||
rig = child
|
||||
break
|
||||
if not rig:
|
||||
raise Exception("No armature in the rig asset group.")
|
||||
if rig.animation_data and rig.animation_data.action:
|
||||
namespace = obj_meta.get('namespace')
|
||||
actions[namespace] = rig.animation_data.action
|
||||
|
||||
mat = asset_group.matrix_basis.copy()
|
||||
|
||||
self._remove_animation_instances(asset_group)
|
||||
|
||||
self._remove(asset_group)
|
||||
|
||||
self._process(str(libpath), asset_group, actions)
|
||||
|
||||
asset_group.matrix_basis = mat
|
||||
|
||||
metadata["libpath"] = str(libpath)
|
||||
metadata["representation"] = str(representation["_id"])
|
||||
|
||||
def exec_remove(self, container: Dict) -> bool:
|
||||
"""Remove an existing container from a Blender scene.
|
||||
|
||||
Arguments:
|
||||
container (openpype:container-1.0): Container to remove,
|
||||
from `host.ls()`.
|
||||
|
||||
Returns:
|
||||
bool: Whether the container was deleted.
|
||||
"""
|
||||
object_name = container["objectName"]
|
||||
asset_group = bpy.data.objects.get(object_name)
|
||||
|
||||
if not asset_group:
|
||||
return False
|
||||
|
||||
self._remove_animation_instances(asset_group)
|
||||
|
||||
self._remove(asset_group)
|
||||
|
||||
bpy.data.objects.remove(asset_group)
|
||||
|
||||
return True
|
||||
218
openpype/hosts/blender/plugins/load/load_look.py
Normal file
218
openpype/hosts/blender/plugins/load/load_look.py
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
"""Load a model asset in Blender."""
|
||||
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import os
|
||||
import json
|
||||
import bpy
|
||||
|
||||
from avalon import api, blender
|
||||
import openpype.hosts.blender.api.plugin as plugin
|
||||
|
||||
|
||||
class BlendLookLoader(plugin.AssetLoader):
|
||||
"""Load models from a .blend file.
|
||||
|
||||
Because they come from a .blend file we can simply link the collection that
|
||||
contains the model. There is no further need to 'containerise' it.
|
||||
"""
|
||||
|
||||
families = ["look"]
|
||||
representations = ["json"]
|
||||
|
||||
label = "Load Look"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def get_all_children(self, obj):
|
||||
children = list(obj.children)
|
||||
|
||||
for child in children:
|
||||
children.extend(child.children)
|
||||
|
||||
return children
|
||||
|
||||
def _process(self, libpath, container_name, objects):
|
||||
with open(libpath, "r") as fp:
|
||||
data = json.load(fp)
|
||||
|
||||
path = os.path.dirname(libpath)
|
||||
materials_path = f"{path}/resources"
|
||||
|
||||
materials = []
|
||||
|
||||
for entry in data:
|
||||
file = entry.get('fbx_filename')
|
||||
if file is None:
|
||||
continue
|
||||
|
||||
bpy.ops.import_scene.fbx(filepath=f"{materials_path}/{file}")
|
||||
|
||||
mesh = [o for o in bpy.context.scene.objects if o.select_get()][0]
|
||||
material = mesh.data.materials[0]
|
||||
material.name = f"{material.name}:{container_name}"
|
||||
|
||||
texture_file = entry.get('tga_filename')
|
||||
if texture_file:
|
||||
node_tree = material.node_tree
|
||||
pbsdf = node_tree.nodes['Principled BSDF']
|
||||
base_color = pbsdf.inputs[0]
|
||||
tex_node = base_color.links[0].from_node
|
||||
tex_node.image.filepath = f"{materials_path}/{texture_file}"
|
||||
|
||||
materials.append(material)
|
||||
|
||||
for obj in objects:
|
||||
for child in self.get_all_children(obj):
|
||||
mesh_name = child.name.split(':')[0]
|
||||
if mesh_name == material.name.split(':')[0]:
|
||||
child.data.materials.clear()
|
||||
child.data.materials.append(material)
|
||||
break
|
||||
|
||||
bpy.data.objects.remove(mesh)
|
||||
|
||||
return materials, objects
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
options: Optional[Dict] = None
|
||||
) -> Optional[List]:
|
||||
"""
|
||||
Arguments:
|
||||
name: Use pre-defined name
|
||||
namespace: Use pre-defined namespace
|
||||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
|
||||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
lib_container = plugin.asset_name(
|
||||
asset, subset
|
||||
)
|
||||
unique_number = plugin.get_unique_number(
|
||||
asset, subset
|
||||
)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
container_name = plugin.asset_name(
|
||||
asset, subset, unique_number
|
||||
)
|
||||
|
||||
container = bpy.data.collections.new(lib_container)
|
||||
container.name = container_name
|
||||
blender.pipeline.containerise_existing(
|
||||
container,
|
||||
name,
|
||||
namespace,
|
||||
context,
|
||||
self.__class__.__name__,
|
||||
)
|
||||
|
||||
metadata = container.get(blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
metadata["libpath"] = libpath
|
||||
metadata["lib_container"] = lib_container
|
||||
|
||||
selected = [o for o in bpy.context.scene.objects if o.select_get()]
|
||||
|
||||
materials, objects = self._process(libpath, container_name, selected)
|
||||
|
||||
# Save the list of imported materials in the metadata container
|
||||
metadata["objects"] = objects
|
||||
metadata["materials"] = materials
|
||||
|
||||
metadata["parent"] = str(context["representation"]["parent"])
|
||||
metadata["family"] = context["representation"]["context"]["family"]
|
||||
|
||||
nodes = list(container.objects)
|
||||
nodes.append(container)
|
||||
self[:] = nodes
|
||||
return nodes
|
||||
|
||||
def update(self, container: Dict, representation: Dict):
|
||||
collection = bpy.data.collections.get(container["objectName"])
|
||||
libpath = Path(api.get_representation_path(representation))
|
||||
extension = libpath.suffix.lower()
|
||||
|
||||
self.log.info(
|
||||
"Container: %s\nRepresentation: %s",
|
||||
pformat(container, indent=2),
|
||||
pformat(representation, indent=2),
|
||||
)
|
||||
|
||||
assert collection, (
|
||||
f"The asset is not loaded: {container['objectName']}"
|
||||
)
|
||||
assert not (collection.children), (
|
||||
"Nested collections are not supported."
|
||||
)
|
||||
assert libpath, (
|
||||
"No existing library file found for {container['objectName']}"
|
||||
)
|
||||
assert libpath.is_file(), (
|
||||
f"The file doesn't exist: {libpath}"
|
||||
)
|
||||
assert extension in plugin.VALID_EXTENSIONS, (
|
||||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(blender.pipeline.AVALON_PROPERTY)
|
||||
collection_libpath = collection_metadata["libpath"]
|
||||
|
||||
normalized_collection_libpath = (
|
||||
str(Path(bpy.path.abspath(collection_libpath)).resolve())
|
||||
)
|
||||
normalized_libpath = (
|
||||
str(Path(bpy.path.abspath(str(libpath))).resolve())
|
||||
)
|
||||
self.log.debug(
|
||||
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_collection_libpath,
|
||||
normalized_libpath,
|
||||
)
|
||||
if normalized_collection_libpath == normalized_libpath:
|
||||
self.log.info("Library already loaded, not updating...")
|
||||
return
|
||||
|
||||
for obj in collection_metadata['objects']:
|
||||
for child in self.get_all_children(obj):
|
||||
child.data.materials.clear()
|
||||
|
||||
for material in collection_metadata['materials']:
|
||||
bpy.data.materials.remove(material)
|
||||
|
||||
namespace = collection_metadata['namespace']
|
||||
name = collection_metadata['name']
|
||||
|
||||
container_name = f"{namespace}_{name}"
|
||||
|
||||
materials, objects = self._process(
|
||||
libpath, container_name, collection_metadata['objects'])
|
||||
|
||||
collection_metadata["objects"] = objects
|
||||
collection_metadata["materials"] = materials
|
||||
collection_metadata["libpath"] = str(libpath)
|
||||
collection_metadata["representation"] = str(representation["_id"])
|
||||
|
||||
def remove(self, container: Dict) -> bool:
|
||||
collection = bpy.data.collections.get(container["objectName"])
|
||||
if not collection:
|
||||
return False
|
||||
|
||||
collection_metadata = collection.get(blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
for obj in collection_metadata['objects']:
|
||||
for child in self.get_all_children(obj):
|
||||
child.data.materials.clear()
|
||||
|
||||
for material in collection_metadata['materials']:
|
||||
bpy.data.materials.remove(material)
|
||||
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
return True
|
||||
|
|
@ -1,13 +1,16 @@
|
|||
"""Load a model asset in Blender."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from avalon import api, blender
|
||||
import bpy
|
||||
import openpype.hosts.blender.api.plugin as plugin
|
||||
|
||||
from avalon import api
|
||||
from avalon.blender.pipeline import AVALON_CONTAINERS
|
||||
from avalon.blender.pipeline import AVALON_CONTAINER_ID
|
||||
from avalon.blender.pipeline import AVALON_PROPERTY
|
||||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
class BlendModelLoader(plugin.AssetLoader):
|
||||
|
|
@ -24,52 +27,75 @@ class BlendModelLoader(plugin.AssetLoader):
|
|||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def _remove(self, objects, container):
|
||||
for obj in list(objects):
|
||||
for material_slot in list(obj.material_slots):
|
||||
bpy.data.materials.remove(material_slot.material)
|
||||
bpy.data.meshes.remove(obj.data)
|
||||
def _remove(self, asset_group):
|
||||
objects = list(asset_group.children)
|
||||
|
||||
bpy.data.collections.remove(container)
|
||||
for obj in objects:
|
||||
if obj.type == 'MESH':
|
||||
for material_slot in list(obj.material_slots):
|
||||
bpy.data.materials.remove(material_slot.material)
|
||||
bpy.data.meshes.remove(obj.data)
|
||||
elif obj.type == 'EMPTY':
|
||||
objects.extend(obj.children)
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
def _process(
|
||||
self, libpath, lib_container, container_name,
|
||||
parent_collection
|
||||
):
|
||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||
def _process(self, libpath, asset_group, group_name):
|
||||
with bpy.data.libraries.load(
|
||||
libpath, link=True, relative=relative
|
||||
) as (_, data_to):
|
||||
data_to.collections = [lib_container]
|
||||
libpath, link=True, relative=False
|
||||
) as (data_from, data_to):
|
||||
data_to.objects = data_from.objects
|
||||
|
||||
parent = parent_collection
|
||||
parent = bpy.context.scene.collection
|
||||
|
||||
if parent is None:
|
||||
parent = bpy.context.scene.collection
|
||||
empties = [obj for obj in data_to.objects if obj.type == 'EMPTY']
|
||||
|
||||
parent.children.link(bpy.data.collections[lib_container])
|
||||
container = None
|
||||
|
||||
model_container = parent.children[lib_container].make_local()
|
||||
model_container.name = container_name
|
||||
for empty in empties:
|
||||
if empty.get(AVALON_PROPERTY):
|
||||
container = empty
|
||||
break
|
||||
|
||||
for obj in model_container.objects:
|
||||
local_obj = plugin.prepare_data(obj, container_name)
|
||||
plugin.prepare_data(local_obj.data, container_name)
|
||||
assert container, "No asset group found"
|
||||
|
||||
for material_slot in local_obj.material_slots:
|
||||
plugin.prepare_data(material_slot.material, container_name)
|
||||
# Children must be linked before parents,
|
||||
# otherwise the hierarchy will break
|
||||
objects = []
|
||||
nodes = list(container.children)
|
||||
|
||||
if not obj.get(blender.pipeline.AVALON_PROPERTY):
|
||||
local_obj[blender.pipeline.AVALON_PROPERTY] = dict()
|
||||
for obj in nodes:
|
||||
obj.parent = asset_group
|
||||
|
||||
avalon_info = local_obj[blender.pipeline.AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": container_name})
|
||||
for obj in nodes:
|
||||
objects.append(obj)
|
||||
nodes.extend(list(obj.children))
|
||||
|
||||
model_container.pop(blender.pipeline.AVALON_PROPERTY)
|
||||
objects.reverse()
|
||||
|
||||
for obj in objects:
|
||||
parent.objects.link(obj)
|
||||
|
||||
for obj in objects:
|
||||
local_obj = plugin.prepare_data(obj, group_name)
|
||||
if local_obj.type != 'EMPTY':
|
||||
plugin.prepare_data(local_obj.data, group_name)
|
||||
|
||||
for material_slot in local_obj.material_slots:
|
||||
plugin.prepare_data(material_slot.material, group_name)
|
||||
|
||||
if not local_obj.get(AVALON_PROPERTY):
|
||||
local_obj[AVALON_PROPERTY] = dict()
|
||||
|
||||
avalon_info = local_obj[AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": group_name})
|
||||
|
||||
objects.reverse()
|
||||
|
||||
bpy.data.orphans_purge(do_local_ids=False)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
return model_container
|
||||
return objects
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
|
|
@ -82,52 +108,80 @@ class BlendModelLoader(plugin.AssetLoader):
|
|||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
|
||||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
lib_container = plugin.asset_name(
|
||||
asset, subset
|
||||
)
|
||||
unique_number = plugin.get_unique_number(
|
||||
asset, subset
|
||||
)
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
container_name = plugin.asset_name(
|
||||
asset, subset, unique_number
|
||||
)
|
||||
|
||||
container = bpy.data.collections.new(lib_container)
|
||||
container.name = container_name
|
||||
blender.pipeline.containerise_existing(
|
||||
container,
|
||||
name,
|
||||
namespace,
|
||||
context,
|
||||
self.__class__.__name__,
|
||||
)
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
if not avalon_container:
|
||||
avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
|
||||
bpy.context.scene.collection.children.link(avalon_container)
|
||||
|
||||
container_metadata = container.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
asset_group = bpy.data.objects.new(group_name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
avalon_container.objects.link(asset_group)
|
||||
|
||||
container_metadata["libpath"] = libpath
|
||||
container_metadata["lib_container"] = lib_container
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
obj_container = self._process(
|
||||
libpath, lib_container, container_name, None)
|
||||
if options is not None:
|
||||
parent = options.get('parent')
|
||||
transform = options.get('transform')
|
||||
|
||||
container_metadata["obj_container"] = obj_container
|
||||
if parent and transform:
|
||||
location = transform.get('translation')
|
||||
rotation = transform.get('rotation')
|
||||
scale = transform.get('scale')
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
container_metadata["objects"] = obj_container.all_objects
|
||||
asset_group.location = (
|
||||
location.get('x'),
|
||||
location.get('y'),
|
||||
location.get('z')
|
||||
)
|
||||
asset_group.rotation_euler = (
|
||||
rotation.get('x'),
|
||||
rotation.get('y'),
|
||||
rotation.get('z')
|
||||
)
|
||||
asset_group.scale = (
|
||||
scale.get('x'),
|
||||
scale.get('y'),
|
||||
scale.get('z')
|
||||
)
|
||||
|
||||
nodes = list(container.objects)
|
||||
nodes.append(container)
|
||||
self[:] = nodes
|
||||
return nodes
|
||||
bpy.context.view_layer.objects.active = parent
|
||||
asset_group.select_set(True)
|
||||
|
||||
def update(self, container: Dict, representation: Dict):
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
objects = self._process(libpath, asset_group, group_name)
|
||||
|
||||
bpy.context.scene.collection.objects.link(asset_group)
|
||||
|
||||
asset_group[AVALON_PROPERTY] = {
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": AVALON_CONTAINER_ID,
|
||||
"name": name,
|
||||
"namespace": namespace or '',
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": str(context["representation"]["_id"]),
|
||||
"libpath": libpath,
|
||||
"asset_name": asset_name,
|
||||
"parent": str(context["representation"]["parent"]),
|
||||
"family": context["representation"]["context"]["family"],
|
||||
"objectName": group_name
|
||||
}
|
||||
|
||||
self[:] = objects
|
||||
return objects
|
||||
|
||||
def exec_update(self, container: Dict, representation: Dict):
|
||||
"""Update the loaded asset.
|
||||
|
||||
This will remove all objects of the current collection, load the new
|
||||
|
|
@ -135,13 +189,9 @@ class BlendModelLoader(plugin.AssetLoader):
|
|||
If the objects of the collection are used in another collection they
|
||||
will not be removed, only unlinked. Normally this should not be the
|
||||
case though.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
collection = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
object_name = container["objectName"]
|
||||
asset_group = bpy.data.objects.get(object_name)
|
||||
libpath = Path(api.get_representation_path(representation))
|
||||
extension = libpath.suffix.lower()
|
||||
|
||||
|
|
@ -151,12 +201,9 @@ class BlendModelLoader(plugin.AssetLoader):
|
|||
pformat(representation, indent=2),
|
||||
)
|
||||
|
||||
assert collection, (
|
||||
assert asset_group, (
|
||||
f"The asset is not loaded: {container['objectName']}"
|
||||
)
|
||||
assert not (collection.children), (
|
||||
"Nested collections are not supported."
|
||||
)
|
||||
assert libpath, (
|
||||
"No existing library file found for {container['objectName']}"
|
||||
)
|
||||
|
|
@ -167,47 +214,47 @@ class BlendModelLoader(plugin.AssetLoader):
|
|||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
collection_libpath = collection_metadata["libpath"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
metadata = asset_group.get(AVALON_PROPERTY)
|
||||
group_libpath = metadata["libpath"]
|
||||
|
||||
obj_container = plugin.get_local_collection_with_name(
|
||||
collection_metadata["obj_container"].name
|
||||
)
|
||||
objects = obj_container.all_objects
|
||||
|
||||
container_name = obj_container.name
|
||||
|
||||
normalized_collection_libpath = (
|
||||
str(Path(bpy.path.abspath(collection_libpath)).resolve())
|
||||
normalized_group_libpath = (
|
||||
str(Path(bpy.path.abspath(group_libpath)).resolve())
|
||||
)
|
||||
normalized_libpath = (
|
||||
str(Path(bpy.path.abspath(str(libpath))).resolve())
|
||||
)
|
||||
self.log.debug(
|
||||
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_collection_libpath,
|
||||
"normalized_group_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_group_libpath,
|
||||
normalized_libpath,
|
||||
)
|
||||
if normalized_collection_libpath == normalized_libpath:
|
||||
if normalized_group_libpath == normalized_libpath:
|
||||
self.log.info("Library already loaded, not updating...")
|
||||
return
|
||||
|
||||
parent = plugin.get_parent_collection(obj_container)
|
||||
# Check how many assets use the same library
|
||||
count = 0
|
||||
for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects:
|
||||
if obj.get(AVALON_PROPERTY).get('libpath') == group_libpath:
|
||||
count += 1
|
||||
|
||||
self._remove(objects, obj_container)
|
||||
mat = asset_group.matrix_basis.copy()
|
||||
|
||||
obj_container = self._process(
|
||||
str(libpath), lib_container, container_name, parent)
|
||||
self._remove(asset_group)
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
collection_metadata["obj_container"] = obj_container
|
||||
collection_metadata["objects"] = obj_container.all_objects
|
||||
collection_metadata["libpath"] = str(libpath)
|
||||
collection_metadata["representation"] = str(representation["_id"])
|
||||
# If it is the last object to use that library, remove it
|
||||
if count == 1:
|
||||
library = bpy.data.libraries.get(bpy.path.basename(group_libpath))
|
||||
bpy.data.libraries.remove(library)
|
||||
|
||||
def remove(self, container: Dict) -> bool:
|
||||
self._process(str(libpath), asset_group, object_name)
|
||||
|
||||
asset_group.matrix_basis = mat
|
||||
|
||||
metadata["libpath"] = str(libpath)
|
||||
metadata["representation"] = str(representation["_id"])
|
||||
|
||||
def exec_remove(self, container: Dict) -> bool:
|
||||
"""Remove an existing container from a Blender scene.
|
||||
|
||||
Arguments:
|
||||
|
|
@ -216,91 +263,27 @@ class BlendModelLoader(plugin.AssetLoader):
|
|||
|
||||
Returns:
|
||||
bool: Whether the container was deleted.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
collection = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
if not collection:
|
||||
object_name = container["objectName"]
|
||||
asset_group = bpy.data.objects.get(object_name)
|
||||
libpath = asset_group.get(AVALON_PROPERTY).get('libpath')
|
||||
|
||||
# Check how many assets use the same library
|
||||
count = 0
|
||||
for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects:
|
||||
if obj.get(AVALON_PROPERTY).get('libpath') == libpath:
|
||||
count += 1
|
||||
|
||||
if not asset_group:
|
||||
return False
|
||||
assert not (collection.children), (
|
||||
"Nested collections are not supported."
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
self._remove(asset_group)
|
||||
|
||||
obj_container = plugin.get_local_collection_with_name(
|
||||
collection_metadata["obj_container"].name
|
||||
)
|
||||
objects = obj_container.all_objects
|
||||
bpy.data.objects.remove(asset_group)
|
||||
|
||||
self._remove(objects, obj_container)
|
||||
|
||||
bpy.data.collections.remove(collection)
|
||||
# If it is the last object to use that library, remove it
|
||||
if count == 1:
|
||||
library = bpy.data.libraries.get(bpy.path.basename(libpath))
|
||||
bpy.data.libraries.remove(library)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class CacheModelLoader(plugin.AssetLoader):
|
||||
"""Load cache models.
|
||||
|
||||
Stores the imported asset in a collection named after the asset.
|
||||
|
||||
Note:
|
||||
At least for now it only supports Alembic files.
|
||||
"""
|
||||
|
||||
families = ["model"]
|
||||
representations = ["abc"]
|
||||
|
||||
label = "Link Model"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
options: Optional[Dict] = None
|
||||
) -> Optional[List]:
|
||||
"""
|
||||
Arguments:
|
||||
name: Use pre-defined name
|
||||
namespace: Use pre-defined namespace
|
||||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Loading of Alembic files is not yet implemented.")
|
||||
# TODO (jasper): implement Alembic import.
|
||||
|
||||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
# TODO (jasper): evaluate use of namespace which is 'alien' to Blender.
|
||||
lib_container = container_name = (
|
||||
plugin.asset_name(asset, subset, namespace)
|
||||
)
|
||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||
|
||||
with bpy.data.libraries.load(
|
||||
libpath, link=True, relative=relative
|
||||
) as (data_from, data_to):
|
||||
data_to.collections = [lib_container]
|
||||
|
||||
scene = bpy.context.scene
|
||||
instance_empty = bpy.data.objects.new(
|
||||
container_name, None
|
||||
)
|
||||
scene.collection.objects.link(instance_empty)
|
||||
instance_empty.instance_type = 'COLLECTION'
|
||||
collection = bpy.data.collections[lib_container]
|
||||
collection.name = container_name
|
||||
instance_empty.instance_collection = collection
|
||||
|
||||
nodes = list(collection.objects)
|
||||
nodes.append(collection)
|
||||
nodes.append(instance_empty)
|
||||
self[:] = nodes
|
||||
return nodes
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
"""Load a rig asset in Blender."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from avalon import api, blender
|
||||
import bpy
|
||||
import openpype.hosts.blender.api.plugin as plugin
|
||||
|
||||
from avalon import api
|
||||
from avalon.blender.pipeline import AVALON_CONTAINERS
|
||||
from avalon.blender.pipeline import AVALON_CONTAINER_ID
|
||||
from avalon.blender.pipeline import AVALON_PROPERTY
|
||||
from openpype import lib
|
||||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
class BlendRigLoader(plugin.AssetLoader):
|
||||
"""Load rigs from a .blend file.
|
||||
|
||||
Because they come from a .blend file we can simply link the collection that
|
||||
contains the model. There is no further need to 'containerise' it.
|
||||
"""
|
||||
"""Load rigs from a .blend file."""
|
||||
|
||||
families = ["rig"]
|
||||
representations = ["blend"]
|
||||
|
|
@ -24,102 +24,113 @@ class BlendRigLoader(plugin.AssetLoader):
|
|||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def _remove(self, objects, obj_container):
|
||||
for obj in list(objects):
|
||||
if obj.type == 'ARMATURE':
|
||||
bpy.data.armatures.remove(obj.data)
|
||||
elif obj.type == 'MESH':
|
||||
def _remove(self, asset_group):
|
||||
objects = list(asset_group.children)
|
||||
|
||||
for obj in objects:
|
||||
if obj.type == 'MESH':
|
||||
for material_slot in list(obj.material_slots):
|
||||
if material_slot.material:
|
||||
bpy.data.materials.remove(material_slot.material)
|
||||
bpy.data.meshes.remove(obj.data)
|
||||
elif obj.type == 'ARMATURE':
|
||||
objects.extend(obj.children)
|
||||
bpy.data.armatures.remove(obj.data)
|
||||
elif obj.type == 'CURVE':
|
||||
bpy.data.curves.remove(obj.data)
|
||||
elif obj.type == 'EMPTY':
|
||||
objects.extend(obj.children)
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
for child in obj_container.children:
|
||||
bpy.data.collections.remove(child)
|
||||
|
||||
bpy.data.collections.remove(obj_container)
|
||||
|
||||
def make_local_and_metadata(self, obj, collection_name):
|
||||
local_obj = plugin.prepare_data(obj, collection_name)
|
||||
plugin.prepare_data(local_obj.data, collection_name)
|
||||
|
||||
if not local_obj.get(blender.pipeline.AVALON_PROPERTY):
|
||||
local_obj[blender.pipeline.AVALON_PROPERTY] = dict()
|
||||
|
||||
avalon_info = local_obj[blender.pipeline.AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": collection_name + '_CON'})
|
||||
|
||||
return local_obj
|
||||
|
||||
def _process(
|
||||
self, libpath, lib_container, collection_name,
|
||||
action, parent_collection
|
||||
):
|
||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||
def _process(self, libpath, asset_group, group_name, action):
|
||||
with bpy.data.libraries.load(
|
||||
libpath, link=True, relative=relative
|
||||
) as (_, data_to):
|
||||
data_to.collections = [lib_container]
|
||||
libpath, link=True, relative=False
|
||||
) as (data_from, data_to):
|
||||
data_to.objects = data_from.objects
|
||||
|
||||
parent = parent_collection
|
||||
parent = bpy.context.scene.collection
|
||||
|
||||
if parent is None:
|
||||
parent = bpy.context.scene.collection
|
||||
empties = [obj for obj in data_to.objects if obj.type == 'EMPTY']
|
||||
|
||||
parent.children.link(bpy.data.collections[lib_container])
|
||||
container = None
|
||||
|
||||
rig_container = parent.children[lib_container].make_local()
|
||||
rig_container.name = collection_name
|
||||
for empty in empties:
|
||||
if empty.get(AVALON_PROPERTY):
|
||||
container = empty
|
||||
break
|
||||
|
||||
assert container, "No asset group found"
|
||||
|
||||
# Children must be linked before parents,
|
||||
# otherwise the hierarchy will break
|
||||
objects = []
|
||||
armatures = [
|
||||
obj for obj in rig_container.objects
|
||||
if obj.type == 'ARMATURE'
|
||||
]
|
||||
nodes = list(container.children)
|
||||
|
||||
for child in rig_container.children:
|
||||
local_child = plugin.prepare_data(child, collection_name)
|
||||
objects.extend(local_child.objects)
|
||||
for obj in nodes:
|
||||
obj.parent = asset_group
|
||||
|
||||
# for obj in bpy.data.objects:
|
||||
# obj.select_set(False)
|
||||
for obj in nodes:
|
||||
objects.append(obj)
|
||||
nodes.extend(list(obj.children))
|
||||
|
||||
objects.reverse()
|
||||
|
||||
constraints = []
|
||||
|
||||
armatures = [obj for obj in objects if obj.type == 'ARMATURE']
|
||||
|
||||
for armature in armatures:
|
||||
for bone in armature.pose.bones:
|
||||
for constraint in bone.constraints:
|
||||
if hasattr(constraint, 'target'):
|
||||
constraints.append(constraint)
|
||||
|
||||
# Link armatures after other objects.
|
||||
# The armature is unparented for all the non-local meshes,
|
||||
# when it is made local.
|
||||
for obj in objects:
|
||||
local_obj = self.make_local_and_metadata(obj, collection_name)
|
||||
parent.objects.link(obj)
|
||||
|
||||
if obj != local_obj:
|
||||
for constraint in constraints:
|
||||
if constraint.target == obj:
|
||||
constraint.target = local_obj
|
||||
for obj in objects:
|
||||
local_obj = plugin.prepare_data(obj, group_name)
|
||||
|
||||
for armature in armatures:
|
||||
local_obj = self.make_local_and_metadata(armature, collection_name)
|
||||
if local_obj.type == 'MESH':
|
||||
plugin.prepare_data(local_obj.data, group_name)
|
||||
|
||||
if action is not None:
|
||||
local_obj.animation_data.action = action
|
||||
if obj != local_obj:
|
||||
for constraint in constraints:
|
||||
if constraint.target == obj:
|
||||
constraint.target = local_obj
|
||||
|
||||
# Set link the drivers to the local object
|
||||
if local_obj.data.animation_data:
|
||||
for d in local_obj.data.animation_data.drivers:
|
||||
for v in d.driver.variables:
|
||||
for t in v.targets:
|
||||
t.id = local_obj
|
||||
for material_slot in local_obj.material_slots:
|
||||
if material_slot.material:
|
||||
plugin.prepare_data(material_slot.material, group_name)
|
||||
elif local_obj.type == 'ARMATURE':
|
||||
plugin.prepare_data(local_obj.data, group_name)
|
||||
|
||||
rig_container.pop(blender.pipeline.AVALON_PROPERTY)
|
||||
if action is not None:
|
||||
local_obj.animation_data.action = action
|
||||
elif local_obj.animation_data.action is not None:
|
||||
plugin.prepare_data(
|
||||
local_obj.animation_data.action, group_name)
|
||||
|
||||
# Set link the drivers to the local object
|
||||
if local_obj.data.animation_data:
|
||||
for d in local_obj.data.animation_data.drivers:
|
||||
for v in d.driver.variables:
|
||||
for t in v.targets:
|
||||
t.id = local_obj
|
||||
|
||||
if not local_obj.get(AVALON_PROPERTY):
|
||||
local_obj[AVALON_PROPERTY] = dict()
|
||||
|
||||
avalon_info = local_obj[AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": group_name})
|
||||
|
||||
objects.reverse()
|
||||
|
||||
bpy.data.orphans_purge(do_local_ids=False)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
return rig_container
|
||||
return objects
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
|
|
@ -135,59 +146,111 @@ class BlendRigLoader(plugin.AssetLoader):
|
|||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
lib_container = plugin.asset_name(
|
||||
asset, subset
|
||||
)
|
||||
unique_number = plugin.get_unique_number(
|
||||
asset, subset
|
||||
)
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
collection_name = plugin.asset_name(
|
||||
asset, subset, unique_number
|
||||
)
|
||||
|
||||
container = bpy.data.collections.new(collection_name)
|
||||
blender.pipeline.containerise_existing(
|
||||
container,
|
||||
name,
|
||||
namespace,
|
||||
context,
|
||||
self.__class__.__name__,
|
||||
)
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
if not avalon_container:
|
||||
avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
|
||||
bpy.context.scene.collection.children.link(avalon_container)
|
||||
|
||||
container_metadata = container.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
asset_group = bpy.data.objects.new(group_name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
avalon_container.objects.link(asset_group)
|
||||
|
||||
container_metadata["libpath"] = libpath
|
||||
container_metadata["lib_container"] = lib_container
|
||||
action = None
|
||||
|
||||
obj_container = self._process(
|
||||
libpath, lib_container, collection_name, None, None)
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
container_metadata["obj_container"] = obj_container
|
||||
# Save the list of objects in the metadata container
|
||||
container_metadata["objects"] = obj_container.all_objects
|
||||
create_animation = False
|
||||
|
||||
nodes = list(container.objects)
|
||||
nodes.append(container)
|
||||
self[:] = nodes
|
||||
return nodes
|
||||
if options is not None:
|
||||
parent = options.get('parent')
|
||||
transform = options.get('transform')
|
||||
action = options.get('action')
|
||||
create_animation = options.get('create_animation')
|
||||
|
||||
def update(self, container: Dict, representation: Dict):
|
||||
if parent and transform:
|
||||
location = transform.get('translation')
|
||||
rotation = transform.get('rotation')
|
||||
scale = transform.get('scale')
|
||||
|
||||
asset_group.location = (
|
||||
location.get('x'),
|
||||
location.get('y'),
|
||||
location.get('z')
|
||||
)
|
||||
asset_group.rotation_euler = (
|
||||
rotation.get('x'),
|
||||
rotation.get('y'),
|
||||
rotation.get('z')
|
||||
)
|
||||
asset_group.scale = (
|
||||
scale.get('x'),
|
||||
scale.get('y'),
|
||||
scale.get('z')
|
||||
)
|
||||
|
||||
bpy.context.view_layer.objects.active = parent
|
||||
asset_group.select_set(True)
|
||||
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
objects = self._process(libpath, asset_group, group_name, action)
|
||||
|
||||
if create_animation:
|
||||
creator_plugin = lib.get_creator_by_name("CreateAnimation")
|
||||
if not creator_plugin:
|
||||
raise ValueError("Creator plugin \"CreateAnimation\" was "
|
||||
"not found.")
|
||||
|
||||
asset_group.select_set(True)
|
||||
|
||||
animation_asset = options.get('animation_asset')
|
||||
|
||||
api.create(
|
||||
creator_plugin,
|
||||
name=namespace + "_animation",
|
||||
# name=f"{unique_number}_{subset}_animation",
|
||||
asset=animation_asset,
|
||||
options={"useSelection": False, "asset_group": asset_group},
|
||||
data={"dependencies": str(context["representation"]["_id"])}
|
||||
)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
bpy.context.scene.collection.objects.link(asset_group)
|
||||
|
||||
asset_group[AVALON_PROPERTY] = {
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": AVALON_CONTAINER_ID,
|
||||
"name": name,
|
||||
"namespace": namespace or '',
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": str(context["representation"]["_id"]),
|
||||
"libpath": libpath,
|
||||
"asset_name": asset_name,
|
||||
"parent": str(context["representation"]["parent"]),
|
||||
"family": context["representation"]["context"]["family"],
|
||||
"objectName": group_name
|
||||
}
|
||||
|
||||
self[:] = objects
|
||||
return objects
|
||||
|
||||
def exec_update(self, container: Dict, representation: Dict):
|
||||
"""Update the loaded asset.
|
||||
|
||||
This will remove all objects of the current collection, load the new
|
||||
ones and add them to the collection.
|
||||
If the objects of the collection are used in another collection they
|
||||
will not be removed, only unlinked. Normally this should not be the
|
||||
case though.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
This will remove all children of the asset group, load the new ones
|
||||
and add them as children of the group.
|
||||
"""
|
||||
collection = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
object_name = container["objectName"]
|
||||
asset_group = bpy.data.objects.get(object_name)
|
||||
libpath = Path(api.get_representation_path(representation))
|
||||
extension = libpath.suffix.lower()
|
||||
|
||||
|
|
@ -197,12 +260,9 @@ class BlendRigLoader(plugin.AssetLoader):
|
|||
pformat(representation, indent=2),
|
||||
)
|
||||
|
||||
assert collection, (
|
||||
assert asset_group, (
|
||||
f"The asset is not loaded: {container['objectName']}"
|
||||
)
|
||||
assert not (collection.children), (
|
||||
"Nested collections are not supported."
|
||||
)
|
||||
assert libpath, (
|
||||
"No existing library file found for {container['objectName']}"
|
||||
)
|
||||
|
|
@ -213,89 +273,84 @@ class BlendRigLoader(plugin.AssetLoader):
|
|||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
collection_libpath = collection_metadata["libpath"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
metadata = asset_group.get(AVALON_PROPERTY)
|
||||
group_libpath = metadata["libpath"]
|
||||
|
||||
obj_container = plugin.get_local_collection_with_name(
|
||||
collection_metadata["obj_container"].name
|
||||
)
|
||||
objects = obj_container.all_objects
|
||||
|
||||
container_name = obj_container.name
|
||||
|
||||
normalized_collection_libpath = (
|
||||
str(Path(bpy.path.abspath(collection_libpath)).resolve())
|
||||
normalized_group_libpath = (
|
||||
str(Path(bpy.path.abspath(group_libpath)).resolve())
|
||||
)
|
||||
normalized_libpath = (
|
||||
str(Path(bpy.path.abspath(str(libpath))).resolve())
|
||||
)
|
||||
self.log.debug(
|
||||
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_collection_libpath,
|
||||
"normalized_group_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_group_libpath,
|
||||
normalized_libpath,
|
||||
)
|
||||
if normalized_collection_libpath == normalized_libpath:
|
||||
if normalized_group_libpath == normalized_libpath:
|
||||
self.log.info("Library already loaded, not updating...")
|
||||
return
|
||||
|
||||
# Check how many assets use the same library
|
||||
count = 0
|
||||
for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects:
|
||||
if obj.get(AVALON_PROPERTY).get('libpath') == group_libpath:
|
||||
count += 1
|
||||
|
||||
# Get the armature of the rig
|
||||
armatures = [obj for obj in objects if obj.type == 'ARMATURE']
|
||||
assert(len(armatures) == 1)
|
||||
objects = asset_group.children
|
||||
armature = [obj for obj in objects if obj.type == 'ARMATURE'][0]
|
||||
|
||||
action = None
|
||||
if armatures[0].animation_data and armatures[0].animation_data.action:
|
||||
action = armatures[0].animation_data.action
|
||||
if armature.animation_data and armature.animation_data.action:
|
||||
action = armature.animation_data.action
|
||||
|
||||
parent = plugin.get_parent_collection(obj_container)
|
||||
mat = asset_group.matrix_basis.copy()
|
||||
|
||||
self._remove(objects, obj_container)
|
||||
self._remove(asset_group)
|
||||
|
||||
obj_container = self._process(
|
||||
str(libpath), lib_container, container_name, action, parent)
|
||||
# If it is the last object to use that library, remove it
|
||||
if count == 1:
|
||||
library = bpy.data.libraries.get(bpy.path.basename(group_libpath))
|
||||
bpy.data.libraries.remove(library)
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
collection_metadata["obj_container"] = obj_container
|
||||
collection_metadata["objects"] = obj_container.all_objects
|
||||
collection_metadata["libpath"] = str(libpath)
|
||||
collection_metadata["representation"] = str(representation["_id"])
|
||||
self._process(str(libpath), asset_group, object_name, action)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
asset_group.matrix_basis = mat
|
||||
|
||||
def remove(self, container: Dict) -> bool:
|
||||
"""Remove an existing container from a Blender scene.
|
||||
metadata["libpath"] = str(libpath)
|
||||
metadata["representation"] = str(representation["_id"])
|
||||
|
||||
def exec_remove(self, container: Dict) -> bool:
|
||||
"""Remove an existing asset group from a Blender scene.
|
||||
|
||||
Arguments:
|
||||
container (openpype:container-1.0): Container to remove,
|
||||
from `host.ls()`.
|
||||
|
||||
Returns:
|
||||
bool: Whether the container was deleted.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
bool: Whether the asset group was deleted.
|
||||
"""
|
||||
object_name = container["objectName"]
|
||||
asset_group = bpy.data.objects.get(object_name)
|
||||
libpath = asset_group.get(AVALON_PROPERTY).get('libpath')
|
||||
|
||||
collection = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
if not collection:
|
||||
# Check how many assets use the same library
|
||||
count = 0
|
||||
for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects:
|
||||
if obj.get(AVALON_PROPERTY).get('libpath') == libpath:
|
||||
count += 1
|
||||
|
||||
if not asset_group:
|
||||
return False
|
||||
assert not (collection.children), (
|
||||
"Nested collections are not supported."
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
self._remove(asset_group)
|
||||
|
||||
obj_container = plugin.get_local_collection_with_name(
|
||||
collection_metadata["obj_container"].name
|
||||
)
|
||||
objects = obj_container.all_objects
|
||||
bpy.data.objects.remove(asset_group)
|
||||
|
||||
self._remove(objects, obj_container)
|
||||
|
||||
bpy.data.collections.remove(collection)
|
||||
# If it is the last object to use that library, remove it
|
||||
if count == 1:
|
||||
library = bpy.data.libraries.get(bpy.path.basename(libpath))
|
||||
bpy.data.libraries.remove(library)
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import json
|
|||
|
||||
import pyblish.api
|
||||
from avalon.blender.pipeline import AVALON_PROPERTY
|
||||
from avalon.blender.pipeline import AVALON_INSTANCES
|
||||
|
||||
|
||||
class CollectInstances(pyblish.api.ContextPlugin):
|
||||
|
|
@ -14,6 +15,20 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
label = "Collect Instances"
|
||||
order = pyblish.api.CollectorOrder
|
||||
|
||||
@staticmethod
|
||||
def get_asset_groups() -> Generator:
|
||||
"""Return all 'model' collections.
|
||||
|
||||
Check if the family is 'model' and if it doesn't have the
|
||||
representation set. If the representation is set, it is a loaded model
|
||||
and we don't want to publish it.
|
||||
"""
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
for obj in instances.objects:
|
||||
avalon_prop = obj.get(AVALON_PROPERTY) or dict()
|
||||
if avalon_prop.get('id') == 'pyblish.avalon.instance':
|
||||
yield obj
|
||||
|
||||
@staticmethod
|
||||
def get_collections() -> Generator:
|
||||
"""Return all 'model' collections.
|
||||
|
|
@ -29,8 +44,35 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
|
||||
def process(self, context):
|
||||
"""Collect the models from the current Blender scene."""
|
||||
asset_groups = self.get_asset_groups()
|
||||
collections = self.get_collections()
|
||||
|
||||
for group in asset_groups:
|
||||
avalon_prop = group[AVALON_PROPERTY]
|
||||
asset = avalon_prop['asset']
|
||||
family = avalon_prop['family']
|
||||
subset = avalon_prop['subset']
|
||||
task = avalon_prop['task']
|
||||
name = f"{asset}_{subset}"
|
||||
instance = context.create_instance(
|
||||
name=name,
|
||||
family=family,
|
||||
families=[family],
|
||||
subset=subset,
|
||||
asset=asset,
|
||||
task=task,
|
||||
)
|
||||
objects = list(group.children)
|
||||
members = set()
|
||||
for obj in objects:
|
||||
objects.extend(list(obj.children))
|
||||
members.add(obj)
|
||||
members.add(group)
|
||||
instance[:] = list(members)
|
||||
self.log.debug(json.dumps(instance.data, indent=4))
|
||||
for obj in instance:
|
||||
self.log.debug(obj)
|
||||
|
||||
for collection in collections:
|
||||
avalon_prop = collection[AVALON_PROPERTY]
|
||||
asset = avalon_prop['asset']
|
||||
|
|
@ -47,6 +89,12 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
task=task,
|
||||
)
|
||||
members = list(collection.objects)
|
||||
if family == "animation":
|
||||
for obj in collection.objects:
|
||||
if obj.type == 'EMPTY' and obj.get(AVALON_PROPERTY):
|
||||
for child in obj.children:
|
||||
if child.type == 'ARMATURE':
|
||||
members.append(child)
|
||||
members.append(collection)
|
||||
instance[:] = members
|
||||
self.log.debug(json.dumps(instance.data, indent=4))
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
import os
|
||||
|
||||
import openpype.api
|
||||
import openpype.hosts.blender.api.plugin
|
||||
from openpype import api
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from avalon.blender.pipeline import AVALON_PROPERTY
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
class ExtractABC(openpype.api.Extractor):
|
||||
class ExtractABC(api.Extractor):
|
||||
"""Extract as ABC."""
|
||||
|
||||
label = "Extract ABC"
|
||||
hosts = ["blender"]
|
||||
families = ["model"]
|
||||
families = ["model", "pointcache"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
# Define extract output file path
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.fbx"
|
||||
filename = f"{instance.name}.abc"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
context = bpy.context
|
||||
|
|
@ -28,58 +28,29 @@ class ExtractABC(openpype.api.Extractor):
|
|||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
|
||||
collections = [
|
||||
obj for obj in instance if type(obj) is bpy.types.Collection]
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
assert len(collections) == 1, "There should be one and only one " \
|
||||
"collection collected for this asset"
|
||||
|
||||
old_active_layer_collection = view_layer.active_layer_collection
|
||||
|
||||
layers = view_layer.layer_collection.children
|
||||
|
||||
# Get the layer collection from the collection we need to export.
|
||||
# This is needed because in Blender you can only set the active
|
||||
# collection with the layer collection, and there is no way to get
|
||||
# the layer collection from the collection
|
||||
# (but there is the vice versa).
|
||||
layer_collections = [
|
||||
layer for layer in layers if layer.collection == collections[0]]
|
||||
|
||||
assert len(layer_collections) == 1
|
||||
|
||||
view_layer.active_layer_collection = layer_collections[0]
|
||||
|
||||
old_scale = scene.unit_settings.scale_length
|
||||
|
||||
selected = list()
|
||||
selected = []
|
||||
asset_group = None
|
||||
|
||||
for obj in instance:
|
||||
try:
|
||||
obj.select_set(True)
|
||||
selected.append(obj)
|
||||
except:
|
||||
continue
|
||||
obj.select_set(True)
|
||||
selected.append(obj)
|
||||
if obj.get(AVALON_PROPERTY):
|
||||
asset_group = obj
|
||||
|
||||
new_context = openpype.hosts.blender.api.plugin.create_blender_context(
|
||||
active=selected[0], selected=selected)
|
||||
|
||||
# We set the scale of the scene for the export
|
||||
scene.unit_settings.scale_length = 0.01
|
||||
|
||||
self.log.info(new_context)
|
||||
context = plugin.create_blender_context(
|
||||
active=asset_group, selected=selected)
|
||||
|
||||
# We export the abc
|
||||
bpy.ops.wm.alembic_export(
|
||||
new_context,
|
||||
context,
|
||||
filepath=filepath,
|
||||
start=1,
|
||||
end=1
|
||||
selected=True,
|
||||
flatten=False
|
||||
)
|
||||
|
||||
view_layer.active_layer_collection = old_active_layer_collection
|
||||
|
||||
scene.unit_settings.scale_length = old_scale
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
import openpype.api
|
||||
import pyblish.api
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
class ExtractSetDress(openpype.api.Extractor):
|
||||
"""Extract setdress."""
|
||||
|
||||
label = "Extract SetDress"
|
||||
hosts = ["blender"]
|
||||
families = ["setdress"]
|
||||
optional = True
|
||||
order = pyblish.api.ExtractorOrder + 0.1
|
||||
|
||||
def process(self, instance):
|
||||
stagingdir = self.staging_dir(instance)
|
||||
|
||||
json_data = []
|
||||
|
||||
for i in instance.context:
|
||||
collection = i.data.get("name")
|
||||
container = None
|
||||
for obj in bpy.data.collections[collection].objects:
|
||||
if obj.type == "ARMATURE":
|
||||
container_name = obj.get("avalon").get("container_name")
|
||||
container = bpy.data.collections[container_name]
|
||||
if container:
|
||||
json_dict = {
|
||||
"subset": i.data.get("subset"),
|
||||
"container": container.name,
|
||||
}
|
||||
json_dict["instance_name"] = container.get("avalon").get(
|
||||
"instance_name"
|
||||
)
|
||||
json_data.append(json_dict)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
json_filename = f"{instance.name}.json"
|
||||
json_path = os.path.join(stagingdir, json_filename)
|
||||
|
||||
with open(json_path, "w+") as file:
|
||||
json.dump(json_data, fp=file, indent=2)
|
||||
|
||||
json_representation = {
|
||||
"name": "json",
|
||||
"ext": "json",
|
||||
"files": json_filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(json_representation)
|
||||
|
||||
self.log.info(
|
||||
"Extracted instance '{}' to: {}".format(instance.name,
|
||||
json_representation)
|
||||
)
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import os
|
||||
import avalon.blender.workio
|
||||
|
||||
import bpy
|
||||
|
||||
# import avalon.blender.workio
|
||||
import openpype.api
|
||||
|
||||
|
||||
|
|
@ -9,7 +11,7 @@ class ExtractBlend(openpype.api.Extractor):
|
|||
|
||||
label = "Extract Blend"
|
||||
hosts = ["blender"]
|
||||
families = ["model", "camera", "rig", "action", "layout", "animation"]
|
||||
families = ["model", "camera", "rig", "action", "layout"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
|
|
@ -22,15 +24,12 @@ class ExtractBlend(openpype.api.Extractor):
|
|||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
|
||||
# Just save the file to a temporary location. At least for now it's no
|
||||
# problem to have (possibly) extra stuff in the file.
|
||||
avalon.blender.workio.save_file(filepath, copy=True)
|
||||
#
|
||||
# # Store reference for integration
|
||||
# if "files" not in instance.data:
|
||||
# instance.data["files"] = list()
|
||||
#
|
||||
# # instance.data["files"].append(filename)
|
||||
data_blocks = set()
|
||||
|
||||
for obj in instance:
|
||||
data_blocks.add(obj)
|
||||
|
||||
bpy.data.libraries.write(filepath, data_blocks)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import os
|
||||
|
||||
import bpy
|
||||
|
||||
import openpype.api
|
||||
|
||||
|
||||
class ExtractBlendAnimation(openpype.api.Extractor):
|
||||
"""Extract a blend file."""
|
||||
|
||||
label = "Extract Blend"
|
||||
hosts = ["blender"]
|
||||
families = ["animation"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
# Define extract output file path
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.blend"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
|
||||
data_blocks = set()
|
||||
|
||||
for obj in instance:
|
||||
if isinstance(obj, bpy.types.Object) and obj.type == 'EMPTY':
|
||||
child = obj.children[0]
|
||||
if child and child.type == 'ARMATURE':
|
||||
if not obj.animation_data:
|
||||
obj.animation_data_create()
|
||||
obj.animation_data.action = child.animation_data.action
|
||||
obj.animation_data_clear()
|
||||
data_blocks.add(child.animation_data.action)
|
||||
data_blocks.add(obj)
|
||||
|
||||
bpy.data.libraries.write(filepath, data_blocks)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'blend',
|
||||
'ext': 'blend',
|
||||
'files': filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
import os
|
||||
|
||||
import openpype.api
|
||||
from openpype import api
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from avalon.blender.pipeline import AVALON_PROPERTY
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
class ExtractFBX(openpype.api.Extractor):
|
||||
class ExtractFBX(api.Extractor):
|
||||
"""Extract as FBX."""
|
||||
|
||||
label = "Extract FBX"
|
||||
|
|
@ -15,56 +17,56 @@ class ExtractFBX(openpype.api.Extractor):
|
|||
|
||||
def process(self, instance):
|
||||
# Define extract output file path
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.fbx"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
context = bpy.context
|
||||
scene = context.scene
|
||||
view_layer = context.view_layer
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
|
||||
collections = [
|
||||
obj for obj in instance if type(obj) is bpy.types.Collection]
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
assert len(collections) == 1, "There should be one and only one " \
|
||||
"collection collected for this asset"
|
||||
selected = []
|
||||
asset_group = None
|
||||
|
||||
old_active_layer_collection = view_layer.active_layer_collection
|
||||
for obj in instance:
|
||||
obj.select_set(True)
|
||||
selected.append(obj)
|
||||
if obj.get(AVALON_PROPERTY):
|
||||
asset_group = obj
|
||||
|
||||
layers = view_layer.layer_collection.children
|
||||
context = plugin.create_blender_context(
|
||||
active=asset_group, selected=selected)
|
||||
|
||||
# Get the layer collection from the collection we need to export.
|
||||
# This is needed because in Blender you can only set the active
|
||||
# collection with the layer collection, and there is no way to get
|
||||
# the layer collection from the collection
|
||||
# (but there is the vice versa).
|
||||
layer_collections = [
|
||||
layer for layer in layers if layer.collection == collections[0]]
|
||||
new_materials = []
|
||||
new_materials_objs = []
|
||||
objects = list(asset_group.children)
|
||||
|
||||
assert len(layer_collections) == 1
|
||||
|
||||
view_layer.active_layer_collection = layer_collections[0]
|
||||
|
||||
old_scale = scene.unit_settings.scale_length
|
||||
|
||||
# We set the scale of the scene for the export
|
||||
scene.unit_settings.scale_length = 0.01
|
||||
for obj in objects:
|
||||
objects.extend(obj.children)
|
||||
if obj.type == 'MESH' and len(obj.data.materials) == 0:
|
||||
mat = bpy.data.materials.new(obj.name)
|
||||
obj.data.materials.append(mat)
|
||||
new_materials.append(mat)
|
||||
new_materials_objs.append(obj)
|
||||
|
||||
# We export the fbx
|
||||
bpy.ops.export_scene.fbx(
|
||||
context,
|
||||
filepath=filepath,
|
||||
use_active_collection=True,
|
||||
use_active_collection=False,
|
||||
use_selection=True,
|
||||
mesh_smooth_type='FACE',
|
||||
add_leaf_bones=False
|
||||
)
|
||||
|
||||
view_layer.active_layer_collection = old_active_layer_collection
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
scene.unit_settings.scale_length = old_scale
|
||||
for mat in new_materials:
|
||||
bpy.data.materials.remove(mat)
|
||||
|
||||
for obj in new_materials_objs:
|
||||
obj.data.materials.pop()
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import os
|
||||
|
||||
import openpype.api
|
||||
import json
|
||||
|
||||
import bpy
|
||||
import bpy_extras
|
||||
import bpy_extras.anim_utils
|
||||
|
||||
from openpype import api
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from avalon.blender.pipeline import AVALON_PROPERTY
|
||||
|
||||
class ExtractAnimationFBX(openpype.api.Extractor):
|
||||
|
||||
class ExtractAnimationFBX(api.Extractor):
|
||||
"""Extract as animation."""
|
||||
|
||||
label = "Extract FBX"
|
||||
|
|
@ -19,33 +22,26 @@ class ExtractAnimationFBX(openpype.api.Extractor):
|
|||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
|
||||
context = bpy.context
|
||||
scene = context.scene
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
|
||||
collections = [
|
||||
obj for obj in instance if type(obj) is bpy.types.Collection]
|
||||
# The first collection object in the instance is taken, as there
|
||||
# should be only one that contains the asset group.
|
||||
collection = [
|
||||
obj for obj in instance if type(obj) is bpy.types.Collection][0]
|
||||
|
||||
assert len(collections) == 1, "There should be one and only one " \
|
||||
"collection collected for this asset"
|
||||
# Again, the first object in the collection is taken , as there
|
||||
# should be only the asset group in the collection.
|
||||
asset_group = collection.objects[0]
|
||||
|
||||
old_scale = scene.unit_settings.scale_length
|
||||
armature = [
|
||||
obj for obj in asset_group.children if obj.type == 'ARMATURE'][0]
|
||||
|
||||
# We set the scale of the scene for the export
|
||||
scene.unit_settings.scale_length = 0.01
|
||||
|
||||
armatures = [
|
||||
obj for obj in collections[0].objects if obj.type == 'ARMATURE']
|
||||
|
||||
assert len(collections) == 1, "There should be one and only one " \
|
||||
"armature collected for this asset"
|
||||
|
||||
armature = armatures[0]
|
||||
asset_group_name = asset_group.name
|
||||
asset_group.name = asset_group.get(AVALON_PROPERTY).get("asset_name")
|
||||
|
||||
armature_name = armature.name
|
||||
original_name = armature_name.split(':')[0]
|
||||
original_name = armature_name.split(':')[1]
|
||||
armature.name = original_name
|
||||
|
||||
object_action_pairs = []
|
||||
|
|
@ -88,27 +84,29 @@ class ExtractAnimationFBX(openpype.api.Extractor):
|
|||
for obj in bpy.data.objects:
|
||||
obj.select_set(False)
|
||||
|
||||
asset_group.select_set(True)
|
||||
armature.select_set(True)
|
||||
fbx_filename = f"{instance.name}_{armature.name}.fbx"
|
||||
filepath = os.path.join(stagingdir, fbx_filename)
|
||||
|
||||
override = bpy.context.copy()
|
||||
override['selected_objects'] = [armature]
|
||||
override = plugin.create_blender_context(
|
||||
active=asset_group, selected=[asset_group, armature])
|
||||
bpy.ops.export_scene.fbx(
|
||||
override,
|
||||
filepath=filepath,
|
||||
use_active_collection=False,
|
||||
use_selection=True,
|
||||
bake_anim_use_nla_strips=False,
|
||||
bake_anim_use_all_actions=False,
|
||||
add_leaf_bones=False,
|
||||
armature_nodetype='ROOT',
|
||||
object_types={'ARMATURE'}
|
||||
object_types={'EMPTY', 'ARMATURE'}
|
||||
)
|
||||
armature.name = armature_name
|
||||
asset_group.name = asset_group_name
|
||||
asset_group.select_set(False)
|
||||
armature.select_set(False)
|
||||
|
||||
scene.unit_settings.scale_length = old_scale
|
||||
|
||||
# We delete the baked action and set the original one back
|
||||
for i in range(0, len(object_action_pairs)):
|
||||
pair = object_action_pairs[i]
|
||||
|
|
@ -121,6 +119,27 @@ class ExtractAnimationFBX(openpype.api.Extractor):
|
|||
pair[1].user_clear()
|
||||
bpy.data.actions.remove(pair[1])
|
||||
|
||||
json_filename = f"{instance.name}.json"
|
||||
json_path = os.path.join(stagingdir, json_filename)
|
||||
|
||||
json_dict = {
|
||||
"instance_name": asset_group.get(AVALON_PROPERTY).get("namespace")
|
||||
}
|
||||
|
||||
# collection = instance.data.get("name")
|
||||
# container = None
|
||||
# for obj in bpy.data.collections[collection].objects:
|
||||
# if obj.type == "ARMATURE":
|
||||
# container_name = obj.get("avalon").get("container_name")
|
||||
# container = bpy.data.collections[container_name]
|
||||
# if container:
|
||||
# json_dict = {
|
||||
# "instance_name": container.get("avalon").get("instance_name")
|
||||
# }
|
||||
|
||||
with open(json_path, "w+") as file:
|
||||
json.dump(json_dict, fp=file, indent=2)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
|
|
@ -130,7 +149,14 @@ class ExtractAnimationFBX(openpype.api.Extractor):
|
|||
'files': fbx_filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
json_representation = {
|
||||
'name': 'json',
|
||||
'ext': 'json',
|
||||
'files': json_filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(fbx_representation)
|
||||
instance.data["representations"].append(json_representation)
|
||||
|
||||
self.log.info("Extracted instance '{}' to: {}".format(
|
||||
instance.name, fbx_representation))
|
||||
|
|
|
|||
90
openpype/hosts/blender/plugins/publish/extract_layout.py
Normal file
90
openpype/hosts/blender/plugins/publish/extract_layout.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
import bpy
|
||||
|
||||
from avalon import io
|
||||
from avalon.blender.pipeline import AVALON_PROPERTY
|
||||
import openpype.api
|
||||
|
||||
|
||||
class ExtractLayout(openpype.api.Extractor):
|
||||
"""Extract a layout."""
|
||||
|
||||
label = "Extract Layout"
|
||||
hosts = ["blender"]
|
||||
families = ["layout"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
|
||||
json_data = []
|
||||
|
||||
asset_group = bpy.data.objects[str(instance)]
|
||||
|
||||
for asset in asset_group.children:
|
||||
metadata = asset.get(AVALON_PROPERTY)
|
||||
|
||||
parent = metadata["parent"]
|
||||
family = metadata["family"]
|
||||
|
||||
self.log.debug("Parent: {}".format(parent))
|
||||
blend = io.find_one(
|
||||
{
|
||||
"type": "representation",
|
||||
"parent": io.ObjectId(parent),
|
||||
"name": "blend"
|
||||
},
|
||||
projection={"_id": True})
|
||||
blend_id = blend["_id"]
|
||||
|
||||
json_element = {}
|
||||
json_element["reference"] = str(blend_id)
|
||||
json_element["family"] = family
|
||||
json_element["instance_name"] = asset.name
|
||||
json_element["asset_name"] = metadata["asset_name"]
|
||||
json_element["file_path"] = metadata["libpath"]
|
||||
|
||||
json_element["transform"] = {
|
||||
"translation": {
|
||||
"x": asset.location.x,
|
||||
"y": asset.location.y,
|
||||
"z": asset.location.z
|
||||
},
|
||||
"rotation": {
|
||||
"x": asset.rotation_euler.x,
|
||||
"y": asset.rotation_euler.y,
|
||||
"z": asset.rotation_euler.z,
|
||||
},
|
||||
"scale": {
|
||||
"x": asset.scale.x,
|
||||
"y": asset.scale.y,
|
||||
"z": asset.scale.z
|
||||
}
|
||||
}
|
||||
json_data.append(json_element)
|
||||
|
||||
json_filename = "{}.json".format(instance.name)
|
||||
json_path = os.path.join(stagingdir, json_filename)
|
||||
|
||||
with open(json_path, "w+") as file:
|
||||
json.dump(json_data, fp=file, indent=2)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'json',
|
||||
'ext': 'json',
|
||||
'files': json_filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
from typing import List
|
||||
|
||||
import pyblish.api
|
||||
import openpype.hosts.blender.api.action
|
||||
|
||||
|
||||
class ValidateNoColonsInName(pyblish.api.InstancePlugin):
|
||||
"""There cannot be colons in names
|
||||
|
||||
Object or bone names cannot include colons. Other software do not
|
||||
handle colons correctly.
|
||||
|
||||
"""
|
||||
|
||||
order = openpype.api.ValidateContentsOrder
|
||||
hosts = ["blender"]
|
||||
families = ["model", "rig"]
|
||||
version = (0, 1, 0)
|
||||
label = "No Colons in names"
|
||||
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance) -> List:
|
||||
invalid = []
|
||||
for obj in [obj for obj in instance]:
|
||||
if ':' in obj.name:
|
||||
invalid.append(obj)
|
||||
if obj.type == 'ARMATURE':
|
||||
for bone in obj.data.bones:
|
||||
if ':' in bone.name:
|
||||
invalid.append(obj)
|
||||
break
|
||||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
f"Objects found with colon in name: {invalid}")
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
from typing import List
|
||||
|
||||
import mathutils
|
||||
|
||||
import pyblish.api
|
||||
import openpype.hosts.blender.api.action
|
||||
|
||||
|
||||
class ValidateTransformZero(pyblish.api.InstancePlugin):
|
||||
"""Transforms can't have any values
|
||||
|
||||
To solve this issue, try freezing the transforms. So long
|
||||
as the transforms, rotation and scale values are zero,
|
||||
you're all good.
|
||||
|
||||
"""
|
||||
|
||||
order = openpype.api.ValidateContentsOrder
|
||||
hosts = ["blender"]
|
||||
families = ["model"]
|
||||
category = "geometry"
|
||||
version = (0, 1, 0)
|
||||
label = "Transform Zero"
|
||||
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]
|
||||
|
||||
_identity = mathutils.Matrix()
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance) -> List:
|
||||
invalid = []
|
||||
for obj in [obj for obj in instance]:
|
||||
if obj.matrix_basis != cls._identity:
|
||||
invalid.append(obj)
|
||||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
f"Object found in instance is not in Object Mode: {invalid}")
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import os
|
||||
|
||||
|
||||
def add_implementation_envs(env, _app):
|
||||
"""Modify environments to contain all required for implementation."""
|
||||
openharmony_path = os.path.join(
|
||||
os.environ["OPENPYPE_REPOS_ROOT"], "pype", "vendor", "OpenHarmony"
|
||||
)
|
||||
# TODO check if is already set? What to do if is already set?
|
||||
env["LIB_OPENHARMONY_PATH"] = openharmony_path
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import re
|
||||
|
||||
from openpype import lib
|
||||
from openpype.api import (get_current_project_settings)
|
||||
|
|
@ -63,26 +64,9 @@ def get_asset_settings():
|
|||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"resolutionWidth": resolution_width,
|
||||
"resolutionHeight": resolution_height
|
||||
"resolutionHeight": resolution_height,
|
||||
"entityType": entity_type
|
||||
}
|
||||
settings = get_current_project_settings()
|
||||
|
||||
try:
|
||||
skip_resolution_check = \
|
||||
settings["harmony"]["general"]["skip_resolution_check"]
|
||||
skip_timelines_check = \
|
||||
settings["harmony"]["general"]["skip_timelines_check"]
|
||||
except KeyError:
|
||||
skip_resolution_check = []
|
||||
skip_timelines_check = []
|
||||
|
||||
if os.getenv('AVALON_TASK') in skip_resolution_check:
|
||||
scene_data.pop("resolutionWidth")
|
||||
scene_data.pop("resolutionHeight")
|
||||
|
||||
if entity_type in skip_timelines_check:
|
||||
scene_data.pop('frameStart', None)
|
||||
scene_data.pop('frameEnd', None)
|
||||
|
||||
return scene_data
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"""Validate scene settings."""
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
|
@ -41,22 +42,42 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin):
|
|||
families = ["workfile"]
|
||||
hosts = ["harmony"]
|
||||
actions = [ValidateSceneSettingsRepair]
|
||||
optional = True
|
||||
|
||||
frame_check_filter = ["_ch_", "_pr_", "_intd_", "_extd_"]
|
||||
# used for skipping resolution validation for render tasks
|
||||
render_check_filter = ["render", "Render"]
|
||||
# skip frameEnd check if asset contains any of:
|
||||
frame_check_filter = ["_ch_", "_pr_", "_intd_", "_extd_"] # regex
|
||||
|
||||
# skip resolution check if Task name matches any of regex patterns
|
||||
skip_resolution_check = ["render", "Render"] # regex
|
||||
|
||||
# skip frameStart, frameEnd check if Task name matches any of regex patt.
|
||||
skip_timelines_check = [] # regex
|
||||
|
||||
def process(self, instance):
|
||||
"""Plugin entry point."""
|
||||
expected_settings = openpype.hosts.harmony.api.get_asset_settings()
|
||||
self.log.info(expected_settings)
|
||||
self.log.info("scene settings from DB:".format(expected_settings))
|
||||
|
||||
expected_settings = _update_frames(dict.copy(expected_settings))
|
||||
expected_settings["frameEndHandle"] = expected_settings["frameEnd"] +\
|
||||
expected_settings["handleEnd"]
|
||||
|
||||
if any(string in instance.context.data['anatomyData']['asset']
|
||||
for string in self.frame_check_filter):
|
||||
if (any(re.search(pattern, os.getenv('AVALON_TASK'))
|
||||
for pattern in 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)
|
||||
for pattern in self.skip_timelines_check)):
|
||||
expected_settings.pop('frameStart', None)
|
||||
expected_settings.pop('frameEnd', None)
|
||||
|
||||
expected_settings.pop("entityType") # not useful after the check
|
||||
|
||||
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")
|
||||
|
||||
# handle case where ftrack uses only two decimal places
|
||||
|
|
@ -66,13 +87,7 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin):
|
|||
fps = float(
|
||||
"{:.2f}".format(instance.context.data.get("frameRate")))
|
||||
|
||||
if any(string in instance.context.data['anatomyData']['task']
|
||||
for string in self.render_check_filter):
|
||||
self.log.debug("Render task detected, resolution check skipped")
|
||||
expected_settings.pop("resolutionWidth")
|
||||
expected_settings.pop("resolutionHeight")
|
||||
|
||||
self.log.debug(expected_settings)
|
||||
self.log.debug("filtered settings: {}".format(expected_settings))
|
||||
|
||||
current_settings = {
|
||||
"fps": fps,
|
||||
|
|
@ -84,7 +99,7 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin):
|
|||
"resolutionWidth": instance.context.data.get("resolutionWidth"),
|
||||
"resolutionHeight": instance.context.data.get("resolutionHeight"),
|
||||
}
|
||||
self.log.debug("curr:: {}".format(current_settings))
|
||||
self.log.debug("current scene settings {}".format(current_settings))
|
||||
|
||||
invalid_settings = []
|
||||
for key, value in expected_settings.items():
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import os
|
||||
import platform
|
||||
|
||||
|
||||
def add_implementation_envs(env, _app):
|
||||
# Add requirements to HIERO_PLUGIN_PATH
|
||||
pype_root = os.environ["OPENPYPE_REPOS_ROOT"]
|
||||
new_hiero_paths = [
|
||||
os.path.join(pype_root, "openpype", "hosts", "hiero", "startup")
|
||||
]
|
||||
old_hiero_path = env.get("HIERO_PLUGIN_PATH") or ""
|
||||
for path in old_hiero_path.split(os.pathsep):
|
||||
if not path or not os.path.exists(path):
|
||||
continue
|
||||
|
||||
norm_path = os.path.normpath(path)
|
||||
if norm_path not in new_hiero_paths:
|
||||
new_hiero_paths.append(norm_path)
|
||||
|
||||
env["HIERO_PLUGIN_PATH"] = os.pathsep.join(new_hiero_paths)
|
||||
|
||||
# Try to add QuickTime to PATH
|
||||
quick_time_path = "C:/Program Files (x86)/QuickTime/QTSystem"
|
||||
if platform.system() == "windows" and os.path.exists(quick_time_path):
|
||||
path_value = env.get("PATH") or ""
|
||||
path_paths = [
|
||||
path
|
||||
for path in path_value.split(os.pathsep)
|
||||
if path
|
||||
]
|
||||
path_paths.append(quick_time_path)
|
||||
env["PATH"] = os.pathsep.join(path_paths)
|
||||
|
||||
# Set default values if are not already set via settings
|
||||
defaults = {
|
||||
"LOGLEVEL": "DEBUG"
|
||||
}
|
||||
for key, value in defaults.items():
|
||||
if not env.get(key):
|
||||
env[key] = value
|
||||
|
|
@ -22,6 +22,7 @@ from .pipeline import (
|
|||
)
|
||||
|
||||
from .lib import (
|
||||
pype_tag_name,
|
||||
get_track_items,
|
||||
get_current_project,
|
||||
get_current_sequence,
|
||||
|
|
@ -73,6 +74,7 @@ __all__ = [
|
|||
"work_root",
|
||||
|
||||
# Lib functions
|
||||
"pype_tag_name",
|
||||
"get_track_items",
|
||||
"get_current_project",
|
||||
"get_current_sequence",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@ import os
|
|||
import hiero.core.events
|
||||
import avalon.api as avalon
|
||||
from openpype.api import Logger
|
||||
from .lib import sync_avalon_data_to_workfile, launch_workfiles_app
|
||||
from .lib import (
|
||||
sync_avalon_data_to_workfile,
|
||||
launch_workfiles_app,
|
||||
selection_changed_timeline,
|
||||
before_project_save
|
||||
)
|
||||
from .tags import add_tags_to_workfile
|
||||
from .menu import update_menu_task_label
|
||||
|
||||
|
|
@ -78,7 +83,7 @@ def register_hiero_events():
|
|||
"Registering events for: kBeforeNewProjectCreated, "
|
||||
"kAfterNewProjectCreated, kBeforeProjectLoad, kAfterProjectLoad, "
|
||||
"kBeforeProjectSave, kAfterProjectSave, kBeforeProjectClose, "
|
||||
"kAfterProjectClose, kShutdown, kStartup"
|
||||
"kAfterProjectClose, kShutdown, kStartup, kSelectionChanged"
|
||||
)
|
||||
|
||||
# hiero.core.events.registerInterest(
|
||||
|
|
@ -91,8 +96,8 @@ def register_hiero_events():
|
|||
hiero.core.events.registerInterest(
|
||||
"kAfterProjectLoad", afterProjectLoad)
|
||||
|
||||
# hiero.core.events.registerInterest(
|
||||
# "kBeforeProjectSave", beforeProjectSaved)
|
||||
hiero.core.events.registerInterest(
|
||||
"kBeforeProjectSave", before_project_save)
|
||||
# hiero.core.events.registerInterest(
|
||||
# "kAfterProjectSave", afterProjectSaved)
|
||||
#
|
||||
|
|
@ -104,10 +109,16 @@ def register_hiero_events():
|
|||
# hiero.core.events.registerInterest("kShutdown", shutDown)
|
||||
# hiero.core.events.registerInterest("kStartup", startupCompleted)
|
||||
|
||||
# workfiles
|
||||
hiero.core.events.registerEventType("kStartWorkfiles")
|
||||
hiero.core.events.registerInterest("kStartWorkfiles", launch_workfiles_app)
|
||||
hiero.core.events.registerInterest(
|
||||
("kSelectionChanged", "kTimeline"), selection_changed_timeline)
|
||||
|
||||
# workfiles
|
||||
try:
|
||||
hiero.core.events.registerEventType("kStartWorkfiles")
|
||||
hiero.core.events.registerInterest(
|
||||
"kStartWorkfiles", launch_workfiles_app)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def register_events():
|
||||
"""
|
||||
|
|
|
|||
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