mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into feature/blender-review
This commit is contained in:
commit
6754e97490
962 changed files with 38669 additions and 14887 deletions
15
.github/pull_request_template.md
vendored
15
.github/pull_request_template.md
vendored
|
|
@ -1,16 +1,9 @@
|
|||
## Brief description
|
||||
First sentence is brief description.
|
||||
|
||||
## Description
|
||||
Next paragraf is more elaborate text with more info. This will be displayed for example in collapsed form under the first sentence in a changelog.
|
||||
## Changelog Description
|
||||
Paragraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation.
|
||||
|
||||
## Additional info
|
||||
The rest will be ignored in changelog and should contain any additional
|
||||
technical information.
|
||||
|
||||
## Documentation (add _"type: documentation"_ label)
|
||||
[feature_documentation](future_url_after_it_will_be_merged)
|
||||
Paragraphs of text giving context of additional technical information or code examples.
|
||||
|
||||
## Testing notes:
|
||||
1. start with this step
|
||||
2. follow this step
|
||||
2. follow this step
|
||||
|
|
|
|||
19
.github/workflows/automate-projects.yml
vendored
19
.github/workflows/automate-projects.yml
vendored
|
|
@ -1,19 +0,0 @@
|
|||
name: Automate Projects
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, labeled]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
jobs:
|
||||
assign_one_project:
|
||||
runs-on: ubuntu-latest
|
||||
name: Assign to One Project
|
||||
steps:
|
||||
- name: Assign NEW bugs to triage
|
||||
uses: srggrs/assign-one-project-github-action@1.2.0
|
||||
if: contains(github.event.issue.labels.*.name, 'bug')
|
||||
with:
|
||||
project: 'https://github.com/pypeclub/pype/projects/2'
|
||||
column_name: 'Needs triage'
|
||||
6
.github/workflows/milestone_assign.yml
vendored
6
.github/workflows/milestone_assign.yml
vendored
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
if: github.event.pull_request.milestone == null
|
||||
uses: zoispag/action-assign-milestone@v1
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
repo-token: "${{ secrets.YNPUT_BOT_TOKEN }}"
|
||||
milestone: 'next-minor'
|
||||
|
||||
run_if_develop:
|
||||
|
|
@ -24,5 +24,5 @@ jobs:
|
|||
if: github.event.pull_request.milestone == null
|
||||
uses: zoispag/action-assign-milestone@v1
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
milestone: 'next-patch'
|
||||
repo-token: "${{ secrets.YNPUT_BOT_TOKEN }}"
|
||||
milestone: 'next-patch'
|
||||
|
|
|
|||
8
.github/workflows/milestone_create.yml
vendored
8
.github/workflows/milestone_create.yml
vendored
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
uses: "WyriHaximus/github-action-get-milestones@master"
|
||||
id: milestones
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}"
|
||||
|
||||
- run: printf "name=number::%s" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[] | select(.title == $MILESTONE) | .number')
|
||||
id: querymilestone
|
||||
|
|
@ -31,7 +31,7 @@ jobs:
|
|||
with:
|
||||
title: 'next-patch'
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}"
|
||||
|
||||
generate-next-minor:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -40,7 +40,7 @@ jobs:
|
|||
uses: "WyriHaximus/github-action-get-milestones@master"
|
||||
id: milestones
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}"
|
||||
|
||||
- run: printf "name=number::%s" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[] | select(.title == $MILESTONE) | .number')
|
||||
id: querymilestone
|
||||
|
|
@ -59,4 +59,4 @@ jobs:
|
|||
with:
|
||||
title: 'next-minor'
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}"
|
||||
|
|
|
|||
47
.github/workflows/miletone_release_trigger.yml
vendored
Normal file
47
.github/workflows/miletone_release_trigger.yml
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
name: Milestone Release [trigger]
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
milestone:
|
||||
required: true
|
||||
release-type:
|
||||
type: choice
|
||||
description: What release should be created
|
||||
options:
|
||||
- release
|
||||
- pre-release
|
||||
milestone:
|
||||
types: closed
|
||||
|
||||
|
||||
jobs:
|
||||
milestone-title:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
milestone: ${{ steps.milestoneTitle.outputs.value }}
|
||||
steps:
|
||||
- name: Switch input milestone
|
||||
uses: haya14busa/action-cond@v1
|
||||
id: milestoneTitle
|
||||
with:
|
||||
cond: ${{ inputs.milestone == '' }}
|
||||
if_true: ${{ github.event.milestone.title }}
|
||||
if_false: ${{ inputs.milestone }}
|
||||
- name: Print resulted milestone
|
||||
run: |
|
||||
echo "${{ steps.milestoneTitle.outputs.value }}"
|
||||
|
||||
call-ci-tools-milestone-release:
|
||||
needs: milestone-title
|
||||
uses: ynput/ci-tools/.github/workflows/milestone_release_ref.yml@main
|
||||
with:
|
||||
milestone: ${{ needs.milestone-title.outputs.milestone }}
|
||||
repo-owner: ${{ github.event.repository.owner.login }}
|
||||
repo-name: ${{ github.event.repository.name }}
|
||||
version-py-path: "./openpype/version.py"
|
||||
pyproject-path: "./pyproject.toml"
|
||||
secrets:
|
||||
token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
user_email: ${{ secrets.CI_EMAIL }}
|
||||
user_name: ${{ secrets.CI_USER }}
|
||||
6
.github/workflows/nightly_merge.yml
vendored
6
.github/workflows/nightly_merge.yml
vendored
|
|
@ -14,10 +14,10 @@ jobs:
|
|||
- name: 🚛 Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: 🔨 Merge develop to main
|
||||
- name: 🔨 Merge develop to main
|
||||
uses: everlytic/branch-merge@1.1.0
|
||||
with:
|
||||
github_token: ${{ secrets.ADMIN_TOKEN }}
|
||||
github_token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
source_ref: 'develop'
|
||||
target_branch: 'main'
|
||||
commit_message_template: '[Automated] Merged {source_ref} into {target_branch}'
|
||||
|
|
@ -26,4 +26,4 @@ jobs:
|
|||
uses: benc-uk/workflow-dispatch@v1
|
||||
with:
|
||||
workflow: Nightly Prerelease
|
||||
token: ${{ secrets.ADMIN_TOKEN }}
|
||||
token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
|
|
|
|||
42
.github/workflows/prerelease.yml
vendored
42
.github/workflows/prerelease.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install Python requirements
|
||||
run: pip install gitpython semver PyGithub
|
||||
|
|
@ -25,43 +25,15 @@ jobs:
|
|||
- name: 🔎 Determine next version type
|
||||
id: version_type
|
||||
run: |
|
||||
TYPE=$(python ./tools/ci_tools.py --bump --github_token ${{ secrets.GITHUB_TOKEN }})
|
||||
|
||||
echo ::set-output name=type::$TYPE
|
||||
TYPE=$(python ./tools/ci_tools.py --bump --github_token ${{ secrets.YNPUT_BOT_TOKEN }})
|
||||
echo "type=${TYPE}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 💉 Inject new version into files
|
||||
id: version
|
||||
if: steps.version_type.outputs.type != 'skip'
|
||||
run: |
|
||||
RESULT=$(python ./tools/ci_tools.py --nightly --github_token ${{ secrets.GITHUB_TOKEN }})
|
||||
|
||||
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.3
|
||||
# with:
|
||||
# token: ${{ secrets.ADMIN_TOKEN }}
|
||||
# addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}'
|
||||
# issues: false
|
||||
# issuesWoLabels: false
|
||||
# sinceTag: "3.12.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
|
||||
NEW_VERSION_TAG=$(python ./tools/ci_tools.py --nightly --github_token ${{ secrets.YNPUT_BOT_TOKEN }})
|
||||
echo "next_tag=${NEW_VERSION_TAG}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 💾 Commit and Tag
|
||||
id: git_commit
|
||||
|
|
@ -80,7 +52,7 @@ jobs:
|
|||
- name: Push to protected main branch
|
||||
uses: CasperWA/push-protected@v2.10.0
|
||||
with:
|
||||
token: ${{ secrets.ADMIN_TOKEN }}
|
||||
token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
branch: main
|
||||
tags: true
|
||||
unprotect_reviews: true
|
||||
|
|
@ -89,7 +61,7 @@ jobs:
|
|||
uses: everlytic/branch-merge@1.1.0
|
||||
if: steps.version_type.outputs.type != 'skip'
|
||||
with:
|
||||
github_token: ${{ secrets.ADMIN_TOKEN }}
|
||||
github_token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
source_ref: 'main'
|
||||
target_branch: 'develop'
|
||||
commit_message_template: '[Automated] Merged {source_ref} into {target_branch}'
|
||||
|
|
|
|||
124
.github/workflows/release.yml
vendored
124
.github/workflows/release.yml
vendored
|
|
@ -1,124 +0,0 @@
|
|||
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 PyGithub
|
||||
|
||||
- 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.3
|
||||
# with:
|
||||
# token: ${{ secrets.ADMIN_TOKEN }}
|
||||
# addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}'
|
||||
# issues: false
|
||||
# issuesWoLabels: false
|
||||
# sinceTag: "3.12.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.10.0
|
||||
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 }}
|
||||
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}'
|
||||
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}'
|
||||
30
.github/workflows/test_build.yml
vendored
30
.github/workflows/test_build.yml
vendored
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.7]
|
||||
python-version: [3.9]
|
||||
|
||||
steps:
|
||||
- name: 🚛 Checkout Code
|
||||
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
||||
- name: 🧵 Install Requirements
|
||||
shell: pwsh
|
||||
run: |
|
||||
|
|
@ -45,7 +45,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.7]
|
||||
python-version: [3.9]
|
||||
|
||||
steps:
|
||||
- name: 🚛 Checkout Code
|
||||
|
|
@ -64,27 +64,3 @@ jobs:
|
|||
run: |
|
||||
export SKIP_THIRD_PARTY_VALIDATION="1"
|
||||
./tools/build.sh
|
||||
|
||||
# MacOS-latest:
|
||||
|
||||
# runs-on: macos-latest
|
||||
# strategy:
|
||||
# matrix:
|
||||
# python-version: [3.7]
|
||||
|
||||
# steps:
|
||||
# - name: 🚛 Checkout Code
|
||||
# uses: actions/checkout@v2
|
||||
|
||||
# - name: Set up Python
|
||||
# uses: actions/setup-python@v2
|
||||
# with:
|
||||
# python-version: ${{ matrix.python-version }}
|
||||
|
||||
# - name: 🧵 Install Requirements
|
||||
# run: |
|
||||
# ./tools/create_env.sh
|
||||
|
||||
# - name: 🔨 Build
|
||||
# run: |
|
||||
# ./tools/build.sh
|
||||
12
.pre-commit-config.yaml
Normal file
12
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: no-commit-to-branch
|
||||
args: [ '--pattern', '^(?!((release|enhancement|feature|bugfix|documentation|tests|local|chore)\/[a-zA-Z0-9\-_]+)$).*' ]
|
||||
2157
CHANGELOG.md
2157
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
# Build Pype docker image
|
||||
FROM ubuntu:focal AS builder
|
||||
ARG OPENPYPE_PYTHON_VERSION=3.7.12
|
||||
ARG OPENPYPE_PYTHON_VERSION=3.9.12
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Build Pype docker image
|
||||
FROM centos:7 AS builder
|
||||
ARG OPENPYPE_PYTHON_VERSION=3.7.12
|
||||
ARG OPENPYPE_PYTHON_VERSION=3.9.12
|
||||
|
||||
LABEL org.opencontainers.image.name="pypeclub/openpype"
|
||||
LABEL org.opencontainers.image.title="OpenPype Docker Image"
|
||||
|
|
@ -96,11 +96,11 @@ RUN source $HOME/.bashrc \
|
|||
RUN source $HOME/.bashrc \
|
||||
&& bash ./tools/build.sh
|
||||
|
||||
RUN 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 \
|
||||
&& cp /root/.pyenv/versions/${OPENPYPE_PYTHON_VERSION}/lib/libpython* ./build/exe.linux-x86_64-3.7/lib \
|
||||
&& cp /usr/lib64/libxcb* ./build/exe.linux-x86_64-3.7/vendor/python/PySide2/Qt/lib
|
||||
RUN cp /usr/lib64/libffi* ./build/exe.linux-x86_64-3.9/lib \
|
||||
&& cp /usr/lib64/libssl* ./build/exe.linux-x86_64-3.9/lib \
|
||||
&& cp /usr/lib64/libcrypto* ./build/exe.linux-x86_64-3.9/lib \
|
||||
&& cp /root/.pyenv/versions/${OPENPYPE_PYTHON_VERSION}/lib/libpython* ./build/exe.linux-x86_64-3.9/lib \
|
||||
&& cp /usr/lib64/libxcb* ./build/exe.linux-x86_64-3.9/vendor/python/PySide2/Qt/lib
|
||||
|
||||
RUN cd /opt/openpype \
|
||||
rm -rf ./vendor/bin
|
||||
|
|
|
|||
81
Dockerfile.debian
Normal file
81
Dockerfile.debian
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# Build Pype docker image
|
||||
FROM debian:bullseye AS builder
|
||||
ARG OPENPYPE_PYTHON_VERSION=3.9.12
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION
|
||||
|
||||
LABEL maintainer="info@openpype.io"
|
||||
LABEL description="Docker Image to build and run OpenPype under Ubuntu 20.04"
|
||||
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/OpenPype"
|
||||
LABEL org.opencontainers.image.documentation="https://openpype.io/docs/system_introduction"
|
||||
LABEL org.opencontainers.image.created=$BUILD_DATE
|
||||
LABEL org.opencontainers.image.version=$VERSION
|
||||
|
||||
USER root
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# update base
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
bash \
|
||||
git \
|
||||
cmake \
|
||||
make \
|
||||
curl \
|
||||
wget \
|
||||
build-essential \
|
||||
libssl-dev \
|
||||
zlib1g-dev \
|
||||
libbz2-dev \
|
||||
libreadline-dev \
|
||||
libsqlite3-dev \
|
||||
llvm \
|
||||
libncursesw5-dev \
|
||||
xz-utils \
|
||||
tk-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libffi-dev \
|
||||
liblzma-dev \
|
||||
patchelf
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
|
||||
RUN mkdir /opt/openpype
|
||||
|
||||
# download and install pyenv
|
||||
RUN curl https://pyenv.run | bash \
|
||||
&& echo 'export PATH="$HOME/.pyenv/bin:$PATH"'>> $HOME/init_pyenv.sh \
|
||||
&& echo 'eval "$(pyenv init -)"' >> $HOME/init_pyenv.sh \
|
||||
&& echo 'eval "$(pyenv virtualenv-init -)"' >> $HOME/init_pyenv.sh \
|
||||
&& echo 'eval "$(pyenv init --path)"' >> $HOME/init_pyenv.sh
|
||||
|
||||
# install python with pyenv
|
||||
RUN source $HOME/init_pyenv.sh \
|
||||
&& pyenv install ${OPENPYPE_PYTHON_VERSION}
|
||||
|
||||
COPY . /opt/openpype/
|
||||
|
||||
RUN chmod +x /opt/openpype/tools/create_env.sh && chmod +x /opt/openpype/tools/build.sh
|
||||
|
||||
WORKDIR /opt/openpype
|
||||
|
||||
# set local python version
|
||||
RUN cd /opt/openpype \
|
||||
&& source $HOME/init_pyenv.sh \
|
||||
&& pyenv local ${OPENPYPE_PYTHON_VERSION}
|
||||
|
||||
# fetch third party tools/libraries
|
||||
RUN source $HOME/init_pyenv.sh \
|
||||
&& ./tools/create_env.sh \
|
||||
&& ./tools/fetch_thirdparty_libs.sh
|
||||
|
||||
# build openpype
|
||||
RUN source $HOME/init_pyenv.sh \
|
||||
&& bash ./tools/build.sh
|
||||
130
HISTORY.md
130
HISTORY.md
|
|
@ -1,5 +1,135 @@
|
|||
# Changelog
|
||||
|
||||
## [3.15.0](https://github.com/ynput/OpenPype/tree/3.15.0)
|
||||
|
||||
[Full Changelog](https://github.com/ynput/OpenPype/compare/3.14.10...3.15.0)
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
- General: Fill default values of new publish template profiles [\#4245](https://github.com/ynput/OpenPype/pull/4245)
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- documentation: Split tools into separate entries [\#4342](https://github.com/ynput/OpenPype/pull/4342)
|
||||
- Documentation: Fix harmony docs [\#4301](https://github.com/ynput/OpenPype/pull/4301)
|
||||
- Remove staging logic set by OpenPype version [\#3979](https://github.com/ynput/OpenPype/pull/3979)
|
||||
|
||||
**🆕 New features**
|
||||
|
||||
- General: Push to studio library [\#4284](https://github.com/ynput/OpenPype/pull/4284)
|
||||
- Colorspace Management and Distribution [\#4195](https://github.com/ynput/OpenPype/pull/4195)
|
||||
- Nuke: refactor to latest publisher workfow [\#4006](https://github.com/ynput/OpenPype/pull/4006)
|
||||
- Update to Python 3.9 [\#3546](https://github.com/ynput/OpenPype/pull/3546)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Unreal: Don't use mongo queries in 'ExistingLayoutLoader' [\#4356](https://github.com/ynput/OpenPype/pull/4356)
|
||||
- General: Loader and Creator plugins can be disabled [\#4310](https://github.com/ynput/OpenPype/pull/4310)
|
||||
- General: Unbind poetry version [\#4306](https://github.com/ynput/OpenPype/pull/4306)
|
||||
- General: Enhanced enum def items [\#4295](https://github.com/ynput/OpenPype/pull/4295)
|
||||
- Git: add pre-commit hooks [\#4289](https://github.com/ynput/OpenPype/pull/4289)
|
||||
- Tray Publisher: Improve Online family functionality [\#4263](https://github.com/ynput/OpenPype/pull/4263)
|
||||
- General: Update MacOs to PySide6 [\#4255](https://github.com/ynput/OpenPype/pull/4255)
|
||||
- Build: update to Gazu in toml [\#4208](https://github.com/ynput/OpenPype/pull/4208)
|
||||
- Global: adding imageio to settings [\#4158](https://github.com/ynput/OpenPype/pull/4158)
|
||||
- Blender: added project settings for validator no colons in name [\#4149](https://github.com/ynput/OpenPype/pull/4149)
|
||||
- Dockerfile for Debian Bullseye [\#4108](https://github.com/ynput/OpenPype/pull/4108)
|
||||
- AfterEffects: publish multiple compositions [\#4092](https://github.com/ynput/OpenPype/pull/4092)
|
||||
- AfterEffects: make new publisher default [\#4056](https://github.com/ynput/OpenPype/pull/4056)
|
||||
- Photoshop: make new publisher default [\#4051](https://github.com/ynput/OpenPype/pull/4051)
|
||||
- Feature/multiverse [\#4046](https://github.com/ynput/OpenPype/pull/4046)
|
||||
- Tests: add support for deadline for automatic tests [\#3989](https://github.com/ynput/OpenPype/pull/3989)
|
||||
- Add version to shortcut name [\#3906](https://github.com/ynput/OpenPype/pull/3906)
|
||||
- TrayPublisher: Removed from experimental tools [\#3667](https://github.com/ynput/OpenPype/pull/3667)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- change 3.7 to 3.9 in folder name [\#4354](https://github.com/ynput/OpenPype/pull/4354)
|
||||
- PushToProject: Fix hierarchy of project change [\#4350](https://github.com/ynput/OpenPype/pull/4350)
|
||||
- Fix photoshop workfile save-as [\#4347](https://github.com/ynput/OpenPype/pull/4347)
|
||||
- Nuke Input process node sourcing improvements [\#4341](https://github.com/ynput/OpenPype/pull/4341)
|
||||
- New publisher: Some validation plugin tweaks [\#4339](https://github.com/ynput/OpenPype/pull/4339)
|
||||
- Harmony: fix unable to change workfile on Mac [\#4334](https://github.com/ynput/OpenPype/pull/4334)
|
||||
- Global: fixing in-place source publishing for editorial [\#4333](https://github.com/ynput/OpenPype/pull/4333)
|
||||
- General: Use class constants of QMessageBox [\#4332](https://github.com/ynput/OpenPype/pull/4332)
|
||||
- TVPaint: Fix plugin for TVPaint 11.7 [\#4328](https://github.com/ynput/OpenPype/pull/4328)
|
||||
- Exctract OTIO review has improved quality [\#4325](https://github.com/ynput/OpenPype/pull/4325)
|
||||
- Ftrack: fix typos causing bugs in sync [\#4322](https://github.com/ynput/OpenPype/pull/4322)
|
||||
- General: Python 2 compatibility of instance collector [\#4320](https://github.com/ynput/OpenPype/pull/4320)
|
||||
- Slack: user groups speedup [\#4318](https://github.com/ynput/OpenPype/pull/4318)
|
||||
- Maya: Bug - Multiverse extractor executed on plain animation family [\#4315](https://github.com/ynput/OpenPype/pull/4315)
|
||||
- Fix run\_documentation.ps1 [\#4312](https://github.com/ynput/OpenPype/pull/4312)
|
||||
- Nuke: new creators fixes [\#4308](https://github.com/ynput/OpenPype/pull/4308)
|
||||
- General: missing comment on standalone and tray publisher [\#4303](https://github.com/ynput/OpenPype/pull/4303)
|
||||
- AfterEffects: Fix for audio from mp4 layer [\#4296](https://github.com/ynput/OpenPype/pull/4296)
|
||||
- General: Update gazu in poetry lock [\#4247](https://github.com/ynput/OpenPype/pull/4247)
|
||||
- Bug: Fixing version detection and filtering in Igniter [\#3914](https://github.com/ynput/OpenPype/pull/3914)
|
||||
- Bug: Create missing version dir [\#3903](https://github.com/ynput/OpenPype/pull/3903)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
- Remove redundant export\_alembic method. [\#4293](https://github.com/ynput/OpenPype/pull/4293)
|
||||
- Igniter: Use qtpy modules instead of Qt [\#4237](https://github.com/ynput/OpenPype/pull/4237)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Sort families by alphabetical order in the Create plugin [\#4346](https://github.com/ynput/OpenPype/pull/4346)
|
||||
- Global: Validate unique subsets [\#4336](https://github.com/ynput/OpenPype/pull/4336)
|
||||
- Maya: Collect instances preserve handles even if frameStart + frameEnd matches context [\#3437](https://github.com/ynput/OpenPype/pull/3437)
|
||||
|
||||
|
||||
## [3.14.10](https://github.com/ynput/OpenPype/tree/3.14.10)
|
||||
|
||||
[Full Changelog](https://github.com/ynput/OpenPype/compare/3.14.9...3.14.10)
|
||||
|
||||
**🆕 New features**
|
||||
|
||||
- Global | Nuke: Creator placeholders in workfile template builder [\#4266](https://github.com/ynput/OpenPype/pull/4266)
|
||||
- Slack: Added dynamic message [\#4265](https://github.com/ynput/OpenPype/pull/4265)
|
||||
- Blender: Workfile Loader [\#4234](https://github.com/ynput/OpenPype/pull/4234)
|
||||
- Unreal: Publishing and Loading for UAssets [\#4198](https://github.com/ynput/OpenPype/pull/4198)
|
||||
- Publish: register publishes without copying them [\#4157](https://github.com/ynput/OpenPype/pull/4157)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- General: Added install method with docstring to HostBase [\#4298](https://github.com/ynput/OpenPype/pull/4298)
|
||||
- Traypublisher: simple editorial multiple edl [\#4248](https://github.com/ynput/OpenPype/pull/4248)
|
||||
- General: Extend 'IPluginPaths' to have more available methods [\#4214](https://github.com/ynput/OpenPype/pull/4214)
|
||||
- Refactorization of folder coloring [\#4211](https://github.com/ynput/OpenPype/pull/4211)
|
||||
- Flame - loading multilayer with controlled layer names [\#4204](https://github.com/ynput/OpenPype/pull/4204)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- Unreal: fix missing `maintained_selection` call [\#4300](https://github.com/ynput/OpenPype/pull/4300)
|
||||
- Ftrack: Fix receive of host ip on MacOs [\#4288](https://github.com/ynput/OpenPype/pull/4288)
|
||||
- SiteSync: sftp connection failing when shouldnt be tested [\#4278](https://github.com/ynput/OpenPype/pull/4278)
|
||||
- Deadline: fix default value for passing mongo url [\#4275](https://github.com/ynput/OpenPype/pull/4275)
|
||||
- Scene Manager: Fix variable name [\#4268](https://github.com/ynput/OpenPype/pull/4268)
|
||||
- Slack: notification fails because of missing published path [\#4264](https://github.com/ynput/OpenPype/pull/4264)
|
||||
- hiero: creator gui with min max [\#4257](https://github.com/ynput/OpenPype/pull/4257)
|
||||
- NiceCheckbox: Fix checker positioning in Python 2 [\#4253](https://github.com/ynput/OpenPype/pull/4253)
|
||||
- Publisher: Fix 'CreatorType' not equal for Python 2 DCCs [\#4249](https://github.com/ynput/OpenPype/pull/4249)
|
||||
- Deadline: fix dependencies [\#4242](https://github.com/ynput/OpenPype/pull/4242)
|
||||
- Houdini: hotfix instance data access [\#4236](https://github.com/ynput/OpenPype/pull/4236)
|
||||
- bugfix/image plane load error [\#4222](https://github.com/ynput/OpenPype/pull/4222)
|
||||
- Hiero: thumbnail from multilayer exr [\#4209](https://github.com/ynput/OpenPype/pull/4209)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
- Resolve: Use qtpy in Resolve [\#4254](https://github.com/ynput/OpenPype/pull/4254)
|
||||
- Houdini: Use qtpy in Houdini [\#4252](https://github.com/ynput/OpenPype/pull/4252)
|
||||
- Max: Use qtpy in Max [\#4251](https://github.com/ynput/OpenPype/pull/4251)
|
||||
- Maya: Use qtpy in Maya [\#4250](https://github.com/ynput/OpenPype/pull/4250)
|
||||
- Hiero: Use qtpy in Hiero [\#4240](https://github.com/ynput/OpenPype/pull/4240)
|
||||
- Nuke: Use qtpy in Nuke [\#4239](https://github.com/ynput/OpenPype/pull/4239)
|
||||
- Flame: Use qtpy in flame [\#4238](https://github.com/ynput/OpenPype/pull/4238)
|
||||
- General: Legacy io not used in global plugins [\#4134](https://github.com/ynput/OpenPype/pull/4134)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Bump json5 from 1.0.1 to 1.0.2 in /website [\#4292](https://github.com/ynput/OpenPype/pull/4292)
|
||||
- Maya: Fix validate frame range repair + fix create render with deadline disabled [\#4279](https://github.com/ynput/OpenPype/pull/4279)
|
||||
|
||||
|
||||
## [3.14.9](https://github.com/pypeclub/OpenPype/tree/3.14.9)
|
||||
|
||||
|
|
|
|||
32
README.md
32
README.md
|
|
@ -5,8 +5,7 @@
|
|||
OpenPype
|
||||
====
|
||||
|
||||
[](https://github.com/pypeclub/pype/actions/workflows/documentation.yml) 
|
||||
|
||||
[](https://github.com/pypeclub/pype/actions/workflows/documentation.yml) 
|
||||
|
||||
|
||||
Introduction
|
||||
|
|
@ -31,7 +30,7 @@ The main things you will need to run and build OpenPype are:
|
|||
- **Terminal** in your OS
|
||||
- PowerShell 5.0+ (Windows)
|
||||
- Bash (Linux)
|
||||
- [**Python 3.7.8**](#python) or higher
|
||||
- [**Python 3.9.6**](#python) or higher
|
||||
- [**MongoDB**](#database) (needed only for local development)
|
||||
|
||||
|
||||
|
|
@ -50,13 +49,14 @@ For more details on requirements visit [requirements documentation](https://open
|
|||
Building OpenPype
|
||||
-------------
|
||||
|
||||
To build OpenPype you currently need [Python 3.7](https://www.python.org/downloads/) as we are following
|
||||
To build OpenPype you currently need [Python 3.9](https://www.python.org/downloads/) as we are following
|
||||
[vfx platform](https://vfxplatform.com). Because of some Linux distros comes with newer Python version
|
||||
already, you need to install **3.7** version and make use of it. You can use perhaps [pyenv](https://github.com/pyenv/pyenv) for this on Linux.
|
||||
already, you need to install **3.9** version and make use of it. You can use perhaps [pyenv](https://github.com/pyenv/pyenv) for this on Linux.
|
||||
**Note**: We do not support 3.9.0 because of [this bug](https://github.com/python/cpython/pull/22670). Please, use higher versions of 3.9.x.
|
||||
|
||||
### Windows
|
||||
|
||||
You will need [Python 3.7](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads).
|
||||
You will need [Python >= 3.9.1](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads).
|
||||
More tools might be needed for installing dependencies (for example for **OpenTimelineIO**) - mostly
|
||||
development tools like [CMake](https://cmake.org/) and [Visual Studio](https://visualstudio.microsoft.com/cs/downloads/)
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ OpenPype is build using [CX_Freeze](https://cx-freeze.readthedocs.io/en/latest)
|
|||
|
||||
### macOS
|
||||
|
||||
You will need [Python 3.7](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads). You'll need also other tools to build
|
||||
You will need [Python >= 3.9](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads). You'll need also other tools to build
|
||||
some OpenPype dependencies like [CMake](https://cmake.org/) and **XCode Command Line Tools** (or some other build system).
|
||||
|
||||
Easy way of installing everything necessary is to use [Homebrew](https://brew.sh):
|
||||
|
|
@ -106,19 +106,19 @@ exec "$SHELL"
|
|||
PATH=$(pyenv root)/shims:$PATH
|
||||
```
|
||||
|
||||
4) Pull in required Python version 3.7.x
|
||||
4) Pull in required Python version 3.9.x
|
||||
```sh
|
||||
# install Python build dependences
|
||||
brew install openssl readline sqlite3 xz zlib
|
||||
|
||||
# replace with up-to-date 3.7.x version
|
||||
pyenv install 3.7.9
|
||||
# replace with up-to-date 3.9.x version
|
||||
pyenv install 3.9.6
|
||||
```
|
||||
|
||||
5) Set local Python version
|
||||
```sh
|
||||
# switch to OpenPype source directory
|
||||
pyenv local 3.7.9
|
||||
pyenv local 3.9.6
|
||||
```
|
||||
|
||||
#### To build OpenPype:
|
||||
|
|
@ -145,7 +145,7 @@ sudo ./tools/docker_build.sh centos7
|
|||
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.
|
||||
You will need [Python >= 3.9](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).
|
||||
|
||||
|
|
@ -222,14 +222,14 @@ eval "$(pyenv virtualenv-init -)"
|
|||
# reload shell
|
||||
exec $SHELL
|
||||
|
||||
# install Python 3.7.9
|
||||
pyenv install -v 3.7.9
|
||||
# install Python 3.9.x
|
||||
pyenv install -v 3.9.6
|
||||
|
||||
# change path to OpenPype 3
|
||||
cd /path/to/openpype-3
|
||||
|
||||
# set local python version
|
||||
pyenv local 3.7.9
|
||||
pyenv local 3.9.6
|
||||
|
||||
```
|
||||
</details>
|
||||
|
|
@ -345,4 +345,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import enlighten
|
||||
import os
|
||||
import re
|
||||
import urllib
|
||||
|
|
@ -252,6 +251,11 @@ class RemoteFileHandler:
|
|||
if key.startswith('download_warning'):
|
||||
return value
|
||||
|
||||
# handle antivirus warning for big zips
|
||||
found = re.search("(confirm=)([^&.+])", response.text)
|
||||
if found:
|
||||
return found.groups()[1]
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -259,15 +263,9 @@ class RemoteFileHandler:
|
|||
response_gen, destination,
|
||||
):
|
||||
with open(destination, "wb") as f:
|
||||
pbar = enlighten.Counter(
|
||||
total=None, desc="Save content", units="%", color="green")
|
||||
progress = 0
|
||||
for chunk in response_gen:
|
||||
if chunk: # filter out keep-alive new chunks
|
||||
f.write(chunk)
|
||||
progress += len(chunk)
|
||||
|
||||
pbar.close()
|
||||
|
||||
@staticmethod
|
||||
def _quota_exceeded(first_chunk):
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ def open_dialog():
|
|||
if os.getenv("OPENPYPE_HEADLESS_MODE"):
|
||||
print("!!! Can't open dialog in headless mode. Exiting.")
|
||||
sys.exit(1)
|
||||
from Qt import QtWidgets, QtCore
|
||||
from qtpy import QtWidgets, QtCore
|
||||
from .install_dialog import InstallDialog
|
||||
|
||||
scale_attr = getattr(QtCore.Qt, "AA_EnableHighDpiScaling", None)
|
||||
|
|
@ -47,7 +47,7 @@ def open_update_window(openpype_version):
|
|||
if os.getenv("OPENPYPE_HEADLESS_MODE"):
|
||||
print("!!! Can't open dialog in headless mode. Exiting.")
|
||||
sys.exit(1)
|
||||
from Qt import QtWidgets, QtCore
|
||||
from qtpy import QtWidgets, QtCore
|
||||
from .update_window import UpdateWindow
|
||||
|
||||
scale_attr = getattr(QtCore.Qt, "AA_EnableHighDpiScaling", None)
|
||||
|
|
@ -71,7 +71,7 @@ def show_message_dialog(title, message):
|
|||
if os.getenv("OPENPYPE_HEADLESS_MODE"):
|
||||
print("!!! Can't open dialog in headless mode. Exiting.")
|
||||
sys.exit(1)
|
||||
from Qt import QtWidgets, QtCore
|
||||
from qtpy import QtWidgets, QtCore
|
||||
from .message_dialog import MessageDialog
|
||||
|
||||
scale_attr = getattr(QtCore.Qt, "AA_EnableHighDpiScaling", None)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
"""Open install dialog."""
|
||||
|
||||
import sys
|
||||
from Qt import QtWidgets # noqa
|
||||
from Qt.QtCore import Signal # noqa
|
||||
from qtpy import QtWidgets
|
||||
|
||||
from .install_dialog import InstallDialog
|
||||
|
||||
|
|
|
|||
|
|
@ -57,11 +57,9 @@ class OpenPypeVersion(semver.VersionInfo):
|
|||
"""Class for storing information about OpenPype version.
|
||||
|
||||
Attributes:
|
||||
staging (bool): True if it is staging version
|
||||
path (str): path to OpenPype
|
||||
|
||||
"""
|
||||
staging = False
|
||||
path = None
|
||||
# this should match any string complying with https://semver.org/
|
||||
_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>[a-zA-Z\d\-.]*))?(?:\+(?P<buildmetadata>[a-zA-Z\d\-.]*))?") # noqa: E501
|
||||
|
|
@ -83,12 +81,10 @@ class OpenPypeVersion(semver.VersionInfo):
|
|||
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.
|
||||
|
||||
"""
|
||||
self.path = None
|
||||
self.staging = False
|
||||
|
||||
if "version" in kwargs.keys():
|
||||
if not kwargs.get("version"):
|
||||
|
|
@ -113,29 +109,8 @@ class OpenPypeVersion(semver.VersionInfo):
|
|||
if "path" in kwargs.keys():
|
||||
kwargs.pop("path")
|
||||
|
||||
if kwargs.get("staging"):
|
||||
self.staging = kwargs.get("staging", False)
|
||||
kwargs.pop("staging")
|
||||
|
||||
if "staging" in kwargs.keys():
|
||||
kwargs.pop("staging")
|
||||
|
||||
if self.staging:
|
||||
if kwargs.get("build"):
|
||||
if "staging" not in kwargs.get("build"):
|
||||
kwargs["build"] = f"{kwargs.get('build')}-staging"
|
||||
else:
|
||||
kwargs["build"] = "staging"
|
||||
|
||||
if kwargs.get("build") and "staging" in kwargs.get("build", ""):
|
||||
self.staging = True
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __eq__(self, other):
|
||||
result = super().__eq__(other)
|
||||
return bool(result and self.staging == other.staging)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}: {str(self)} - path={self.path}>"
|
||||
|
||||
|
|
@ -150,43 +125,11 @@ class OpenPypeVersion(semver.VersionInfo):
|
|||
return True
|
||||
|
||||
if self.finalize_version() == other.finalize_version() and \
|
||||
self.prerelease == other.prerelease and \
|
||||
self.is_staging() and not other.is_staging():
|
||||
self.prerelease == other.prerelease:
|
||||
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.staging
|
||||
|
||||
def get_main_version(self) -> str:
|
||||
"""Return main version component.
|
||||
|
||||
|
|
@ -218,21 +161,8 @@ class OpenPypeVersion(semver.VersionInfo):
|
|||
if not m:
|
||||
return None
|
||||
version = OpenPypeVersion.parse(string[m.start():m.end()])
|
||||
if "staging" in string[m.start():m.end()]:
|
||||
version.staging = True
|
||||
return 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):
|
||||
return hash(self.path) if self.path else hash(str(self))
|
||||
|
||||
|
|
@ -382,80 +312,28 @@ class OpenPypeVersion(semver.VersionInfo):
|
|||
return False
|
||||
|
||||
@classmethod
|
||||
def get_local_versions(
|
||||
cls, production: bool = None,
|
||||
staging: bool = None
|
||||
) -> List:
|
||||
def get_local_versions(cls) -> List:
|
||||
"""Get all versions available on this machine.
|
||||
|
||||
Arguments give ability to specify if filtering is needed. If both
|
||||
arguments are set to None all found versions are returned.
|
||||
|
||||
Args:
|
||||
production (bool): Return production versions.
|
||||
staging (bool): Return staging versions.
|
||||
|
||||
Returns:
|
||||
list: of compatible versions available on the machine.
|
||||
|
||||
"""
|
||||
# Return all local versions if arguments are set to None
|
||||
if production is None and staging is None:
|
||||
production = True
|
||||
staging = True
|
||||
|
||||
elif production is None and not staging:
|
||||
production = True
|
||||
|
||||
elif staging is None and not production:
|
||||
staging = True
|
||||
|
||||
# Just return empty output if both are disabled
|
||||
if not production and not staging:
|
||||
return []
|
||||
|
||||
# DEPRECATED: backwards compatible way to look for versions in root
|
||||
dir_to_search = Path(user_data_dir("openpype", "pypeclub"))
|
||||
versions = OpenPypeVersion.get_versions_from_directory(dir_to_search)
|
||||
|
||||
filtered_versions = []
|
||||
for version in versions:
|
||||
if version.is_staging():
|
||||
if staging:
|
||||
filtered_versions.append(version)
|
||||
elif production:
|
||||
filtered_versions.append(version)
|
||||
return list(sorted(set(filtered_versions)))
|
||||
return list(sorted(set(versions)))
|
||||
|
||||
@classmethod
|
||||
def get_remote_versions(
|
||||
cls, production: bool = None,
|
||||
staging: bool = None
|
||||
) -> List:
|
||||
def get_remote_versions(cls) -> List:
|
||||
"""Get all versions available in OpenPype Path.
|
||||
|
||||
Arguments give ability to specify if filtering is needed. If both
|
||||
arguments are set to None all found versions are returned.
|
||||
|
||||
Args:
|
||||
production (bool): Return production versions.
|
||||
staging (bool): Return staging versions.
|
||||
Returns:
|
||||
list of OpenPypeVersions: Versions found in OpenPype path.
|
||||
|
||||
"""
|
||||
# Return all local versions if arguments are set to None
|
||||
if production is None and staging is None:
|
||||
production = True
|
||||
staging = True
|
||||
|
||||
elif production is None and not staging:
|
||||
production = True
|
||||
|
||||
elif staging is None and not production:
|
||||
staging = True
|
||||
|
||||
# Just return empty output if both are disabled
|
||||
if not production and not staging:
|
||||
return []
|
||||
|
||||
dir_to_search = None
|
||||
if cls.openpype_path_is_accessible():
|
||||
|
|
@ -476,14 +354,7 @@ class OpenPypeVersion(semver.VersionInfo):
|
|||
|
||||
versions = cls.get_versions_from_directory(dir_to_search)
|
||||
|
||||
filtered_versions = []
|
||||
for version in versions:
|
||||
if version.is_staging():
|
||||
if staging:
|
||||
filtered_versions.append(version)
|
||||
elif production:
|
||||
filtered_versions.append(version)
|
||||
return list(sorted(set(filtered_versions)))
|
||||
return list(sorted(set(versions)))
|
||||
|
||||
@staticmethod
|
||||
def get_versions_from_directory(
|
||||
|
|
@ -562,7 +433,6 @@ class OpenPypeVersion(semver.VersionInfo):
|
|||
|
||||
@staticmethod
|
||||
def get_latest_version(
|
||||
staging: bool = False,
|
||||
local: bool = None,
|
||||
remote: bool = None
|
||||
) -> Union[OpenPypeVersion, None]:
|
||||
|
|
@ -571,7 +441,6 @@ class OpenPypeVersion(semver.VersionInfo):
|
|||
The version does not contain information about path and source.
|
||||
|
||||
This is utility version to get the latest version from all found.
|
||||
Build version is not listed if staging is enabled.
|
||||
|
||||
Arguments 'local' and 'remote' define if local and remote repository
|
||||
versions are used. All versions are used if both are not set (or set
|
||||
|
|
@ -580,7 +449,6 @@ class OpenPypeVersion(semver.VersionInfo):
|
|||
'False' in that case only build version can be used.
|
||||
|
||||
Args:
|
||||
staging (bool, optional): List staging versions if True.
|
||||
local (bool, optional): List local versions if True.
|
||||
remote (bool, optional): List remote versions if True.
|
||||
|
||||
|
|
@ -599,22 +467,9 @@ class OpenPypeVersion(semver.VersionInfo):
|
|||
remote = True
|
||||
|
||||
installed_version = OpenPypeVersion.get_installed_version()
|
||||
local_versions = []
|
||||
remote_versions = []
|
||||
if local:
|
||||
local_versions = OpenPypeVersion.get_local_versions(
|
||||
staging=staging
|
||||
)
|
||||
if remote:
|
||||
remote_versions = OpenPypeVersion.get_remote_versions(
|
||||
staging=staging
|
||||
)
|
||||
all_versions = local_versions + remote_versions
|
||||
if not staging:
|
||||
all_versions.append(installed_version)
|
||||
|
||||
if not all_versions:
|
||||
return None
|
||||
local_versions = OpenPypeVersion.get_local_versions() if local else []
|
||||
remote_versions = OpenPypeVersion.get_remote_versions() if remote else [] # noqa: E501
|
||||
all_versions = local_versions + remote_versions + [installed_version]
|
||||
|
||||
all_versions.sort()
|
||||
return all_versions[-1]
|
||||
|
|
@ -705,7 +560,7 @@ class BootstrapRepos:
|
|||
"""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-nightly.1+test)
|
||||
version_list (list of OpenPypeVersion): list of version to search.
|
||||
|
||||
Returns:
|
||||
|
|
@ -807,6 +662,8 @@ class BootstrapRepos:
|
|||
"""
|
||||
version = OpenPypeVersion.version_in_str(zip_file.name)
|
||||
destination_dir = self.data_dir / f"{version.major}.{version.minor}"
|
||||
if not destination_dir.exists():
|
||||
destination_dir.mkdir(parents=True)
|
||||
destination = destination_dir / zip_file.name
|
||||
|
||||
if destination.exists():
|
||||
|
|
@ -1131,14 +988,12 @@ class BootstrapRepos:
|
|||
|
||||
@staticmethod
|
||||
def find_openpype_version(
|
||||
version: Union[str, OpenPypeVersion],
|
||||
staging: bool
|
||||
version: Union[str, OpenPypeVersion]
|
||||
) -> Union[OpenPypeVersion, None]:
|
||||
"""Find location of specified OpenPype version.
|
||||
|
||||
Args:
|
||||
version (Union[str, OpenPypeVersion): Version to find.
|
||||
staging (bool): Filter staging versions.
|
||||
|
||||
Returns:
|
||||
requested OpenPypeVersion.
|
||||
|
|
@ -1151,9 +1006,7 @@ class BootstrapRepos:
|
|||
if installed_version == version:
|
||||
return installed_version
|
||||
|
||||
local_versions = OpenPypeVersion.get_local_versions(
|
||||
staging=staging, production=not staging
|
||||
)
|
||||
local_versions = OpenPypeVersion.get_local_versions()
|
||||
zip_version = None
|
||||
for local_version in local_versions:
|
||||
if local_version == version:
|
||||
|
|
@ -1165,37 +1018,25 @@ class BootstrapRepos:
|
|||
if zip_version is not None:
|
||||
return zip_version
|
||||
|
||||
remote_versions = OpenPypeVersion.get_remote_versions(
|
||||
staging=staging, production=not staging
|
||||
)
|
||||
for remote_version in remote_versions:
|
||||
if remote_version == version:
|
||||
return remote_version
|
||||
return None
|
||||
remote_versions = OpenPypeVersion.get_remote_versions()
|
||||
return next(
|
||||
(
|
||||
remote_version for remote_version in remote_versions
|
||||
if remote_version == version
|
||||
), None)
|
||||
|
||||
@staticmethod
|
||||
def find_latest_openpype_version(
|
||||
staging: bool
|
||||
) -> Union[OpenPypeVersion, None]:
|
||||
def find_latest_openpype_version() -> Union[OpenPypeVersion, None]:
|
||||
"""Find the latest available OpenPype version in all location.
|
||||
|
||||
Args:
|
||||
staging (bool): True to look for staging versions.
|
||||
|
||||
Returns:
|
||||
Latest OpenPype version on None if nothing was found.
|
||||
|
||||
"""
|
||||
installed_version = OpenPypeVersion.get_installed_version()
|
||||
local_versions = OpenPypeVersion.get_local_versions(
|
||||
staging=staging
|
||||
)
|
||||
remote_versions = OpenPypeVersion.get_remote_versions(
|
||||
staging=staging
|
||||
)
|
||||
all_versions = local_versions + remote_versions
|
||||
if not staging:
|
||||
all_versions.append(installed_version)
|
||||
local_versions = OpenPypeVersion.get_local_versions()
|
||||
remote_versions = OpenPypeVersion.get_remote_versions()
|
||||
all_versions = local_versions + remote_versions + [installed_version]
|
||||
|
||||
if not all_versions:
|
||||
return None
|
||||
|
|
@ -1215,7 +1056,6 @@ class BootstrapRepos:
|
|||
def find_openpype(
|
||||
self,
|
||||
openpype_path: Union[Path, str] = None,
|
||||
staging: bool = False,
|
||||
include_zips: bool = False
|
||||
) -> Union[List[OpenPypeVersion], None]:
|
||||
"""Get ordered dict of detected OpenPype version.
|
||||
|
|
@ -1229,8 +1069,6 @@ class BootstrapRepos:
|
|||
Args:
|
||||
openpype_path (Path or str, optional): Try to find OpenPype on
|
||||
the given path or url.
|
||||
staging (bool, optional): Filter only staging version, skip them
|
||||
otherwise.
|
||||
include_zips (bool, optional): If set True it will try to find
|
||||
OpenPype in zip files in given directory.
|
||||
|
||||
|
|
@ -1278,7 +1116,7 @@ class BootstrapRepos:
|
|||
for dir_to_search in dirs_to_search:
|
||||
try:
|
||||
openpype_versions += self.get_openpype_versions(
|
||||
dir_to_search, staging)
|
||||
dir_to_search)
|
||||
except ValueError:
|
||||
# location is invalid, skip it
|
||||
pass
|
||||
|
|
@ -1643,15 +1481,11 @@ class BootstrapRepos:
|
|||
return False
|
||||
return True
|
||||
|
||||
def get_openpype_versions(
|
||||
self,
|
||||
openpype_dir: Path,
|
||||
staging: bool = False) -> list:
|
||||
def get_openpype_versions(self, openpype_dir: Path) -> list:
|
||||
"""Get all detected OpenPype versions in directory.
|
||||
|
||||
Args:
|
||||
openpype_dir (Path): Directory to scan.
|
||||
staging (bool, optional): Find staging versions if True.
|
||||
|
||||
Returns:
|
||||
list of OpenPypeVersion
|
||||
|
|
@ -1669,8 +1503,7 @@ class BootstrapRepos:
|
|||
for item in openpype_dir.iterdir():
|
||||
# if the item is directory with major.minor version, dive deeper
|
||||
if item.is_dir() and re.match(r"^\d+\.\d+$", item.name):
|
||||
_versions = self.get_openpype_versions(
|
||||
item, staging=staging)
|
||||
_versions = self.get_openpype_versions(item)
|
||||
if _versions:
|
||||
openpype_versions += _versions
|
||||
|
||||
|
|
@ -1693,11 +1526,7 @@ class BootstrapRepos:
|
|||
continue
|
||||
|
||||
detected_version.path = item
|
||||
if staging and detected_version.is_staging():
|
||||
openpype_versions.append(detected_version)
|
||||
|
||||
if not staging and not detected_version.is_staging():
|
||||
openpype_versions.append(detected_version)
|
||||
openpype_versions.append(detected_version)
|
||||
|
||||
return sorted(openpype_versions)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ import sys
|
|||
import re
|
||||
import collections
|
||||
|
||||
from Qt import QtCore, QtGui, QtWidgets # noqa
|
||||
from Qt.QtGui import QValidator # noqa
|
||||
from Qt.QtCore import QTimer # noqa
|
||||
from qtpy import QtCore, QtGui, QtWidgets
|
||||
|
||||
from .install_thread import InstallThread
|
||||
from .tools import (
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import os
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from Qt.QtCore import QThread, Signal, QObject # noqa
|
||||
from qtpy import QtCore
|
||||
|
||||
from .bootstrap_repos import (
|
||||
BootstrapRepos,
|
||||
|
|
@ -17,7 +17,7 @@ from .bootstrap_repos import (
|
|||
from .tools import validate_mongo_connection
|
||||
|
||||
|
||||
class InstallThread(QThread):
|
||||
class InstallThread(QtCore.QThread):
|
||||
"""Install Worker thread.
|
||||
|
||||
This class takes care of finding OpenPype version on user entered path
|
||||
|
|
@ -28,15 +28,14 @@ class InstallThread(QThread):
|
|||
user data dir.
|
||||
|
||||
"""
|
||||
progress = Signal(int)
|
||||
message = Signal((str, bool))
|
||||
progress = QtCore.Signal(int)
|
||||
message = QtCore.Signal((str, bool))
|
||||
|
||||
def __init__(self, parent=None,):
|
||||
self._mongo = None
|
||||
self._path = None
|
||||
self._result = None
|
||||
|
||||
QThread.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
|
||||
def result(self):
|
||||
"""Result of finished installation."""
|
||||
|
|
@ -62,143 +61,117 @@ class InstallThread(QThread):
|
|||
progress_callback=self.set_progress, message=self.message)
|
||||
local_version = OpenPypeVersion.get_installed_version_str()
|
||||
|
||||
# if user did enter nothing, we install OpenPype from local version.
|
||||
# zip content of `repos`, copy it to user data dir and append
|
||||
# version to it.
|
||||
if not self._path:
|
||||
# user did not entered url
|
||||
if not self._mongo:
|
||||
# it not set in environment
|
||||
if not os.getenv("OPENPYPE_MONGO"):
|
||||
# try to get it from settings registry
|
||||
try:
|
||||
self._mongo = bs.secure_registry.get_item(
|
||||
"openPypeMongo")
|
||||
except ValueError:
|
||||
self.message.emit(
|
||||
"!!! We need MongoDB URL to proceed.", True)
|
||||
self._set_result(-1)
|
||||
return
|
||||
else:
|
||||
self._mongo = os.getenv("OPENPYPE_MONGO")
|
||||
else:
|
||||
self.message.emit("Saving mongo connection string ...", False)
|
||||
bs.secure_registry.set_item("openPypeMongo", self._mongo)
|
||||
|
||||
os.environ["OPENPYPE_MONGO"] = self._mongo
|
||||
|
||||
self.message.emit(
|
||||
f"Detecting installed OpenPype versions in {bs.data_dir}",
|
||||
False)
|
||||
detected = bs.find_openpype(include_zips=True)
|
||||
|
||||
if detected:
|
||||
if not OpenPypeVersion.get_installed_version().is_compatible(
|
||||
detected[-1]):
|
||||
self.message.emit((
|
||||
f"Latest detected version {detected[-1]} "
|
||||
"is not compatible with the currently running "
|
||||
f"{local_version}"
|
||||
), True)
|
||||
self.message.emit((
|
||||
"Filtering detected versions to compatible ones..."
|
||||
), False)
|
||||
|
||||
detected = [
|
||||
version for version in detected
|
||||
if version.is_compatible(
|
||||
OpenPypeVersion.get_installed_version())
|
||||
]
|
||||
|
||||
if OpenPypeVersion(
|
||||
version=local_version, path=Path()) < detected[-1]:
|
||||
self.message.emit((
|
||||
f"Latest installed version {detected[-1]} is newer "
|
||||
f"then currently running {local_version}"
|
||||
), False)
|
||||
self.message.emit("Skipping OpenPype install ...", False)
|
||||
if detected[-1].path.suffix.lower() == ".zip":
|
||||
bs.extract_openpype(detected[-1])
|
||||
self._set_result(0)
|
||||
return
|
||||
|
||||
if OpenPypeVersion(version=local_version).get_main_version() == detected[-1].get_main_version(): # noqa
|
||||
self.message.emit((
|
||||
f"Latest installed version is the same as "
|
||||
f"currently running {local_version}"
|
||||
), False)
|
||||
self.message.emit("Skipping OpenPype install ...", False)
|
||||
self._set_result(0)
|
||||
return
|
||||
|
||||
self.message.emit((
|
||||
"All installed versions are older then "
|
||||
f"currently running one {local_version}"
|
||||
), False)
|
||||
else:
|
||||
if getattr(sys, 'frozen', False):
|
||||
self.message.emit("None detected.", True)
|
||||
self.message.emit(("We will use OpenPype coming with "
|
||||
"installer."), False)
|
||||
openpype_version = bs.create_version_from_frozen_code()
|
||||
if not openpype_version:
|
||||
self.message.emit(
|
||||
f"!!! Install failed - {openpype_version}", True)
|
||||
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._set_result(1)
|
||||
return
|
||||
else:
|
||||
self.message.emit("None detected.", False)
|
||||
|
||||
self.message.emit(
|
||||
f"We will use local OpenPype version {local_version}", False)
|
||||
|
||||
local_openpype = bs.create_version_from_live_code()
|
||||
if not local_openpype:
|
||||
self.message.emit(
|
||||
f"!!! Install failed - {local_openpype}", True)
|
||||
self._set_result(-1)
|
||||
return
|
||||
# user did not entered url
|
||||
if self._mongo:
|
||||
self.message.emit("Saving mongo connection string ...", False)
|
||||
bs.secure_registry.set_item("openPypeMongo", self._mongo)
|
||||
|
||||
elif os.getenv("OPENPYPE_MONGO"):
|
||||
self._mongo = os.getenv("OPENPYPE_MONGO")
|
||||
else:
|
||||
# try to get it from settings registry
|
||||
try:
|
||||
bs.install_version(local_openpype)
|
||||
except (OpenPypeVersionExists,
|
||||
OpenPypeVersionInvalid,
|
||||
OpenPypeVersionIOError) as e:
|
||||
self.message.emit(f"Installed failed: ", True)
|
||||
self.message.emit(str(e), True)
|
||||
self._mongo = bs.secure_registry.get_item(
|
||||
"openPypeMongo")
|
||||
except ValueError:
|
||||
self.message.emit(
|
||||
"!!! We need MongoDB URL to proceed.", True)
|
||||
self._set_result(-1)
|
||||
return
|
||||
os.environ["OPENPYPE_MONGO"] = self._mongo
|
||||
|
||||
self.message.emit(f"Installed as {local_openpype}", False)
|
||||
self.message.emit(
|
||||
f"Detecting installed OpenPype versions in {bs.data_dir}",
|
||||
False)
|
||||
detected = bs.find_openpype(include_zips=True)
|
||||
if not detected and getattr(sys, 'frozen', False):
|
||||
self.message.emit("None detected.", True)
|
||||
self.message.emit(("We will use OpenPype coming with "
|
||||
"installer."), False)
|
||||
openpype_version = bs.create_version_from_frozen_code()
|
||||
if not openpype_version:
|
||||
self.message.emit(
|
||||
f"!!! Install failed - {openpype_version}", True)
|
||||
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._set_result(1)
|
||||
return
|
||||
else:
|
||||
# if we have mongo connection string, validate it, set it to
|
||||
# user settings and get OPENPYPE_PATH from there.
|
||||
if self._mongo:
|
||||
if not validate_mongo_connection(self._mongo):
|
||||
self.message.emit(
|
||||
f"!!! invalid mongo url {self._mongo}", True)
|
||||
self._set_result(-1)
|
||||
return
|
||||
bs.secure_registry.set_item("openPypeMongo", self._mongo)
|
||||
os.environ["OPENPYPE_MONGO"] = self._mongo
|
||||
|
||||
self.message.emit(f"processing {self._path}", True)
|
||||
repo_file = bs.process_entered_location(self._path)
|
||||
if detected and not OpenPypeVersion.get_installed_version().is_compatible(detected[-1]): # noqa: E501
|
||||
self.message.emit((
|
||||
f"Latest detected version {detected[-1]} "
|
||||
"is not compatible with the currently running "
|
||||
f"{local_version}"
|
||||
), True)
|
||||
self.message.emit((
|
||||
"Filtering detected versions to compatible ones..."
|
||||
), False)
|
||||
|
||||
if not repo_file:
|
||||
self.message.emit("!!! Cannot install", True)
|
||||
self._set_result(-1)
|
||||
# filter results to get only compatible versions
|
||||
detected = [
|
||||
version for version in detected
|
||||
if version.is_compatible(
|
||||
OpenPypeVersion.get_installed_version())
|
||||
]
|
||||
|
||||
if detected:
|
||||
if OpenPypeVersion(
|
||||
version=local_version, path=Path()) < detected[-1]:
|
||||
self.message.emit((
|
||||
f"Latest installed version {detected[-1]} is newer "
|
||||
f"then currently running {local_version}"
|
||||
), False)
|
||||
self.message.emit("Skipping OpenPype install ...", False)
|
||||
if detected[-1].path.suffix.lower() == ".zip":
|
||||
bs.extract_openpype(detected[-1])
|
||||
self._set_result(0)
|
||||
return
|
||||
|
||||
if OpenPypeVersion(version=local_version).get_main_version() == detected[-1].get_main_version(): # noqa: E501
|
||||
self.message.emit((
|
||||
f"Latest installed version is the same as "
|
||||
f"currently running {local_version}"
|
||||
), False)
|
||||
self.message.emit("Skipping OpenPype install ...", False)
|
||||
self._set_result(0)
|
||||
return
|
||||
|
||||
self.message.emit((
|
||||
"All installed versions are older then "
|
||||
f"currently running one {local_version}"
|
||||
), False)
|
||||
|
||||
self.message.emit("None detected.", False)
|
||||
|
||||
self.message.emit(
|
||||
f"We will use local OpenPype version {local_version}", False)
|
||||
|
||||
local_openpype = bs.create_version_from_live_code()
|
||||
if not local_openpype:
|
||||
self.message.emit(
|
||||
f"!!! Install failed - {local_openpype}", True)
|
||||
self._set_result(-1)
|
||||
return
|
||||
|
||||
try:
|
||||
bs.install_version(local_openpype)
|
||||
except (OpenPypeVersionExists,
|
||||
OpenPypeVersionInvalid,
|
||||
OpenPypeVersionIOError) as e:
|
||||
self.message.emit(f"Installed failed: ", True)
|
||||
self.message.emit(str(e), True)
|
||||
self._set_result(-1)
|
||||
return
|
||||
|
||||
self.message.emit(f"Installed as {local_openpype}", False)
|
||||
self.progress.emit(100)
|
||||
self._set_result(1)
|
||||
return
|
||||
|
||||
self.progress.emit(100)
|
||||
self._set_result(1)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from Qt import QtWidgets, QtGui
|
||||
from qtpy import QtWidgets, QtGui
|
||||
|
||||
from .tools import (
|
||||
load_stylesheet,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from Qt import QtCore, QtGui, QtWidgets # noqa
|
||||
from qtpy import QtWidgets
|
||||
|
||||
|
||||
class NiceProgressBar(QtWidgets.QProgressBar):
|
||||
|
|
|
|||
|
|
@ -153,7 +153,8 @@ def get_openpype_global_settings(url: str) -> dict:
|
|||
# Create mongo connection
|
||||
client = MongoClient(url, **kwargs)
|
||||
# Access settings collection
|
||||
col = client["openpype"]["settings"]
|
||||
openpype_db = os.environ.get("OPENPYPE_DATABASE_NAME") or "openpype"
|
||||
col = client[openpype_db]["settings"]
|
||||
# Query global settings
|
||||
global_settings = col.find_one({"type": "global_settings"}) or {}
|
||||
# Close Mongo connection
|
||||
|
|
@ -184,11 +185,7 @@ def get_openpype_path_from_settings(settings: dict) -> Union[str, None]:
|
|||
if paths and isinstance(paths, str):
|
||||
paths = [paths]
|
||||
|
||||
# Loop over paths and return only existing
|
||||
for path in paths:
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
return None
|
||||
return next((path for path in paths if os.path.exists(path)), None)
|
||||
|
||||
|
||||
def get_expected_studio_version_str(
|
||||
|
|
@ -206,10 +203,7 @@ def get_expected_studio_version_str(
|
|||
mongo_url = os.environ.get("OPENPYPE_MONGO")
|
||||
if global_settings is None:
|
||||
global_settings = get_openpype_global_settings(mongo_url)
|
||||
if staging:
|
||||
key = "staging_version"
|
||||
else:
|
||||
key = "production_version"
|
||||
key = "staging_version" if staging else "production_version"
|
||||
return global_settings.get(key) or ""
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Working thread for update."""
|
||||
from Qt.QtCore import QThread, Signal, QObject # noqa
|
||||
from qtpy import QtCore
|
||||
|
||||
from .bootstrap_repos import (
|
||||
BootstrapRepos,
|
||||
|
|
@ -8,7 +8,7 @@ from .bootstrap_repos import (
|
|||
)
|
||||
|
||||
|
||||
class UpdateThread(QThread):
|
||||
class UpdateThread(QtCore.QThread):
|
||||
"""Install Worker thread.
|
||||
|
||||
This class takes care of finding OpenPype version on user entered path
|
||||
|
|
@ -19,13 +19,13 @@ class UpdateThread(QThread):
|
|||
user data dir.
|
||||
|
||||
"""
|
||||
progress = Signal(int)
|
||||
message = Signal((str, bool))
|
||||
progress = QtCore.Signal(int)
|
||||
message = QtCore.Signal((str, bool))
|
||||
|
||||
def __init__(self, parent=None):
|
||||
self._result = None
|
||||
self._openpype_version = None
|
||||
QThread.__init__(self, parent)
|
||||
super().__init__(parent)
|
||||
|
||||
def set_version(self, openpype_version: OpenPypeVersion):
|
||||
self._openpype_version = openpype_version
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Progress window to show when OpenPype is updating/installing locally."""
|
||||
import os
|
||||
|
||||
from qtpy import QtCore, QtGui, QtWidgets
|
||||
|
||||
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
|
||||
|
|
@ -47,7 +49,6 @@ class UpdateWindow(QtWidgets.QDialog):
|
|||
|
||||
self._update_thread = None
|
||||
|
||||
self.resize(QtCore.QSize(self._width, self._height))
|
||||
self._init_ui()
|
||||
|
||||
# Set stylesheet
|
||||
|
|
@ -79,6 +80,16 @@ class UpdateWindow(QtWidgets.QDialog):
|
|||
|
||||
self._progress_bar = progress_bar
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
current_size = self.size()
|
||||
new_size = QtCore.QSize(
|
||||
max(current_size.width(), self._width),
|
||||
max(current_size.height(), self._height)
|
||||
)
|
||||
if current_size != new_size:
|
||||
self.resize(new_size)
|
||||
|
||||
def _run_update(self):
|
||||
"""Start install process.
|
||||
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ Source: "build\{#build}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdir
|
|||
; 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
|
||||
Name: "{autoprograms}\{#MyAppName} {#AppVer}"; Filename: "{app}\openpype_gui.exe"
|
||||
Name: "{autodesktop}\{#MyAppName} {#AppVer}"; Filename: "{app}\openpype_gui.exe"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\openpype_gui.exe"; Description: "{cm:LaunchProgram,OpenPype}"; Flags: nowait postinstall skipifsilent
|
||||
|
|
|
|||
112
openpype/api.py
112
openpype/api.py
|
|
@ -1,112 +0,0 @@
|
|||
from .settings import (
|
||||
get_system_settings,
|
||||
get_project_settings,
|
||||
get_current_project_settings,
|
||||
get_anatomy_settings,
|
||||
|
||||
SystemSettings,
|
||||
ProjectSettings
|
||||
)
|
||||
from .lib import (
|
||||
PypeLogger,
|
||||
Logger,
|
||||
Anatomy,
|
||||
execute,
|
||||
run_subprocess,
|
||||
version_up,
|
||||
get_asset,
|
||||
get_workdir_data,
|
||||
get_version_from_path,
|
||||
get_last_version_from_path,
|
||||
get_app_environments_for_context,
|
||||
source_hash,
|
||||
get_latest_version,
|
||||
get_local_site_id,
|
||||
change_openpype_mongo_url,
|
||||
create_project_folders,
|
||||
get_project_basic_paths
|
||||
)
|
||||
|
||||
from .lib.mongo import (
|
||||
get_default_components
|
||||
)
|
||||
|
||||
from .lib.applications import (
|
||||
ApplicationManager
|
||||
)
|
||||
|
||||
from .lib.avalon_context import (
|
||||
BuildWorkfile
|
||||
)
|
||||
|
||||
from . import resources
|
||||
|
||||
from .plugin import (
|
||||
Extractor,
|
||||
|
||||
ValidatePipelineOrder,
|
||||
ValidateContentsOrder,
|
||||
ValidateSceneOrder,
|
||||
ValidateMeshOrder,
|
||||
)
|
||||
|
||||
# temporary fix, might
|
||||
from .action import (
|
||||
get_errored_instances_from_context,
|
||||
RepairAction,
|
||||
RepairContextAction
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_system_settings",
|
||||
"get_project_settings",
|
||||
"get_current_project_settings",
|
||||
"get_anatomy_settings",
|
||||
"get_project_basic_paths",
|
||||
|
||||
"SystemSettings",
|
||||
"ProjectSettings",
|
||||
|
||||
"PypeLogger",
|
||||
"Logger",
|
||||
"Anatomy",
|
||||
"execute",
|
||||
"get_default_components",
|
||||
"ApplicationManager",
|
||||
"BuildWorkfile",
|
||||
|
||||
# Resources
|
||||
"resources",
|
||||
|
||||
# plugin classes
|
||||
"Extractor",
|
||||
# ordering
|
||||
"ValidatePipelineOrder",
|
||||
"ValidateContentsOrder",
|
||||
"ValidateSceneOrder",
|
||||
"ValidateMeshOrder",
|
||||
# action
|
||||
"get_errored_instances_from_context",
|
||||
"RepairAction",
|
||||
"RepairContextAction",
|
||||
|
||||
# get contextual data
|
||||
"version_up",
|
||||
"get_asset",
|
||||
"get_workdir_data",
|
||||
"get_version_from_path",
|
||||
"get_last_version_from_path",
|
||||
"get_app_environments_for_context",
|
||||
"source_hash",
|
||||
|
||||
"run_subprocess",
|
||||
"get_latest_version",
|
||||
|
||||
"get_local_site_id",
|
||||
"change_openpype_mongo_url",
|
||||
|
||||
"get_project_basic_paths",
|
||||
"create_project_folders"
|
||||
|
||||
]
|
||||
|
|
@ -16,14 +16,15 @@ from .pype_commands import PypeCommands
|
|||
@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."))
|
||||
help="list all detected versions.")
|
||||
@click.option("--validate-version", expose_value=False,
|
||||
help="validate given version integrity")
|
||||
@click.option("--debug", is_flag=True, expose_value=False,
|
||||
help=("Enable debug"))
|
||||
help="Enable debug")
|
||||
@click.option("--verbose", expose_value=False,
|
||||
help=("Change OpenPype log level (debug - critical or 0-50)"))
|
||||
@click.option("--automatic-tests", is_flag=True, expose_value=False,
|
||||
help=("Run in automatic tests mode"))
|
||||
def main(ctx):
|
||||
"""Pype is main command serving as entry point to pipeline system.
|
||||
|
||||
|
|
@ -366,11 +367,15 @@ def run(script):
|
|||
"--timeout",
|
||||
help="Provide specific timeout value for test case",
|
||||
default=None)
|
||||
@click.option("-so",
|
||||
"--setup_only",
|
||||
help="Only create dbs, do not run tests",
|
||||
default=None)
|
||||
def runtests(folder, mark, pyargs, test_data_folder, persist, app_variant,
|
||||
timeout):
|
||||
timeout, setup_only):
|
||||
"""Run all automatic tests after proper initialization via start.py"""
|
||||
PypeCommands().run_tests(folder, mark, pyargs, test_data_folder,
|
||||
persist, app_variant, timeout)
|
||||
persist, app_variant, timeout, setup_only)
|
||||
|
||||
|
||||
@main.command()
|
||||
|
|
@ -429,20 +434,18 @@ def unpack_project(zipfile, root):
|
|||
|
||||
@main.command()
|
||||
def interactive():
|
||||
"""Interative (Python like) console.
|
||||
"""Interactive (Python like) console.
|
||||
|
||||
Helpfull command not only for development to directly work with python
|
||||
Helpful command not only for development to directly work with python
|
||||
interpreter.
|
||||
|
||||
Warning:
|
||||
Executable 'openpype_gui' on windows won't work.
|
||||
Executable 'openpype_gui' on Windows won't work.
|
||||
"""
|
||||
|
||||
from openpype.version import __version__
|
||||
|
||||
banner = "OpenPype {}\nPython {} on {}".format(
|
||||
__version__, sys.version, sys.platform
|
||||
)
|
||||
banner = f"OpenPype {__version__}\nPython {sys.version} on {sys.platform}"
|
||||
code.interact(banner)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -164,7 +164,6 @@ def get_linked_representation_id(
|
|||
# Recursive graph lookup for inputs
|
||||
{"$graphLookup": graph_lookup}
|
||||
]
|
||||
|
||||
conn = get_project_connection(project_name)
|
||||
result = conn.aggregate(query_pipeline)
|
||||
referenced_version_ids = _process_referenced_pipeline_result(
|
||||
|
|
@ -213,7 +212,7 @@ def _process_referenced_pipeline_result(result, link_type):
|
|||
|
||||
for output in sorted(outputs_recursive, key=lambda o: o["depth"]):
|
||||
output_links = output.get("data", {}).get("inputLinks")
|
||||
if not output_links:
|
||||
if not output_links and output["type"] != "hero_version":
|
||||
continue
|
||||
|
||||
# Leaf
|
||||
|
|
@ -232,6 +231,9 @@ def _process_referenced_pipeline_result(result, link_type):
|
|||
|
||||
|
||||
def _filter_input_links(input_links, link_type, correctly_linked_ids):
|
||||
if not input_links: # to handle hero versions
|
||||
return
|
||||
|
||||
for input_link in input_links:
|
||||
if link_type and input_link["type"] != link_type:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
|
||||
from openpype.lib import PreLaunchHook
|
||||
|
||||
|
||||
|
|
@ -40,5 +41,13 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
|
|||
self.log.info("Current context does not have any workfile yet.")
|
||||
return
|
||||
|
||||
# Determine whether to open workfile post initialization.
|
||||
if self.host_name == "maya":
|
||||
key = "open_workfile_post_initialization"
|
||||
if self.data["project_settings"]["maya"][key]:
|
||||
self.log.debug("Opening workfile post initialization.")
|
||||
self.data["env"]["OPENPYPE_" + key.upper()] = "1"
|
||||
return
|
||||
|
||||
# Add path to workfile to arguments
|
||||
self.launch_context.launch_args.append(last_workfile)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class LaunchFoundryAppsWindows(PreLaunchHook):
|
|||
|
||||
# Should be as last hook because must change launch arguments to string
|
||||
order = 1000
|
||||
app_groups = ["nuke", "nukex", "hiero", "nukestudio"]
|
||||
app_groups = ["nuke", "nukeassist", "nukex", "hiero", "nukestudio"]
|
||||
platforms = ["windows"]
|
||||
|
||||
def execute(self):
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ exists is used.
|
|||
|
||||
import os
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import platform
|
||||
|
||||
import six
|
||||
|
||||
|
|
@ -38,7 +39,6 @@ class HostDirmap(object):
|
|||
self._project_settings = project_settings
|
||||
self._sync_module = sync_module # to limit reinit of Modules
|
||||
self._log = None
|
||||
self._mapping = None # cache mapping
|
||||
|
||||
@property
|
||||
def sync_module(self):
|
||||
|
|
@ -69,29 +69,28 @@ class HostDirmap(object):
|
|||
"""Run host dependent remapping from source_path to destination_path"""
|
||||
pass
|
||||
|
||||
def process_dirmap(self):
|
||||
def process_dirmap(self, mapping=None):
|
||||
# type: (dict) -> None
|
||||
"""Go through all paths in Settings and set them using `dirmap`.
|
||||
|
||||
If artists has Site Sync enabled, take dirmap mapping directly from
|
||||
Local Settings when artist is syncing workfile locally.
|
||||
|
||||
Args:
|
||||
project_settings (dict): Settings for current project.
|
||||
"""
|
||||
|
||||
if not self._mapping:
|
||||
self._mapping = self.get_mappings(self.project_settings)
|
||||
if not self._mapping:
|
||||
if not mapping:
|
||||
mapping = self.get_mappings()
|
||||
if not mapping:
|
||||
return
|
||||
|
||||
self.log.info("Processing directory mapping ...")
|
||||
self.on_enable_dirmap()
|
||||
self.log.info("mapping:: {}".format(self._mapping))
|
||||
|
||||
for k, sp in enumerate(self._mapping["source-path"]):
|
||||
dst = self._mapping["destination-path"][k]
|
||||
for k, sp in enumerate(mapping["source-path"]):
|
||||
dst = mapping["destination-path"][k]
|
||||
try:
|
||||
# add trailing slash if missing
|
||||
sp = os.path.join(sp, '')
|
||||
dst = os.path.join(dst, '')
|
||||
print("{} -> {}".format(sp, dst))
|
||||
self.dirmap_routine(sp, dst)
|
||||
except IndexError:
|
||||
|
|
@ -109,28 +108,24 @@ class HostDirmap(object):
|
|||
)
|
||||
continue
|
||||
|
||||
def get_mappings(self, project_settings):
|
||||
def get_mappings(self):
|
||||
"""Get translation from source-path to destination-path.
|
||||
|
||||
It checks if Site Sync is enabled and user chose to use local
|
||||
site, in that case configuration in Local Settings takes precedence
|
||||
"""
|
||||
|
||||
local_mapping = self._get_local_sync_dirmap(project_settings)
|
||||
dirmap_label = "{}-dirmap".format(self.host_name)
|
||||
if (
|
||||
not self.project_settings[self.host_name].get(dirmap_label)
|
||||
and not local_mapping
|
||||
):
|
||||
return {}
|
||||
mapping_settings = self.project_settings[self.host_name][dirmap_label]
|
||||
mapping_enabled = mapping_settings["enabled"] or bool(local_mapping)
|
||||
mapping_sett = self.project_settings[self.host_name].get(dirmap_label,
|
||||
{})
|
||||
local_mapping = self._get_local_sync_dirmap()
|
||||
mapping_enabled = mapping_sett.get("enabled") or bool(local_mapping)
|
||||
if not mapping_enabled:
|
||||
return {}
|
||||
|
||||
mapping = (
|
||||
local_mapping
|
||||
or mapping_settings["paths"]
|
||||
or mapping_sett["paths"]
|
||||
or {}
|
||||
)
|
||||
|
||||
|
|
@ -140,28 +135,27 @@ class HostDirmap(object):
|
|||
or not mapping.get("source-path")
|
||||
):
|
||||
return {}
|
||||
self.log.info("Processing directory mapping ...")
|
||||
self.log.info("mapping:: {}".format(mapping))
|
||||
return mapping
|
||||
|
||||
def _get_local_sync_dirmap(self, project_settings):
|
||||
def _get_local_sync_dirmap(self):
|
||||
"""
|
||||
Returns dirmap if synch to local project is enabled.
|
||||
|
||||
Only valid mapping is from roots of remote site to local site set
|
||||
in Local Settings.
|
||||
|
||||
Args:
|
||||
project_settings (dict)
|
||||
Returns:
|
||||
dict : { "source-path": [XXX], "destination-path": [YYYY]}
|
||||
"""
|
||||
project_name = os.getenv("AVALON_PROJECT")
|
||||
|
||||
mapping = {}
|
||||
|
||||
if not project_settings["global"]["sync_server"]["enabled"]:
|
||||
if (not self.sync_module.enabled or
|
||||
project_name not in self.sync_module.get_enabled_projects()):
|
||||
return mapping
|
||||
|
||||
project_name = os.getenv("AVALON_PROJECT")
|
||||
|
||||
active_site = self.sync_module.get_local_normalized_site(
|
||||
self.sync_module.get_active_site(project_name))
|
||||
remote_site = self.sync_module.get_local_normalized_site(
|
||||
|
|
@ -170,11 +164,7 @@ class HostDirmap(object):
|
|||
"active {} - remote {}".format(active_site, remote_site)
|
||||
)
|
||||
|
||||
if (
|
||||
active_site == "local"
|
||||
and project_name in self.sync_module.get_enabled_projects()
|
||||
and active_site != remote_site
|
||||
):
|
||||
if active_site == "local" and active_site != remote_site:
|
||||
sync_settings = self.sync_module.get_sync_project_setting(
|
||||
project_name,
|
||||
exclude_locals=False,
|
||||
|
|
@ -187,11 +177,27 @@ class HostDirmap(object):
|
|||
|
||||
self.log.debug("local overrides {}".format(active_overrides))
|
||||
self.log.debug("remote overrides {}".format(remote_overrides))
|
||||
|
||||
current_platform = platform.system().lower()
|
||||
remote_provider = self.sync_module.get_provider_for_site(
|
||||
project_name, remote_site
|
||||
)
|
||||
# dirmap has sense only with regular disk provider, in the workfile
|
||||
# wont be root on cloud or sftp provider
|
||||
if remote_provider != "local_drive":
|
||||
remote_site = "studio"
|
||||
for root_name, active_site_dir in active_overrides.items():
|
||||
remote_site_dir = (
|
||||
remote_overrides.get(root_name)
|
||||
or sync_settings["sites"][remote_site]["root"][root_name]
|
||||
)
|
||||
|
||||
if isinstance(remote_site_dir, dict):
|
||||
remote_site_dir = remote_site_dir.get(current_platform)
|
||||
|
||||
if not remote_site_dir:
|
||||
continue
|
||||
|
||||
if os.path.isdir(active_site_dir):
|
||||
if "destination-path" not in mapping:
|
||||
mapping["destination-path"] = []
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import os
|
||||
import logging
|
||||
import contextlib
|
||||
from abc import ABCMeta, abstractproperty
|
||||
|
|
@ -76,6 +77,18 @@ class HostBase(object):
|
|||
|
||||
pass
|
||||
|
||||
def install(self):
|
||||
"""Install host specific functionality.
|
||||
|
||||
This is where should be added menu with tools, registered callbacks
|
||||
and other host integration initialization.
|
||||
|
||||
It is called automatically when 'openpype.pipeline.install_host' is
|
||||
triggered.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
if self._log is None:
|
||||
|
|
@ -88,6 +101,30 @@ class HostBase(object):
|
|||
|
||||
pass
|
||||
|
||||
def get_current_project_name(self):
|
||||
"""
|
||||
Returns:
|
||||
Union[str, None]: Current project name.
|
||||
"""
|
||||
|
||||
return os.environ.get("AVALON_PROJECT")
|
||||
|
||||
def get_current_asset_name(self):
|
||||
"""
|
||||
Returns:
|
||||
Union[str, None]: Current asset name.
|
||||
"""
|
||||
|
||||
return os.environ.get("AVALON_ASSET")
|
||||
|
||||
def get_current_task_name(self):
|
||||
"""
|
||||
Returns:
|
||||
Union[str, None]: Current task name.
|
||||
"""
|
||||
|
||||
return os.environ.get("AVALON_TASK")
|
||||
|
||||
def get_current_context(self):
|
||||
"""Get current context information.
|
||||
|
||||
|
|
@ -99,19 +136,14 @@ class HostBase(object):
|
|||
Default implementation returns values from 'legacy_io.Session'.
|
||||
|
||||
Returns:
|
||||
dict: Context with 3 keys 'project_name', 'asset_name' and
|
||||
'task_name'. All of them can be 'None'.
|
||||
Dict[str, Union[str, None]]: Context with 3 keys 'project_name',
|
||||
'asset_name' and 'task_name'. All of them can be 'None'.
|
||||
"""
|
||||
|
||||
from openpype.pipeline import legacy_io
|
||||
|
||||
if legacy_io.is_installed():
|
||||
legacy_io.install()
|
||||
|
||||
return {
|
||||
"project_name": legacy_io.Session["AVALON_PROJECT"],
|
||||
"asset_name": legacy_io.Session["AVALON_ASSET"],
|
||||
"task_name": legacy_io.Session["AVALON_TASK"]
|
||||
"project_name": self.get_current_project_name(),
|
||||
"asset_name": self.get_current_asset_name(),
|
||||
"task_name": self.get_current_task_name()
|
||||
}
|
||||
|
||||
def get_context_title(self):
|
||||
|
|
|
|||
|
|
@ -10,30 +10,15 @@ from .launch_logic import (
|
|||
)
|
||||
|
||||
from .pipeline import (
|
||||
AfterEffectsHost,
|
||||
ls,
|
||||
get_asset_settings,
|
||||
install,
|
||||
uninstall,
|
||||
list_instances,
|
||||
remove_instance,
|
||||
containerise,
|
||||
get_context_data,
|
||||
update_context_data,
|
||||
get_context_title
|
||||
)
|
||||
|
||||
from .workio import (
|
||||
file_extensions,
|
||||
has_unsaved_changes,
|
||||
save_file,
|
||||
open_file,
|
||||
current_file,
|
||||
work_root,
|
||||
containerise
|
||||
)
|
||||
|
||||
from .lib import (
|
||||
maintained_selection,
|
||||
get_extension_manifest_path
|
||||
get_extension_manifest_path,
|
||||
get_asset_settings
|
||||
)
|
||||
|
||||
from .plugin import (
|
||||
|
|
@ -48,26 +33,12 @@ __all__ = [
|
|||
|
||||
# pipeline
|
||||
"ls",
|
||||
"get_asset_settings",
|
||||
"install",
|
||||
"uninstall",
|
||||
"list_instances",
|
||||
"remove_instance",
|
||||
"containerise",
|
||||
"get_context_data",
|
||||
"update_context_data",
|
||||
"get_context_title",
|
||||
|
||||
"file_extensions",
|
||||
"has_unsaved_changes",
|
||||
"save_file",
|
||||
"open_file",
|
||||
"current_file",
|
||||
"work_root",
|
||||
|
||||
# lib
|
||||
"maintained_selection",
|
||||
"get_extension_manifest_path",
|
||||
"get_asset_settings",
|
||||
|
||||
# plugin
|
||||
"AfterEffectsLoader"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ExtensionManifest Version="8.0" ExtensionBundleId="com.openpype.AE.panel" ExtensionBundleVersion="1.0.23"
|
||||
<ExtensionManifest Version="8.0" ExtensionBundleId="com.openpype.AE.panel" ExtensionBundleVersion="1.0.24"
|
||||
ExtensionBundleName="openpype" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<ExtensionList>
|
||||
<Extension Id="com.openpype.AE.panel" Version="1.0" />
|
||||
|
|
|
|||
|
|
@ -38,17 +38,6 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#creator-button").bind("click", function() {
|
||||
RPC.call('AfterEffects.creator_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#loader-button").bind("click", function() {
|
||||
|
|
@ -82,17 +71,6 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#subsetmanager-button").bind("click", function() {
|
||||
RPC.call('AfterEffects.subsetmanager_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#experimental-button").bind("click", function() {
|
||||
|
|
@ -113,11 +91,9 @@
|
|||
|
||||
<div>
|
||||
<div></div><a href=# id=workfiles-button><button class="hostFontSize">Workfiles...</button></a></div>
|
||||
<div> <a href=# id=creator-button><button class="hostFontSize">Create...</button></a></div>
|
||||
<div><a href=# id=loader-button><button class="hostFontSize">Load...</button></a></div>
|
||||
<div><a href=# id=publish-button><button class="hostFontSize">Publish...</button></a></div>
|
||||
<div><a href=# id=sceneinventory-button><button class="hostFontSize">Manage...</button></a></div>
|
||||
<div><a href=# id=subsetmanager-button><button class="hostFontSize">Subset Manager...</button></a></div>
|
||||
<div><a href=# id=experimental-button><button class="hostFontSize">Experimental Tools...</button></a></div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ function main(websocket_url){
|
|||
|
||||
RPC.addRoute('AfterEffects.get_render_info', function (data) {
|
||||
log.warn('Server called client route "get_render_info":', data);
|
||||
return runEvalScript("getRenderInfo()")
|
||||
return runEvalScript("getRenderInfo(" + data.comp_id +")")
|
||||
.then(function(result){
|
||||
log.warn("get_render_info: " + result);
|
||||
return result;
|
||||
|
|
@ -289,7 +289,7 @@ function main(websocket_url){
|
|||
RPC.addRoute('AfterEffects.render', function (data) {
|
||||
log.warn('Server called client route "render":', data);
|
||||
var escapedPath = EscapeStringForJSX(data.folder_url);
|
||||
return runEvalScript("render('" + escapedPath +"')")
|
||||
return runEvalScript("render('" + escapedPath +"', " + data.comp_id + ")")
|
||||
.then(function(result){
|
||||
log.warn("render: " + result);
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -395,41 +395,84 @@ function saveAs(path){
|
|||
app.project.save(fp = new File(path));
|
||||
}
|
||||
|
||||
function getRenderInfo(){
|
||||
function getRenderInfo(comp_id){
|
||||
/***
|
||||
Get info from render queue.
|
||||
Currently pulls only file name to parse extension and
|
||||
Currently pulls only file name to parse extension and
|
||||
if it is sequence in Python
|
||||
Args:
|
||||
comp_id (int): id of composition
|
||||
Return:
|
||||
(list) [{file_name:"xx.png", width:00, height:00}]
|
||||
**/
|
||||
var item = app.project.itemByID(comp_id);
|
||||
if (!item){
|
||||
return _prepareError("Composition with '" + comp_id + "' wasn't found! Recreate publishable instance(s)")
|
||||
}
|
||||
|
||||
var comp_name = item.name;
|
||||
var output_metadata = []
|
||||
try{
|
||||
var render_item = app.project.renderQueue.item(1);
|
||||
if (render_item.status == RQItemStatus.DONE){
|
||||
render_item.duplicate(); // create new, cannot change status if DONE
|
||||
render_item.remove(); // remove existing to limit duplications
|
||||
render_item = app.project.renderQueue.item(1);
|
||||
// render_item.duplicate() should create new item on renderQueue
|
||||
// BUT it works only sometimes, there are some weird synchronization issue
|
||||
// this method will be called always before render, so prepare items here
|
||||
// for render to spare the hassle
|
||||
for (i = 1; i <= app.project.renderQueue.numItems; ++i){
|
||||
var render_item = app.project.renderQueue.item(i);
|
||||
if (render_item.comp.id != comp_id){
|
||||
continue;
|
||||
}
|
||||
|
||||
if (render_item.status == RQItemStatus.DONE){
|
||||
render_item.duplicate(); // create new, cannot change status if DONE
|
||||
render_item.remove(); // remove existing to limit duplications
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
render_item.render = true; // always set render queue to render
|
||||
var item = render_item.outputModule(1);
|
||||
// properly validate as `numItems` won't change magically
|
||||
var comp_id_count = 0;
|
||||
for (i = 1; i <= app.project.renderQueue.numItems; ++i){
|
||||
var render_item = app.project.renderQueue.item(i);
|
||||
if (render_item.comp.id != comp_id){
|
||||
continue;
|
||||
}
|
||||
comp_id_count += 1;
|
||||
var item = render_item.outputModule(1);
|
||||
|
||||
for (j = 1; j<= render_item.numOutputModules; ++j){
|
||||
var file_url = item.file.toString();
|
||||
output_metadata.push(
|
||||
JSON.stringify({
|
||||
"file_name": file_url,
|
||||
"width": render_item.comp.width,
|
||||
"height": render_item.comp.height
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return _prepareError("There is no render queue, create one");
|
||||
}
|
||||
var file_url = item.file.toString();
|
||||
|
||||
return JSON.stringify({
|
||||
"file_name": file_url,
|
||||
"width": render_item.comp.width,
|
||||
"height": render_item.comp.height
|
||||
})
|
||||
if (comp_id_count > 1){
|
||||
return _prepareError("There cannot be more items in Render Queue for '" + comp_name + "'!")
|
||||
}
|
||||
|
||||
if (comp_id_count == 0){
|
||||
return _prepareError("There is no item in Render Queue for '" + comp_name + "'! Add composition to Render Queue.")
|
||||
}
|
||||
|
||||
return '[' + output_metadata.join() + ']';
|
||||
}
|
||||
|
||||
function getAudioUrlForComp(comp_id){
|
||||
/**
|
||||
* Searches composition for audio layer
|
||||
*
|
||||
*
|
||||
* Only single AVLayer is expected!
|
||||
* Used for collecting Audio
|
||||
*
|
||||
*
|
||||
* Args:
|
||||
* comp_id (int): id of composition
|
||||
* Return:
|
||||
|
|
@ -457,7 +500,7 @@ function addItemAsLayerToComp(comp_id, item_id, found_comp){
|
|||
/**
|
||||
* Adds already imported FootageItem ('item_id') as a new
|
||||
* layer to composition ('comp_id').
|
||||
*
|
||||
*
|
||||
* Args:
|
||||
* comp_id (int): id of target composition
|
||||
* item_id (int): FootageItem.id
|
||||
|
|
@ -480,17 +523,17 @@ function addItemAsLayerToComp(comp_id, item_id, found_comp){
|
|||
function importBackground(comp_id, composition_name, files_to_import){
|
||||
/**
|
||||
* Imports backgrounds images to existing or new composition.
|
||||
*
|
||||
*
|
||||
* If comp_id is not provided, new composition is created, basic
|
||||
* values (width, heights, frameRatio) takes from first imported
|
||||
* image.
|
||||
*
|
||||
*
|
||||
* Args:
|
||||
* comp_id (int): id of existing composition (null if new)
|
||||
* composition_name (str): used when new composition
|
||||
* composition_name (str): used when new composition
|
||||
* files_to_import (list): list of absolute paths to import and
|
||||
* add as layers
|
||||
*
|
||||
*
|
||||
* Returns:
|
||||
* (str): json representation (id, name, members)
|
||||
*/
|
||||
|
|
@ -512,7 +555,7 @@ function importBackground(comp_id, composition_name, files_to_import){
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (files_to_import){
|
||||
for (i = 0; i < files_to_import.length; ++i){
|
||||
item = _importItem(files_to_import[i]);
|
||||
|
|
@ -524,8 +567,8 @@ function importBackground(comp_id, composition_name, files_to_import){
|
|||
if (!comp){
|
||||
folder = app.project.items.addFolder(composition_name);
|
||||
imported_ids.push(folder.id);
|
||||
comp = app.project.items.addComp(composition_name, item.width,
|
||||
item.height, item.pixelAspect,
|
||||
comp = app.project.items.addComp(composition_name, item.width,
|
||||
item.height, item.pixelAspect,
|
||||
1, 26.7); // hardcode defaults
|
||||
imported_ids.push(comp.id);
|
||||
comp.parentFolder = folder;
|
||||
|
|
@ -534,7 +577,7 @@ function importBackground(comp_id, composition_name, files_to_import){
|
|||
item.parentFolder = folder;
|
||||
|
||||
addItemAsLayerToComp(comp.id, item.id, comp);
|
||||
}
|
||||
}
|
||||
}
|
||||
var item = {"name": comp.name,
|
||||
"id": folder.id,
|
||||
|
|
@ -545,19 +588,19 @@ function importBackground(comp_id, composition_name, files_to_import){
|
|||
function reloadBackground(comp_id, composition_name, files_to_import){
|
||||
/**
|
||||
* Reloads existing composition.
|
||||
*
|
||||
*
|
||||
* It deletes complete composition with encompassing folder, recreates
|
||||
* from scratch via 'importBackground' functionality.
|
||||
*
|
||||
*
|
||||
* Args:
|
||||
* comp_id (int): id of existing composition (null if new)
|
||||
* composition_name (str): used when new composition
|
||||
* composition_name (str): used when new composition
|
||||
* files_to_import (list): list of absolute paths to import and
|
||||
* add as layers
|
||||
*
|
||||
*
|
||||
* Returns:
|
||||
* (str): json representation (id, name, members)
|
||||
*
|
||||
*
|
||||
*/
|
||||
var imported_ids = []; // keep track of members of composition
|
||||
comp = app.project.itemByID(comp_id);
|
||||
|
|
@ -620,7 +663,7 @@ function reloadBackground(comp_id, composition_name, files_to_import){
|
|||
function _get_file_name(file_url){
|
||||
/**
|
||||
* Returns file name without extension from 'file_url'
|
||||
*
|
||||
*
|
||||
* Args:
|
||||
* file_url (str): full absolute url
|
||||
* Returns:
|
||||
|
|
@ -635,7 +678,7 @@ function _delete_obsolete_items(folder, new_filenames){
|
|||
/***
|
||||
* Goes through 'folder' and removes layers not in new
|
||||
* background
|
||||
*
|
||||
*
|
||||
* Args:
|
||||
* folder (FolderItem)
|
||||
* new_filenames (array): list of layer names in new bg
|
||||
|
|
@ -660,14 +703,14 @@ function _delete_obsolete_items(folder, new_filenames){
|
|||
function _importItem(file_url){
|
||||
/**
|
||||
* Imports 'file_url' as new FootageItem
|
||||
*
|
||||
*
|
||||
* Args:
|
||||
* file_url (str): file url with content
|
||||
* Returns:
|
||||
* (FootageItem)
|
||||
*/
|
||||
file_name = _get_file_name(file_url);
|
||||
|
||||
|
||||
//importFile prepared previously to return json
|
||||
item_json = importFile(file_url, file_name, JSON.stringify({"ImportAsType":"FOOTAGE"}));
|
||||
item_json = JSON.parse(item_json);
|
||||
|
|
@ -689,30 +732,42 @@ function isFileSequence (item){
|
|||
return false;
|
||||
}
|
||||
|
||||
function render(target_folder){
|
||||
function render(target_folder, comp_id){
|
||||
var out_dir = new Folder(target_folder);
|
||||
var out_dir = out_dir.fsName;
|
||||
for (i = 1; i <= app.project.renderQueue.numItems; ++i){
|
||||
var render_item = app.project.renderQueue.item(i);
|
||||
var om1 = app.project.renderQueue.item(i).outputModule(1);
|
||||
var file_name = File.decode( om1.file.name ).replace('℗', ''); // Name contains special character, space?
|
||||
var composition = render_item.comp;
|
||||
if (composition.id == comp_id){
|
||||
if (render_item.status == RQItemStatus.DONE){
|
||||
var new_item = render_item.duplicate();
|
||||
render_item.remove();
|
||||
render_item = new_item;
|
||||
}
|
||||
|
||||
render_item.render = true;
|
||||
|
||||
var om1 = app.project.renderQueue.item(i).outputModule(1);
|
||||
var file_name = File.decode( om1.file.name ).replace('℗', ''); // Name contains special character, space?
|
||||
|
||||
var omItem1_settable_str = app.project.renderQueue.item(i).outputModule(1).getSettings( GetSettingsFormat.STRING_SETTABLE );
|
||||
|
||||
var targetFolder = new Folder(target_folder);
|
||||
if (!targetFolder.exists) {
|
||||
targetFolder.create();
|
||||
}
|
||||
|
||||
om1.file = new File(targetFolder.fsName + '/' + file_name);
|
||||
}else{
|
||||
if (render_item.status != RQItemStatus.DONE){
|
||||
render_item.render = false;
|
||||
}
|
||||
}
|
||||
|
||||
var omItem1_settable_str = app.project.renderQueue.item(i).outputModule(1).getSettings( GetSettingsFormat.STRING_SETTABLE );
|
||||
|
||||
if (render_item.status == RQItemStatus.DONE){
|
||||
render_item.duplicate();
|
||||
render_item.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
var targetFolder = new Folder(target_folder);
|
||||
if (!targetFolder.exists) {
|
||||
targetFolder.create();
|
||||
}
|
||||
|
||||
om1.file = new File(targetFolder.fsName + '/' + file_name);
|
||||
}
|
||||
app.beginSuppressDialogs();
|
||||
app.project.renderQueue.render();
|
||||
app.endSuppressDialogs(false);
|
||||
}
|
||||
|
||||
function close(){
|
||||
|
|
|
|||
|
|
@ -284,9 +284,6 @@ class AfterEffectsRoute(WebSocketRoute):
|
|||
return await self.socket.call('aftereffects.read')
|
||||
|
||||
# panel routes for tools
|
||||
async def creator_route(self):
|
||||
self._tool_route("creator")
|
||||
|
||||
async def workfiles_route(self):
|
||||
self._tool_route("workfiles")
|
||||
|
||||
|
|
@ -294,14 +291,11 @@ class AfterEffectsRoute(WebSocketRoute):
|
|||
self._tool_route("loader")
|
||||
|
||||
async def publish_route(self):
|
||||
self._tool_route("publish")
|
||||
self._tool_route("publisher")
|
||||
|
||||
async def sceneinventory_route(self):
|
||||
self._tool_route("sceneinventory")
|
||||
|
||||
async def subsetmanager_route(self):
|
||||
self._tool_route("subsetmanager")
|
||||
|
||||
async def experimental_tools_route(self):
|
||||
self._tool_route("experimental_tools")
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from openpype.pipeline import install_host
|
|||
from openpype.modules import ModulesManager
|
||||
|
||||
from openpype.tools.utils import host_tools
|
||||
from openpype.tests.lib import is_in_tests
|
||||
from .launch_logic import ProcessLauncher, get_stub
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -26,9 +27,10 @@ def safe_excepthook(*args):
|
|||
def main(*subprocess_args):
|
||||
sys.excepthook = safe_excepthook
|
||||
|
||||
from openpype.hosts.aftereffects import api
|
||||
from openpype.hosts.aftereffects.api import AfterEffectsHost
|
||||
|
||||
install_host(api)
|
||||
host = AfterEffectsHost()
|
||||
install_host(host)
|
||||
|
||||
os.environ["OPENPYPE_LOG_NO_COLORS"] = "False"
|
||||
app = QtWidgets.QApplication([])
|
||||
|
|
@ -46,7 +48,7 @@ def main(*subprocess_args):
|
|||
webpublisher_addon.headless_publish,
|
||||
log,
|
||||
"CloseAE",
|
||||
os.environ.get("IS_TEST")
|
||||
is_in_tests()
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -133,3 +135,32 @@ def get_background_layers(file_url):
|
|||
layer.get("filename")).
|
||||
replace("\\", "/"))
|
||||
return layers
|
||||
|
||||
|
||||
def get_asset_settings(asset_doc):
|
||||
"""Get settings on current asset from database.
|
||||
|
||||
Returns:
|
||||
dict: Scene data.
|
||||
|
||||
"""
|
||||
asset_data = asset_doc["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
|
||||
|
||||
return {
|
||||
"fps": fps,
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"resolutionWidth": resolution_width,
|
||||
"resolutionHeight": resolution_height,
|
||||
"duration": duration
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@ from openpype.pipeline import (
|
|||
from openpype.pipeline.load import any_outdated_containers
|
||||
import openpype.hosts.aftereffects
|
||||
|
||||
from openpype.host import (
|
||||
HostBase,
|
||||
IWorkfileHost,
|
||||
ILoadHost,
|
||||
IPublishHost
|
||||
)
|
||||
|
||||
from .launch_logic import get_stub, ConnectionNotEstablishedYet
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
|
@ -30,27 +37,142 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
|
|||
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
|
||||
|
||||
|
||||
def install():
|
||||
print("Installing Pype config...")
|
||||
class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
||||
name = "aftereffects"
|
||||
|
||||
pyblish.api.register_host("aftereffects")
|
||||
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||
def __init__(self):
|
||||
self._stub = None
|
||||
super(AfterEffectsHost, self).__init__()
|
||||
|
||||
register_loader_plugin_path(LOAD_PATH)
|
||||
register_creator_plugin_path(CREATE_PATH)
|
||||
log.info(PUBLISH_PATH)
|
||||
@property
|
||||
def stub(self):
|
||||
"""
|
||||
Handle pulling stub from PS to run operations on host
|
||||
Returns:
|
||||
(AEServerStub) or None
|
||||
"""
|
||||
if self._stub:
|
||||
return self._stub
|
||||
|
||||
pyblish.api.register_callback(
|
||||
"instanceToggled", on_pyblish_instance_toggled
|
||||
)
|
||||
try:
|
||||
stub = get_stub() # only after Photoshop is up
|
||||
except ConnectionNotEstablishedYet:
|
||||
print("Not connected yet, ignoring")
|
||||
return
|
||||
|
||||
register_event_callback("application.launched", application_launch)
|
||||
if not stub.get_active_document_name():
|
||||
return
|
||||
|
||||
self._stub = stub
|
||||
return self._stub
|
||||
|
||||
def uninstall():
|
||||
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
|
||||
deregister_loader_plugin_path(LOAD_PATH)
|
||||
deregister_creator_plugin_path(CREATE_PATH)
|
||||
def install(self):
|
||||
print("Installing Pype config...")
|
||||
|
||||
pyblish.api.register_host("aftereffects")
|
||||
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||
|
||||
register_loader_plugin_path(LOAD_PATH)
|
||||
register_creator_plugin_path(CREATE_PATH)
|
||||
log.info(PUBLISH_PATH)
|
||||
|
||||
pyblish.api.register_callback(
|
||||
"instanceToggled", on_pyblish_instance_toggled
|
||||
)
|
||||
|
||||
register_event_callback("application.launched", application_launch)
|
||||
|
||||
def get_workfile_extensions(self):
|
||||
return [".aep"]
|
||||
|
||||
def save_workfile(self, dst_path=None):
|
||||
self.stub.saveAs(dst_path, True)
|
||||
|
||||
def open_workfile(self, filepath):
|
||||
self.stub.open(filepath)
|
||||
|
||||
return True
|
||||
|
||||
def get_current_workfile(self):
|
||||
try:
|
||||
full_name = get_stub().get_active_document_full_name()
|
||||
if full_name and full_name != "null":
|
||||
return os.path.normpath(full_name).replace("\\", "/")
|
||||
except ValueError:
|
||||
print("Nothing opened")
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def get_containers(self):
|
||||
return ls()
|
||||
|
||||
def get_context_data(self):
|
||||
meta = self.stub.get_metadata()
|
||||
for item in meta:
|
||||
if item.get("id") == "publish_context":
|
||||
item.pop("id")
|
||||
return item
|
||||
|
||||
return {}
|
||||
|
||||
def update_context_data(self, data, changes):
|
||||
item = data
|
||||
item["id"] = "publish_context"
|
||||
self.stub.imprint(item["id"], item)
|
||||
|
||||
# created instances section
|
||||
def list_instances(self):
|
||||
"""List all created instances from current workfile which
|
||||
will be published.
|
||||
|
||||
Pulls from File > File Info
|
||||
|
||||
For SubsetManager
|
||||
|
||||
Returns:
|
||||
(list) of dictionaries matching instances format
|
||||
"""
|
||||
stub = self.stub
|
||||
if not stub:
|
||||
return []
|
||||
|
||||
instances = []
|
||||
layers_meta = stub.get_metadata()
|
||||
|
||||
for instance in layers_meta:
|
||||
if instance.get("id") == "pyblish.avalon.instance":
|
||||
instances.append(instance)
|
||||
return instances
|
||||
|
||||
def remove_instance(self, instance):
|
||||
"""Remove instance from current workfile metadata.
|
||||
|
||||
Updates metadata of current file in File > File Info and removes
|
||||
icon highlight on group layer.
|
||||
|
||||
For SubsetManager
|
||||
|
||||
Args:
|
||||
instance (dict): instance representation from subsetmanager model
|
||||
"""
|
||||
stub = self.stub
|
||||
|
||||
if not stub:
|
||||
return
|
||||
|
||||
inst_id = instance.get("instance_id") or instance.get("uuid") # legacy
|
||||
if not inst_id:
|
||||
log.warning("No instance identifier for {}".format(instance))
|
||||
return
|
||||
|
||||
stub.remove_instance(inst_id)
|
||||
|
||||
if instance.get("members"):
|
||||
item = stub.get_item(instance["members"][0])
|
||||
if item:
|
||||
stub.rename_item(item.id,
|
||||
item.name.replace(stub.PUBLISH_ICON, ''))
|
||||
|
||||
|
||||
def application_launch():
|
||||
|
|
@ -63,35 +185,6 @@ def on_pyblish_instance_toggled(instance, old_value, new_value):
|
|||
instance[0].Visible = new_value
|
||||
|
||||
|
||||
def get_asset_settings(asset_doc):
|
||||
"""Get settings on current asset from database.
|
||||
|
||||
Returns:
|
||||
dict: Scene data.
|
||||
|
||||
"""
|
||||
asset_data = asset_doc["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
|
||||
|
||||
return {
|
||||
"fps": fps,
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"resolutionWidth": resolution_width,
|
||||
"resolutionHeight": resolution_height,
|
||||
"duration": duration
|
||||
}
|
||||
|
||||
|
||||
def ls():
|
||||
"""Yields containers from active AfterEffects document.
|
||||
|
||||
|
|
@ -191,102 +284,17 @@ def containerise(name,
|
|||
return comp
|
||||
|
||||
|
||||
# created instances section
|
||||
def list_instances():
|
||||
"""
|
||||
List all created instances from current workfile which
|
||||
will be published.
|
||||
def cache_and_get_instances(creator):
|
||||
"""Cache instances in shared data.
|
||||
|
||||
Pulls from File > File Info
|
||||
|
||||
For SubsetManager
|
||||
|
||||
Returns:
|
||||
(list) of dictionaries matching instances format
|
||||
"""
|
||||
stub = _get_stub()
|
||||
if not stub:
|
||||
return []
|
||||
|
||||
instances = []
|
||||
layers_meta = stub.get_metadata()
|
||||
|
||||
for instance in layers_meta:
|
||||
if instance.get("id") == "pyblish.avalon.instance":
|
||||
instances.append(instance)
|
||||
return instances
|
||||
|
||||
|
||||
def remove_instance(instance):
|
||||
"""
|
||||
Remove instance from current workfile metadata.
|
||||
|
||||
Updates metadata of current file in File > File Info and removes
|
||||
icon highlight on group layer.
|
||||
|
||||
For SubsetManager
|
||||
|
||||
Args:
|
||||
instance (dict): instance representation from subsetmanager model
|
||||
"""
|
||||
stub = _get_stub()
|
||||
|
||||
if not stub:
|
||||
return
|
||||
|
||||
inst_id = instance.get("instance_id") or instance.get("uuid") # legacy
|
||||
if not inst_id:
|
||||
log.warning("No instance identifier for {}".format(instance))
|
||||
return
|
||||
|
||||
stub.remove_instance(inst_id)
|
||||
|
||||
if instance.get("members"):
|
||||
item = stub.get_item(instance["members"][0])
|
||||
if item:
|
||||
stub.rename_item(item.id,
|
||||
item.name.replace(stub.PUBLISH_ICON, ''))
|
||||
|
||||
|
||||
# new publisher section
|
||||
def get_context_data():
|
||||
meta = _get_stub().get_metadata()
|
||||
for item in meta:
|
||||
if item.get("id") == "publish_context":
|
||||
item.pop("id")
|
||||
return item
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def update_context_data(data, changes):
|
||||
item = data
|
||||
item["id"] = "publish_context"
|
||||
_get_stub().imprint(item["id"], item)
|
||||
|
||||
|
||||
def get_context_title():
|
||||
"""Returns title for Creator window"""
|
||||
|
||||
project_name = legacy_io.Session["AVALON_PROJECT"]
|
||||
asset_name = legacy_io.Session["AVALON_ASSET"]
|
||||
task_name = legacy_io.Session["AVALON_TASK"]
|
||||
return "{}/{}/{}".format(project_name, asset_name, task_name)
|
||||
|
||||
|
||||
def _get_stub():
|
||||
"""
|
||||
Handle pulling stub from PS to run operations on host
|
||||
Storing all instances as a list as legacy instances might be still present.
|
||||
Args:
|
||||
creator (Creator): Plugin which would like to get instances from host.
|
||||
Returns:
|
||||
(AEServerStub) or None
|
||||
List[]: list of all instances stored in metadata
|
||||
"""
|
||||
try:
|
||||
stub = get_stub() # only after Photoshop is up
|
||||
except ConnectionNotEstablishedYet:
|
||||
print("Not connected yet, ignoring")
|
||||
return
|
||||
|
||||
if not stub.get_active_document_name():
|
||||
return
|
||||
|
||||
return stub
|
||||
shared_key = "openpype.photoshop.instances"
|
||||
if shared_key not in creator.collection_shared_data:
|
||||
creator.collection_shared_data[shared_key] = \
|
||||
creator.host.list_instances()
|
||||
return creator.collection_shared_data[shared_key]
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
"""Host API required Work Files tool"""
|
||||
import os
|
||||
|
||||
from .launch_logic import get_stub
|
||||
|
||||
|
||||
def file_extensions():
|
||||
return [".aep"]
|
||||
|
||||
|
||||
def has_unsaved_changes():
|
||||
if _active_document():
|
||||
return not get_stub().is_saved()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def save_file(filepath):
|
||||
get_stub().saveAs(filepath, True)
|
||||
|
||||
|
||||
def open_file(filepath):
|
||||
get_stub().open(filepath)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def current_file():
|
||||
try:
|
||||
full_name = get_stub().get_active_document_full_name()
|
||||
if full_name and full_name != "null":
|
||||
return os.path.normpath(full_name).replace("\\", "/")
|
||||
except ValueError:
|
||||
print("Nothing opened")
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def work_root(session):
|
||||
return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/")
|
||||
|
||||
|
||||
def _active_document():
|
||||
# TODO merge with current_file - even in extension
|
||||
document_name = None
|
||||
try:
|
||||
document_name = get_stub().get_active_document_name()
|
||||
except ValueError:
|
||||
print("Nothing opened")
|
||||
pass
|
||||
|
||||
return document_name
|
||||
|
|
@ -418,18 +418,18 @@ class AfterEffectsServerStub():
|
|||
|
||||
return self._handle_return(res)
|
||||
|
||||
def get_render_info(self):
|
||||
def get_render_info(self, comp_id):
|
||||
""" Get render queue info for render purposes
|
||||
|
||||
Returns:
|
||||
(AEItem): with 'file_name' field
|
||||
(list) of (AEItem): with 'file_name' field
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.get_render_info'))
|
||||
('AfterEffects.get_render_info',
|
||||
comp_id=comp_id))
|
||||
|
||||
records = self._to_records(self._handle_return(res))
|
||||
if records:
|
||||
return records.pop()
|
||||
return records
|
||||
|
||||
def get_audio_url(self, item_id):
|
||||
""" Get audio layer absolute url for comp
|
||||
|
|
@ -522,7 +522,7 @@ class AfterEffectsServerStub():
|
|||
if records:
|
||||
return records.pop()
|
||||
|
||||
def render(self, folder_url):
|
||||
def render(self, folder_url, comp_id):
|
||||
"""
|
||||
Render all renderqueueitem to 'folder_url'
|
||||
Args:
|
||||
|
|
@ -531,7 +531,8 @@ class AfterEffectsServerStub():
|
|||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.render',
|
||||
folder_url=folder_url))
|
||||
folder_url=folder_url,
|
||||
comp_id=comp_id))
|
||||
return self._handle_return(res)
|
||||
|
||||
def get_extension_version(self):
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
from openpype.hosts.aftereffects.plugins.create import create_legacy_render
|
||||
|
||||
|
||||
class CreateLocalRender(create_legacy_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"
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
from openpype.pipeline import create
|
||||
from openpype.pipeline import CreatorError
|
||||
from openpype.hosts.aftereffects.api import (
|
||||
get_stub,
|
||||
list_instances
|
||||
)
|
||||
|
||||
|
||||
class CreateRender(create.LegacyCreator):
|
||||
"""Render folder for publish.
|
||||
|
||||
Creates subsets in format 'familyTaskSubsetname',
|
||||
eg 'renderCompositingMain'.
|
||||
|
||||
Create only single instance from composition at a time.
|
||||
"""
|
||||
|
||||
name = "renderDefault"
|
||||
label = "Render on Farm"
|
||||
family = "render"
|
||||
defaults = ["Main"]
|
||||
|
||||
def process(self):
|
||||
stub = get_stub() # only after After Effects is up
|
||||
items = []
|
||||
if (self.options or {}).get("useSelection"):
|
||||
items = stub.get_selected_items(
|
||||
comps=True, folders=False, footages=False
|
||||
)
|
||||
if len(items) > 1:
|
||||
raise CreatorError(
|
||||
"Please select only single composition at time."
|
||||
)
|
||||
|
||||
if not items:
|
||||
raise CreatorError((
|
||||
"Nothing to create. Select composition "
|
||||
"if 'useSelection' or create at least "
|
||||
"one composition."
|
||||
))
|
||||
|
||||
existing_subsets = [
|
||||
instance['subset'].lower()
|
||||
for instance in list_instances()
|
||||
]
|
||||
|
||||
item = items.pop()
|
||||
if self.name.lower() in existing_subsets:
|
||||
txt = "Instance with name \"{}\" already exists.".format(self.name)
|
||||
raise CreatorError(txt)
|
||||
|
||||
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,15 +1,24 @@
|
|||
import re
|
||||
|
||||
from openpype import resources
|
||||
from openpype.lib import BoolDef, UISeparatorDef
|
||||
from openpype.hosts.aftereffects import api
|
||||
from openpype.pipeline import (
|
||||
Creator,
|
||||
CreatedInstance,
|
||||
CreatorError,
|
||||
legacy_io,
|
||||
CreatorError
|
||||
)
|
||||
from openpype.hosts.aftereffects.api.pipeline import cache_and_get_instances
|
||||
from openpype.lib import prepare_template_data
|
||||
from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS
|
||||
|
||||
|
||||
class RenderCreator(Creator):
|
||||
"""Creates 'render' instance for publishing.
|
||||
|
||||
Result of 'render' instance is video or sequence of images for particular
|
||||
composition based of configuration in its RenderQueue.
|
||||
"""
|
||||
identifier = "render"
|
||||
label = "Render"
|
||||
family = "render"
|
||||
|
|
@ -24,11 +33,82 @@ class RenderCreator(Creator):
|
|||
["RenderCreator"]
|
||||
["defaults"])
|
||||
|
||||
def create(self, subset_name_from_ui, data, pre_create_data):
|
||||
stub = api.get_stub() # only after After Effects is up
|
||||
if pre_create_data.get("use_selection"):
|
||||
comps = stub.get_selected_items(
|
||||
comps=True, folders=False, footages=False
|
||||
)
|
||||
else:
|
||||
comps = stub.get_items(comps=True, folders=False, footages=False)
|
||||
|
||||
if not comps:
|
||||
raise CreatorError(
|
||||
"Nothing to create. Select composition "
|
||||
"if 'useSelection' or create at least "
|
||||
"one composition."
|
||||
)
|
||||
use_composition_name = (pre_create_data.get("use_composition_name") or
|
||||
len(comps) > 1)
|
||||
for comp in comps:
|
||||
if use_composition_name:
|
||||
if "{composition}" not in subset_name_from_ui.lower():
|
||||
subset_name_from_ui += "{Composition}"
|
||||
|
||||
composition_name = re.sub(
|
||||
"[^{}]+".format(SUBSET_NAME_ALLOWED_SYMBOLS),
|
||||
"",
|
||||
comp.name
|
||||
)
|
||||
|
||||
dynamic_fill = prepare_template_data({"composition":
|
||||
composition_name})
|
||||
subset_name = subset_name_from_ui.format(**dynamic_fill)
|
||||
data["composition_name"] = composition_name
|
||||
else:
|
||||
subset_name = subset_name_from_ui
|
||||
subset_name = re.sub(r"\{composition\}", '', subset_name,
|
||||
flags=re.IGNORECASE)
|
||||
|
||||
for inst in self.create_context.instances:
|
||||
if subset_name == inst.subset_name:
|
||||
raise CreatorError("{} already exists".format(
|
||||
inst.subset_name))
|
||||
|
||||
data["members"] = [comp.id]
|
||||
new_instance = CreatedInstance(self.family, subset_name, data,
|
||||
self)
|
||||
if "farm" in pre_create_data:
|
||||
use_farm = pre_create_data["farm"]
|
||||
new_instance.creator_attributes["farm"] = use_farm
|
||||
|
||||
api.get_stub().imprint(new_instance.id,
|
||||
new_instance.data_to_store())
|
||||
self._add_instance_to_context(new_instance)
|
||||
|
||||
stub.rename_item(comp.id, subset_name)
|
||||
|
||||
def get_default_variants(self):
|
||||
return self._default_variants
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
return [BoolDef("farm", label="Render on farm")]
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
output = [
|
||||
BoolDef("use_selection", default=True, label="Use selection"),
|
||||
BoolDef("use_composition_name",
|
||||
label="Use composition name in subset"),
|
||||
UISeparatorDef(),
|
||||
BoolDef("farm", label="Render on farm")
|
||||
]
|
||||
return output
|
||||
|
||||
def get_icon(self):
|
||||
return resources.get_openpype_splash_filepath()
|
||||
|
||||
def collect_instances(self):
|
||||
for instance_data in api.list_instances():
|
||||
for instance_data in cache_and_get_instances(self):
|
||||
# legacy instances have family=='render' or 'renderLocal', use them
|
||||
creator_id = (instance_data.get("creator_identifier") or
|
||||
instance_data.get("family", '').replace("Local", ''))
|
||||
|
|
@ -43,63 +123,65 @@ class RenderCreator(Creator):
|
|||
for created_inst, _changes in update_list:
|
||||
api.get_stub().imprint(created_inst.get("instance_id"),
|
||||
created_inst.data_to_store())
|
||||
subset_change = _changes.get("subset")
|
||||
if subset_change:
|
||||
api.get_stub().rename_item(created_inst.data["members"][0],
|
||||
subset_change.new_value)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
for instance in instances:
|
||||
api.remove_instance(instance)
|
||||
self._remove_instance_from_context(instance)
|
||||
self.host.remove_instance(instance)
|
||||
|
||||
def create(self, subset_name, data, pre_create_data):
|
||||
stub = api.get_stub() # only after After Effects is up
|
||||
if pre_create_data.get("use_selection"):
|
||||
items = stub.get_selected_items(
|
||||
comps=True, folders=False, footages=False
|
||||
)
|
||||
else:
|
||||
items = stub.get_items(comps=True, folders=False, footages=False)
|
||||
|
||||
if len(items) > 1:
|
||||
raise CreatorError(
|
||||
"Please select only single composition at time."
|
||||
)
|
||||
if not items:
|
||||
raise CreatorError((
|
||||
"Nothing to create. Select composition "
|
||||
"if 'useSelection' or create at least "
|
||||
"one composition."
|
||||
))
|
||||
|
||||
for inst in self.create_context.instances:
|
||||
if subset_name == inst.subset_name:
|
||||
raise CreatorError("{} already exists".format(
|
||||
inst.subset_name))
|
||||
|
||||
data["members"] = [items[0].id]
|
||||
new_instance = CreatedInstance(self.family, subset_name, data, self)
|
||||
if "farm" in pre_create_data:
|
||||
use_farm = pre_create_data["farm"]
|
||||
new_instance.creator_attributes["farm"] = use_farm
|
||||
|
||||
api.get_stub().imprint(new_instance.id,
|
||||
new_instance.data_to_store())
|
||||
self._add_instance_to_context(new_instance)
|
||||
|
||||
def get_default_variants(self):
|
||||
return self._default_variants
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
return [BoolDef("farm", label="Render on farm")]
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
output = [
|
||||
BoolDef("use_selection", default=True, label="Use selection"),
|
||||
UISeparatorDef(),
|
||||
BoolDef("farm", label="Render on farm")
|
||||
]
|
||||
return output
|
||||
subset = instance.data["subset"]
|
||||
comp_id = instance.data["members"][0]
|
||||
comp = api.get_stub().get_item(comp_id)
|
||||
if comp:
|
||||
new_comp_name = comp.name.replace(subset, '')
|
||||
if not new_comp_name:
|
||||
new_comp_name = "dummyCompName"
|
||||
api.get_stub().rename_item(comp_id,
|
||||
new_comp_name)
|
||||
|
||||
def get_detail_description(self):
|
||||
return """Creator for Render instances"""
|
||||
return """Creator for Render instances
|
||||
|
||||
Main publishable item in AfterEffects will be of `render` family.
|
||||
Result of this item (instance) is picture sequence or video that could
|
||||
be a final delivery product or loaded and used in another DCCs.
|
||||
|
||||
Select single composition and create instance of 'render' family or
|
||||
turn off 'Use selection' to create instance for all compositions.
|
||||
|
||||
'Use composition name in subset' allows to explicitly add composition
|
||||
name into created subset name.
|
||||
|
||||
Position of composition name could be set in
|
||||
`project_settings/global/tools/creator/subset_name_profiles` with some
|
||||
form of '{composition}' placeholder.
|
||||
|
||||
Composition name will be used implicitly if multiple composition should
|
||||
be handled at same time.
|
||||
|
||||
If {composition} placeholder is not us 'subset_name_profiles'
|
||||
composition name will be capitalized and set at the end of subset name
|
||||
if necessary.
|
||||
|
||||
If composition name should be used, it will be cleaned up of characters
|
||||
that would cause an issue in published file names.
|
||||
"""
|
||||
|
||||
def get_dynamic_data(self, variant, task_name, asset_doc,
|
||||
project_name, host_name, instance):
|
||||
dynamic_data = {}
|
||||
if instance is not None:
|
||||
composition_name = instance.get("composition_name")
|
||||
if composition_name:
|
||||
dynamic_data["composition"] = composition_name
|
||||
else:
|
||||
dynamic_data["composition"] = "{composition}"
|
||||
|
||||
return dynamic_data
|
||||
|
||||
def _handle_legacy(self, instance_data):
|
||||
"""Converts old instances to new format."""
|
||||
|
|
@ -112,7 +194,7 @@ class RenderCreator(Creator):
|
|||
instance_data.pop("uuid")
|
||||
|
||||
if not instance_data.get("task"):
|
||||
instance_data["task"] = legacy_io.Session.get("AVALON_TASK")
|
||||
instance_data["task"] = self.create_context.get_current_task_name()
|
||||
|
||||
if not instance_data.get("creator_attributes"):
|
||||
is_old_farm = instance_data["family"] != "renderLocal"
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import openpype.hosts.aftereffects.api as api
|
|||
from openpype.client import get_asset_by_name
|
||||
from openpype.pipeline import (
|
||||
AutoCreator,
|
||||
CreatedInstance,
|
||||
legacy_io,
|
||||
CreatedInstance
|
||||
)
|
||||
from openpype.hosts.aftereffects.api.pipeline import cache_and_get_instances
|
||||
|
||||
|
||||
class AEWorkfileCreator(AutoCreator):
|
||||
|
|
@ -17,7 +17,7 @@ class AEWorkfileCreator(AutoCreator):
|
|||
return []
|
||||
|
||||
def collect_instances(self):
|
||||
for instance_data in api.list_instances():
|
||||
for instance_data in cache_and_get_instances(self):
|
||||
creator_id = instance_data.get("creator_identifier")
|
||||
if creator_id == self.identifier:
|
||||
subset_name = instance_data["subset"]
|
||||
|
|
@ -37,10 +37,11 @@ class AEWorkfileCreator(AutoCreator):
|
|||
existing_instance = instance
|
||||
break
|
||||
|
||||
project_name = legacy_io.Session["AVALON_PROJECT"]
|
||||
asset_name = legacy_io.Session["AVALON_ASSET"]
|
||||
task_name = legacy_io.Session["AVALON_TASK"]
|
||||
host_name = legacy_io.Session["AVALON_APP"]
|
||||
context = self.create_context
|
||||
project_name = context.get_current_project_name()
|
||||
asset_name = context.get_current_asset_name()
|
||||
task_name = context.get_current_task_name()
|
||||
host_name = context.host_name
|
||||
|
||||
if existing_instance is None:
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
|
|
@ -55,7 +56,7 @@ class AEWorkfileCreator(AutoCreator):
|
|||
}
|
||||
data.update(self.get_dynamic_data(
|
||||
self.default_variant, task_name, asset_doc,
|
||||
project_name, host_name
|
||||
project_name, host_name, None
|
||||
))
|
||||
|
||||
new_instance = CreatedInstance(
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class AERenderInstance(RenderInstance):
|
|||
stagingDir = attr.ib(default=None)
|
||||
app_version = attr.ib(default=None)
|
||||
publish_attributes = attr.ib(default={})
|
||||
file_name = attr.ib(default=None)
|
||||
file_names = attr.ib(default=[])
|
||||
|
||||
|
||||
class CollectAERender(publish.AbstractCollectRender):
|
||||
|
|
@ -64,14 +64,13 @@ class CollectAERender(publish.AbstractCollectRender):
|
|||
if family not in ["render", "renderLocal"]: # legacy
|
||||
continue
|
||||
|
||||
item_id = inst.data["members"][0]
|
||||
comp_id = int(inst.data["members"][0])
|
||||
|
||||
work_area_info = CollectAERender.get_stub().get_work_area(
|
||||
int(item_id))
|
||||
work_area_info = CollectAERender.get_stub().get_work_area(comp_id)
|
||||
|
||||
if not work_area_info:
|
||||
self.log.warning("Orphaned instance, deleting metadata")
|
||||
inst_id = inst.get("instance_id") or item_id
|
||||
inst_id = inst.get("instance_id") or str(comp_id)
|
||||
CollectAERender.get_stub().remove_instance(inst_id)
|
||||
continue
|
||||
|
||||
|
|
@ -84,9 +83,10 @@ class CollectAERender(publish.AbstractCollectRender):
|
|||
|
||||
task_name = inst.data.get("task") # legacy
|
||||
|
||||
render_q = CollectAERender.get_stub().get_render_info()
|
||||
render_q = CollectAERender.get_stub().get_render_info(comp_id)
|
||||
if not render_q:
|
||||
raise ValueError("No file extension set in Render Queue")
|
||||
render_item = render_q[0]
|
||||
|
||||
subset_name = inst.data["subset"]
|
||||
instance = AERenderInstance(
|
||||
|
|
@ -103,8 +103,8 @@ class CollectAERender(publish.AbstractCollectRender):
|
|||
setMembers='',
|
||||
publish=True,
|
||||
name=subset_name,
|
||||
resolutionWidth=render_q.width,
|
||||
resolutionHeight=render_q.height,
|
||||
resolutionWidth=render_item.width,
|
||||
resolutionHeight=render_item.height,
|
||||
pixelAspect=1,
|
||||
tileRendering=False,
|
||||
tilesX=0,
|
||||
|
|
@ -115,16 +115,16 @@ class CollectAERender(publish.AbstractCollectRender):
|
|||
fps=fps,
|
||||
app_version=app_version,
|
||||
publish_attributes=inst.data.get("publish_attributes", {}),
|
||||
file_name=render_q.file_name
|
||||
file_names=[item.file_name for item in render_q]
|
||||
)
|
||||
|
||||
comp = compositions_by_id.get(int(item_id))
|
||||
comp = compositions_by_id.get(comp_id)
|
||||
if not comp:
|
||||
raise ValueError("There is no composition for item {}".
|
||||
format(item_id))
|
||||
format(comp_id))
|
||||
instance.outputDir = self._get_output_dir(instance)
|
||||
instance.comp_name = comp.name
|
||||
instance.comp_id = item_id
|
||||
instance.comp_id = comp_id
|
||||
|
||||
is_local = "renderLocal" in inst.data["family"] # legacy
|
||||
if inst.data.get("creator_attributes"):
|
||||
|
|
@ -163,28 +163,30 @@ class CollectAERender(publish.AbstractCollectRender):
|
|||
start = render_instance.frameStart
|
||||
end = render_instance.frameEnd
|
||||
|
||||
_, ext = os.path.splitext(os.path.basename(render_instance.file_name))
|
||||
|
||||
base_dir = self._get_output_dir(render_instance)
|
||||
expected_files = []
|
||||
if "#" not in render_instance.file_name: # single frame (mov)W
|
||||
path = os.path.join(base_dir, "{}_{}_{}.{}".format(
|
||||
render_instance.asset,
|
||||
render_instance.subset,
|
||||
"v{:03d}".format(render_instance.version),
|
||||
ext.replace('.', '')
|
||||
))
|
||||
expected_files.append(path)
|
||||
else:
|
||||
for frame in range(start, end + 1):
|
||||
path = os.path.join(base_dir, "{}_{}_{}.{}.{}".format(
|
||||
for file_name in render_instance.file_names:
|
||||
_, ext = os.path.splitext(os.path.basename(file_name))
|
||||
ext = ext.replace('.', '')
|
||||
version_str = "v{:03d}".format(render_instance.version)
|
||||
if "#" not in file_name: # single frame (mov)W
|
||||
path = os.path.join(base_dir, "{}_{}_{}.{}".format(
|
||||
render_instance.asset,
|
||||
render_instance.subset,
|
||||
"v{:03d}".format(render_instance.version),
|
||||
str(frame).zfill(self.padding_width),
|
||||
ext.replace('.', '')
|
||||
version_str,
|
||||
ext
|
||||
))
|
||||
expected_files.append(path)
|
||||
else:
|
||||
for frame in range(start, end + 1):
|
||||
path = os.path.join(base_dir, "{}_{}_{}.{}.{}".format(
|
||||
render_instance.asset,
|
||||
render_instance.subset,
|
||||
version_str,
|
||||
str(frame).zfill(self.padding_width),
|
||||
ext
|
||||
))
|
||||
expected_files.append(path)
|
||||
return expected_files
|
||||
|
||||
def _get_output_dir(self, render_instance):
|
||||
|
|
|
|||
|
|
@ -21,41 +21,55 @@ class ExtractLocalRender(publish.Extractor):
|
|||
def process(self, instance):
|
||||
stub = get_stub()
|
||||
staging_dir = instance.data["stagingDir"]
|
||||
self.log.info("staging_dir::{}".format(staging_dir))
|
||||
self.log.debug("staging_dir::{}".format(staging_dir))
|
||||
|
||||
# pull file name from Render Queue Output module
|
||||
render_q = stub.get_render_info()
|
||||
stub.render(staging_dir)
|
||||
if not render_q:
|
||||
# pull file name collected value from Render Queue Output module
|
||||
if not instance.data["file_names"]:
|
||||
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)
|
||||
comp_id = instance.data['comp_id']
|
||||
stub.render(staging_dir, comp_id)
|
||||
|
||||
resulting_files = files
|
||||
if len(files) == 1:
|
||||
resulting_files = files[0]
|
||||
representations = []
|
||||
for file_name in instance.data["file_names"]:
|
||||
_, ext = os.path.splitext(os.path.basename(file_name))
|
||||
ext = ext[1:]
|
||||
|
||||
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"]
|
||||
first_file_path = None
|
||||
files = []
|
||||
for found_file_name in os.listdir(staging_dir):
|
||||
if not found_file_name.endswith(ext):
|
||||
continue
|
||||
|
||||
instance.data["representations"] = [repre_data]
|
||||
files.append(found_file_name)
|
||||
if first_file_path is None:
|
||||
first_file_path = os.path.join(staging_dir,
|
||||
found_file_name)
|
||||
|
||||
if not files:
|
||||
self.log.info("no files")
|
||||
return
|
||||
|
||||
# single file cannot be wrapped in array
|
||||
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
|
||||
}
|
||||
first_repre = not representations
|
||||
if instance.data["review"] and first_repre:
|
||||
repre_data["tags"] = ["review"]
|
||||
|
||||
representations.append(repre_data)
|
||||
|
||||
instance.data["representations"] = representations
|
||||
|
||||
ffmpeg_path = get_ffmpeg_tool_path("ffmpeg")
|
||||
# Generate thumbnail.
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Context of the given subset doesn't match your current scene.
|
|||
|
||||
### How to repair?
|
||||
|
||||
You can fix this with "repair" button on the right.
|
||||
You can fix this with "repair" button on the right and refresh Publish at the bottom right.
|
||||
</description>
|
||||
<detail>
|
||||
### __Detailed Info__ (optional)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import json
|
||||
import pyblish.api
|
||||
from openpype.hosts.aftereffects.api import list_instances
|
||||
from openpype.hosts.aftereffects.api import AfterEffectsHost
|
||||
|
||||
|
||||
class PreCollectRender(pyblish.api.ContextPlugin):
|
||||
|
|
@ -25,7 +25,7 @@ class PreCollectRender(pyblish.api.ContextPlugin):
|
|||
self.log.debug("Not applicable for New Publisher, skip")
|
||||
return
|
||||
|
||||
for inst in list_instances():
|
||||
for inst in AfterEffectsHost().list_instances():
|
||||
if inst.get("creator_attributes"):
|
||||
raise ValueError("Instance created in New publisher, "
|
||||
"cannot be published in Pyblish.\n"
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ class AppendBlendLoader(plugin.AssetLoader):
|
|||
"""
|
||||
|
||||
representations = ["blend"]
|
||||
families = ["*"]
|
||||
families = ["workfile"]
|
||||
|
||||
label = "Append Workfile"
|
||||
order = 9
|
||||
|
|
@ -68,7 +68,7 @@ class ImportBlendLoader(plugin.AssetLoader):
|
|||
"""
|
||||
|
||||
representations = ["blend"]
|
||||
families = ["*"]
|
||||
families = ["workfile"]
|
||||
|
||||
label = "Import Workfile"
|
||||
order = 9
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin):
|
|||
order = ValidateContentsOrder
|
||||
hosts = ["blender"]
|
||||
families = ["camera"]
|
||||
version = (0, 1, 0)
|
||||
label = "Zero Keyframe"
|
||||
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ class ValidateMeshHasUvs(pyblish.api.InstancePlugin):
|
|||
order = ValidateContentsOrder
|
||||
hosts = ["blender"]
|
||||
families = ["model"]
|
||||
category = "geometry"
|
||||
label = "Mesh Has UV's"
|
||||
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]
|
||||
optional = True
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ class ValidateMeshNoNegativeScale(pyblish.api.Validator):
|
|||
order = ValidateContentsOrder
|
||||
hosts = ["blender"]
|
||||
families = ["model"]
|
||||
category = "geometry"
|
||||
label = "Mesh No Negative Scale"
|
||||
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ class ValidateNoColonsInName(pyblish.api.InstancePlugin):
|
|||
order = ValidateContentsOrder
|
||||
hosts = ["blender"]
|
||||
families = ["model", "rig"]
|
||||
version = (0, 1, 0)
|
||||
label = "No Colons in names"
|
||||
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ class ValidateTransformZero(pyblish.api.InstancePlugin):
|
|||
order = ValidateContentsOrder
|
||||
hosts = ["blender"]
|
||||
families = ["model"]
|
||||
version = (0, 1, 0)
|
||||
label = "Transform Zero"
|
||||
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import pyblish.api
|
||||
import argparse
|
||||
import sys
|
||||
from pprint import pformat
|
||||
|
||||
|
|
@ -11,20 +10,40 @@ class CollectCelactionCliKwargs(pyblish.api.Collector):
|
|||
order = pyblish.api.Collector.order - 0.1
|
||||
|
||||
def process(self, context):
|
||||
parser = argparse.ArgumentParser(prog="celaction")
|
||||
parser.add_argument("--currentFile",
|
||||
help="Pass file to Context as `currentFile`")
|
||||
parser.add_argument("--chunk",
|
||||
help=("Render chanks on farm"))
|
||||
parser.add_argument("--frameStart",
|
||||
help=("Start of frame range"))
|
||||
parser.add_argument("--frameEnd",
|
||||
help=("End of frame range"))
|
||||
parser.add_argument("--resolutionWidth",
|
||||
help=("Width of resolution"))
|
||||
parser.add_argument("--resolutionHeight",
|
||||
help=("Height of resolution"))
|
||||
passing_kwargs = parser.parse_args(sys.argv[1:]).__dict__
|
||||
args = list(sys.argv[1:])
|
||||
self.log.info(str(args))
|
||||
missing_kwargs = []
|
||||
passing_kwargs = {}
|
||||
for key in (
|
||||
"chunk",
|
||||
"frameStart",
|
||||
"frameEnd",
|
||||
"resolutionWidth",
|
||||
"resolutionHeight",
|
||||
"currentFile",
|
||||
):
|
||||
arg_key = f"--{key}"
|
||||
if arg_key not in args:
|
||||
missing_kwargs.append(key)
|
||||
continue
|
||||
arg_idx = args.index(arg_key)
|
||||
args.pop(arg_idx)
|
||||
if key != "currentFile":
|
||||
value = args.pop(arg_idx)
|
||||
else:
|
||||
path_parts = []
|
||||
while arg_idx < len(args):
|
||||
path_parts.append(args.pop(arg_idx))
|
||||
value = " ".join(path_parts).strip('"')
|
||||
|
||||
passing_kwargs[key] = value
|
||||
|
||||
if missing_kwargs:
|
||||
raise RuntimeError("Missing arguments {}".format(
|
||||
", ".join(
|
||||
[f'"{key}"' for key in missing_kwargs]
|
||||
)
|
||||
))
|
||||
|
||||
self.log.info("Storing kwargs ...")
|
||||
self.log.debug("_ passing_kwargs: {}".format(pformat(passing_kwargs)))
|
||||
|
|
|
|||
|
|
@ -702,6 +702,37 @@ class ClipLoader(LoaderPlugin):
|
|||
|
||||
_mapping = None
|
||||
|
||||
def apply_settings(cls, project_settings, system_settings):
|
||||
|
||||
plugin_type_settings = (
|
||||
project_settings
|
||||
.get("flame", {})
|
||||
.get("load", {})
|
||||
)
|
||||
|
||||
if not plugin_type_settings:
|
||||
return
|
||||
|
||||
plugin_name = cls.__name__
|
||||
|
||||
plugin_settings = None
|
||||
# Look for plugin settings in host specific settings
|
||||
if plugin_name in plugin_type_settings:
|
||||
plugin_settings = plugin_type_settings[plugin_name]
|
||||
|
||||
if not plugin_settings:
|
||||
return
|
||||
|
||||
print(">>> We have preset for {}".format(plugin_name))
|
||||
for option, value in plugin_settings.items():
|
||||
if option == "enabled" and value is False:
|
||||
print(" - is disabled by preset")
|
||||
elif option == "representations":
|
||||
continue
|
||||
else:
|
||||
print(" - setting `{}`: `{}`".format(option, value))
|
||||
setattr(cls, option, value)
|
||||
|
||||
def get_colorspace(self, context):
|
||||
"""Get colorspace name
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ import flame
|
|||
from pprint import pformat
|
||||
import openpype.hosts.flame.api as opfapi
|
||||
from openpype.lib import StringTemplate
|
||||
from openpype.lib.transcoding import (
|
||||
VIDEO_EXTENSIONS,
|
||||
IMAGE_EXTENSIONS
|
||||
)
|
||||
|
||||
|
||||
class LoadClip(opfapi.ClipLoader):
|
||||
|
|
@ -14,7 +18,10 @@ class LoadClip(opfapi.ClipLoader):
|
|||
"""
|
||||
|
||||
families = ["render2d", "source", "plate", "render", "review"]
|
||||
representations = ["exr", "dpx", "jpg", "jpeg", "png", "h264"]
|
||||
representations = ["*"]
|
||||
extensions = set(
|
||||
ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)
|
||||
)
|
||||
|
||||
label = "Load as clip"
|
||||
order = -10
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import flame
|
|||
from pprint import pformat
|
||||
import openpype.hosts.flame.api as opfapi
|
||||
from openpype.lib import StringTemplate
|
||||
|
||||
from openpype.lib.transcoding import (
|
||||
VIDEO_EXTENSIONS,
|
||||
IMAGE_EXTENSIONS
|
||||
)
|
||||
|
||||
class LoadClipBatch(opfapi.ClipLoader):
|
||||
"""Load a subset to timeline as clip
|
||||
|
|
@ -14,7 +17,10 @@ class LoadClipBatch(opfapi.ClipLoader):
|
|||
"""
|
||||
|
||||
families = ["render2d", "source", "plate", "render", "review"]
|
||||
representations = ["exr", "dpx", "jpg", "jpeg", "png", "h264"]
|
||||
representations = ["*"]
|
||||
extensions = set(
|
||||
ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)
|
||||
)
|
||||
|
||||
label = "Load as clip to current batch"
|
||||
order = -10
|
||||
|
|
|
|||
|
|
@ -143,6 +143,9 @@ class ExtractSubsetResources(publish.Extractor):
|
|||
# create staging dir path
|
||||
staging_dir = self.staging_dir(instance)
|
||||
|
||||
# append staging dir for later cleanup
|
||||
instance.context.data["cleanupFullPaths"].append(staging_dir)
|
||||
|
||||
# add default preset type for thumbnail and reviewable video
|
||||
# update them with settings and override in case the same
|
||||
# are found in there
|
||||
|
|
@ -548,30 +551,3 @@ class ExtractSubsetResources(publish.Extractor):
|
|||
"Path `{}` is containing more that one clip".format(path)
|
||||
)
|
||||
return clips[0]
|
||||
|
||||
def staging_dir(self, instance):
|
||||
"""Provide a temporary directory in which to store extracted files
|
||||
|
||||
Upon calling this method the staging directory is stored inside
|
||||
the instance.data['stagingDir']
|
||||
"""
|
||||
staging_dir = instance.data.get('stagingDir', None)
|
||||
openpype_temp_dir = os.getenv("OPENPYPE_TEMP_DIR")
|
||||
|
||||
if not staging_dir:
|
||||
if openpype_temp_dir and os.path.exists(openpype_temp_dir):
|
||||
staging_dir = os.path.normpath(
|
||||
tempfile.mkdtemp(
|
||||
prefix="pyblish_tmp_",
|
||||
dir=openpype_temp_dir
|
||||
)
|
||||
)
|
||||
else:
|
||||
staging_dir = os.path.normpath(
|
||||
tempfile.mkdtemp(prefix="pyblish_tmp_")
|
||||
)
|
||||
instance.data['stagingDir'] = staging_dir
|
||||
|
||||
instance.context.data["cleanupFullPaths"].append(staging_dir)
|
||||
|
||||
return staging_dir
|
||||
|
|
|
|||
|
|
@ -1,20 +1,11 @@
|
|||
from .pipeline import (
|
||||
install,
|
||||
uninstall,
|
||||
|
||||
FusionHost,
|
||||
ls,
|
||||
|
||||
imprint_container,
|
||||
parse_container
|
||||
)
|
||||
|
||||
from .workio import (
|
||||
open_file,
|
||||
save_file,
|
||||
current_file,
|
||||
has_unsaved_changes,
|
||||
file_extensions,
|
||||
work_root
|
||||
parse_container,
|
||||
list_instances,
|
||||
remove_instance
|
||||
)
|
||||
|
||||
from .lib import (
|
||||
|
|
@ -30,21 +21,11 @@ from .menu import launch_openpype_menu
|
|||
|
||||
__all__ = [
|
||||
# pipeline
|
||||
"install",
|
||||
"uninstall",
|
||||
"ls",
|
||||
|
||||
"imprint_container",
|
||||
"parse_container",
|
||||
|
||||
# workio
|
||||
"open_file",
|
||||
"save_file",
|
||||
"current_file",
|
||||
"has_unsaved_changes",
|
||||
"file_extensions",
|
||||
"work_root",
|
||||
|
||||
# lib
|
||||
"maintained_selection",
|
||||
"update_frame_range",
|
||||
|
|
|
|||
54
openpype/hosts/fusion/api/action.py
Normal file
54
openpype/hosts/fusion/api/action.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import pyblish.api
|
||||
|
||||
|
||||
from openpype.hosts.fusion.api.lib import get_current_comp
|
||||
from openpype.pipeline.publish import get_errored_instances_from_context
|
||||
|
||||
|
||||
class SelectInvalidAction(pyblish.api.Action):
|
||||
"""Select invalid nodes in Maya when plug-in failed.
|
||||
|
||||
To retrieve the invalid nodes this assumes a static `get_invalid()`
|
||||
method is available on the plugin.
|
||||
|
||||
"""
|
||||
label = "Select invalid"
|
||||
on = "failed" # This action is only available on a failed plug-in
|
||||
icon = "search" # Icon from Awesome Icon
|
||||
|
||||
def process(self, context, plugin):
|
||||
errored_instances = get_errored_instances_from_context(context)
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(errored_instances, plugin)
|
||||
|
||||
# Get the invalid nodes for the plug-ins
|
||||
self.log.info("Finding invalid nodes..")
|
||||
invalid = list()
|
||||
for instance in instances:
|
||||
invalid_nodes = plugin.get_invalid(instance)
|
||||
if invalid_nodes:
|
||||
if isinstance(invalid_nodes, (list, tuple)):
|
||||
invalid.extend(invalid_nodes)
|
||||
else:
|
||||
self.log.warning("Plug-in returned to be invalid, "
|
||||
"but has no selectable nodes.")
|
||||
|
||||
if not invalid:
|
||||
# Assume relevant comp is current comp and clear selection
|
||||
self.log.info("No invalid tools found.")
|
||||
comp = get_current_comp()
|
||||
flow = comp.CurrentFrame.FlowView
|
||||
flow.Select() # No args equals clearing selection
|
||||
return
|
||||
|
||||
# Assume a single comp
|
||||
first_tool = invalid[0]
|
||||
comp = first_tool.Comp()
|
||||
flow = comp.CurrentFrame.FlowView
|
||||
flow.Select() # No args equals clearing selection
|
||||
names = set()
|
||||
for tool in invalid:
|
||||
flow.Select(tool, True)
|
||||
names.add(tool.Name)
|
||||
self.log.info("Selecting invalid tools: %s" % ", ".join(sorted(names)))
|
||||
|
|
@ -210,7 +210,8 @@ def switch_item(container,
|
|||
if any(not x for x in [asset_name, subset_name, representation_name]):
|
||||
repre_id = container["representation"]
|
||||
representation = get_representation_by_id(project_name, repre_id)
|
||||
repre_parent_docs = get_representation_parents(representation)
|
||||
repre_parent_docs = get_representation_parents(
|
||||
project_name, representation)
|
||||
if repre_parent_docs:
|
||||
version, subset, asset, _ = repre_parent_docs
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ from openpype.style import load_stylesheet
|
|||
from openpype.lib import register_event_callback
|
||||
from openpype.hosts.fusion.scripts import (
|
||||
set_rendermode,
|
||||
duplicate_with_inputs
|
||||
duplicate_with_inputs,
|
||||
)
|
||||
from openpype.hosts.fusion.api.lib import (
|
||||
set_asset_framerange,
|
||||
set_asset_resolution
|
||||
set_asset_resolution,
|
||||
)
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.resources import get_openpype_icon_filepath
|
||||
|
|
@ -45,17 +45,19 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
self.setWindowTitle("OpenPype")
|
||||
|
||||
asset_label = QtWidgets.QLabel("Context", self)
|
||||
asset_label.setStyleSheet("""QLabel {
|
||||
asset_label.setStyleSheet(
|
||||
"""QLabel {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #5f9fb8;
|
||||
}""")
|
||||
}"""
|
||||
)
|
||||
asset_label.setAlignment(QtCore.Qt.AlignHCenter)
|
||||
|
||||
workfiles_btn = QtWidgets.QPushButton("Workfiles...", self)
|
||||
create_btn = QtWidgets.QPushButton("Create...", self)
|
||||
publish_btn = QtWidgets.QPushButton("Publish...", self)
|
||||
load_btn = QtWidgets.QPushButton("Load...", self)
|
||||
publish_btn = QtWidgets.QPushButton("Publish...", self)
|
||||
manager_btn = QtWidgets.QPushButton("Manage...", self)
|
||||
libload_btn = QtWidgets.QPushButton("Library...", self)
|
||||
rendermode_btn = QtWidgets.QPushButton("Set render mode...", self)
|
||||
|
|
@ -108,7 +110,8 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
libload_btn.clicked.connect(self.on_libload_clicked)
|
||||
rendermode_btn.clicked.connect(self.on_rendermode_clicked)
|
||||
duplicate_with_inputs_btn.clicked.connect(
|
||||
self.on_duplicate_with_inputs_clicked)
|
||||
self.on_duplicate_with_inputs_clicked
|
||||
)
|
||||
set_resolution_btn.clicked.connect(self.on_set_resolution_clicked)
|
||||
set_framerange_btn.clicked.connect(self.on_set_framerange_clicked)
|
||||
|
||||
|
|
@ -130,7 +133,6 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
self.asset_label.setText(label)
|
||||
|
||||
def register_callback(self, name, fn):
|
||||
|
||||
# Create a wrapper callback that we only store
|
||||
# for as long as we want it to persist as callback
|
||||
def _callback(*args):
|
||||
|
|
@ -146,10 +148,10 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
host_tools.show_workfiles()
|
||||
|
||||
def on_create_clicked(self):
|
||||
host_tools.show_creator()
|
||||
host_tools.show_publisher(tab="create")
|
||||
|
||||
def on_publish_clicked(self):
|
||||
host_tools.show_publish()
|
||||
host_tools.show_publisher(tab="publish")
|
||||
|
||||
def on_load_clicked(self):
|
||||
host_tools.show_loader(use_context=True)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ Basic avalon integration
|
|||
import os
|
||||
import sys
|
||||
import logging
|
||||
import contextlib
|
||||
|
||||
import pyblish.api
|
||||
from qtpy import QtCore
|
||||
|
|
@ -17,15 +18,14 @@ from openpype.pipeline import (
|
|||
register_loader_plugin_path,
|
||||
register_creator_plugin_path,
|
||||
register_inventory_action_path,
|
||||
deregister_loader_plugin_path,
|
||||
deregister_creator_plugin_path,
|
||||
deregister_inventory_action_path,
|
||||
AVALON_CONTAINER_ID,
|
||||
)
|
||||
from openpype.pipeline.load import any_outdated_containers
|
||||
from openpype.hosts.fusion import FUSION_HOST_DIR
|
||||
from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost
|
||||
from openpype.tools.utils import host_tools
|
||||
|
||||
|
||||
from .lib import (
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk,
|
||||
|
|
@ -66,94 +66,98 @@ class FusionLogHandler(logging.Handler):
|
|||
self.print(entry)
|
||||
|
||||
|
||||
def install():
|
||||
"""Install fusion-specific functionality of OpenPype.
|
||||
class FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
||||
name = "fusion"
|
||||
|
||||
This is where you install menus and register families, data
|
||||
and loaders into fusion.
|
||||
def install(self):
|
||||
"""Install fusion-specific functionality of OpenPype.
|
||||
|
||||
It is called automatically when installing via
|
||||
`openpype.pipeline.install_host(openpype.hosts.fusion.api)`
|
||||
This is where you install menus and register families, data
|
||||
and loaders into fusion.
|
||||
|
||||
See the Maya equivalent for inspiration on how to implement this.
|
||||
It is called automatically when installing via
|
||||
`openpype.pipeline.install_host(openpype.hosts.fusion.api)`
|
||||
|
||||
"""
|
||||
# Remove all handlers associated with the root logger object, because
|
||||
# that one always logs as "warnings" incorrectly.
|
||||
for handler in logging.root.handlers[:]:
|
||||
logging.root.removeHandler(handler)
|
||||
See the Maya equivalent for inspiration on how to implement this.
|
||||
|
||||
# Attach default logging handler that prints to active comp
|
||||
logger = logging.getLogger()
|
||||
formatter = logging.Formatter(fmt="%(message)s\n")
|
||||
handler = FusionLogHandler()
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
"""
|
||||
# Remove all handlers associated with the root logger object, because
|
||||
# that one always logs as "warnings" incorrectly.
|
||||
for handler in logging.root.handlers[:]:
|
||||
logging.root.removeHandler(handler)
|
||||
|
||||
pyblish.api.register_host("fusion")
|
||||
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||
log.info("Registering Fusion plug-ins..")
|
||||
# Attach default logging handler that prints to active comp
|
||||
logger = logging.getLogger()
|
||||
formatter = logging.Formatter(fmt="%(message)s\n")
|
||||
handler = FusionLogHandler()
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
register_loader_plugin_path(LOAD_PATH)
|
||||
register_creator_plugin_path(CREATE_PATH)
|
||||
register_inventory_action_path(INVENTORY_PATH)
|
||||
pyblish.api.register_host("fusion")
|
||||
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||
log.info("Registering Fusion plug-ins..")
|
||||
|
||||
pyblish.api.register_callback(
|
||||
"instanceToggled", on_pyblish_instance_toggled
|
||||
)
|
||||
register_loader_plugin_path(LOAD_PATH)
|
||||
register_creator_plugin_path(CREATE_PATH)
|
||||
register_inventory_action_path(INVENTORY_PATH)
|
||||
|
||||
# Register events
|
||||
register_event_callback("open", on_after_open)
|
||||
register_event_callback("save", on_save)
|
||||
register_event_callback("new", on_new)
|
||||
# Register events
|
||||
register_event_callback("open", on_after_open)
|
||||
register_event_callback("save", on_save)
|
||||
register_event_callback("new", on_new)
|
||||
|
||||
# region workfile io api
|
||||
def has_unsaved_changes(self):
|
||||
comp = get_current_comp()
|
||||
return comp.GetAttrs()["COMPB_Modified"]
|
||||
|
||||
def uninstall():
|
||||
"""Uninstall all that was installed
|
||||
def get_workfile_extensions(self):
|
||||
return [".comp"]
|
||||
|
||||
This is where you undo everything that was done in `install()`.
|
||||
That means, removing menus, deregistering families and data
|
||||
and everything. It should be as though `install()` was never run,
|
||||
because odds are calling this function means the user is interested
|
||||
in re-installing shortly afterwards. If, for example, he has been
|
||||
modifying the menu or registered families.
|
||||
def save_workfile(self, dst_path=None):
|
||||
comp = get_current_comp()
|
||||
comp.Save(dst_path)
|
||||
|
||||
"""
|
||||
pyblish.api.deregister_host("fusion")
|
||||
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
|
||||
log.info("Deregistering Fusion plug-ins..")
|
||||
def open_workfile(self, filepath):
|
||||
# Hack to get fusion, see
|
||||
# openpype.hosts.fusion.api.pipeline.get_current_comp()
|
||||
fusion = getattr(sys.modules["__main__"], "fusion", None)
|
||||
|
||||
deregister_loader_plugin_path(LOAD_PATH)
|
||||
deregister_creator_plugin_path(CREATE_PATH)
|
||||
deregister_inventory_action_path(INVENTORY_PATH)
|
||||
return fusion.LoadComp(filepath)
|
||||
|
||||
pyblish.api.deregister_callback(
|
||||
"instanceToggled", on_pyblish_instance_toggled
|
||||
)
|
||||
def get_current_workfile(self):
|
||||
comp = get_current_comp()
|
||||
current_filepath = comp.GetAttrs()["COMPS_FileName"]
|
||||
if not current_filepath:
|
||||
return None
|
||||
|
||||
return current_filepath
|
||||
|
||||
def on_pyblish_instance_toggled(instance, old_value, new_value):
|
||||
"""Toggle saver tool passthrough states on instance toggles."""
|
||||
comp = instance.context.data.get("currentComp")
|
||||
if not comp:
|
||||
return
|
||||
def work_root(self, session):
|
||||
work_dir = session["AVALON_WORKDIR"]
|
||||
scene_dir = session.get("AVALON_SCENEDIR")
|
||||
if scene_dir:
|
||||
return os.path.join(work_dir, scene_dir)
|
||||
else:
|
||||
return work_dir
|
||||
# endregion
|
||||
|
||||
savers = [tool for tool in instance if
|
||||
getattr(tool, "ID", None) == "Saver"]
|
||||
if not savers:
|
||||
return
|
||||
@contextlib.contextmanager
|
||||
def maintained_selection(self):
|
||||
from .lib import maintained_selection
|
||||
return maintained_selection()
|
||||
|
||||
# Whether instances should be passthrough based on new value
|
||||
passthrough = not new_value
|
||||
with comp_lock_and_undo_chunk(comp,
|
||||
undo_queue_name="Change instance "
|
||||
"active state"):
|
||||
for tool in savers:
|
||||
attrs = tool.GetAttrs()
|
||||
current = attrs["TOOLB_PassThrough"]
|
||||
if current != passthrough:
|
||||
tool.SetAttrs({"TOOLB_PassThrough": passthrough})
|
||||
def get_containers(self):
|
||||
return ls()
|
||||
|
||||
def update_context_data(self, data, changes):
|
||||
comp = get_current_comp()
|
||||
comp.SetData("openpype", data)
|
||||
|
||||
def get_context_data(self):
|
||||
comp = get_current_comp()
|
||||
return comp.GetData("openpype") or {}
|
||||
|
||||
|
||||
def on_new(event):
|
||||
|
|
@ -283,9 +287,51 @@ def parse_container(tool):
|
|||
return container
|
||||
|
||||
|
||||
# TODO: Function below is currently unused prototypes
|
||||
def list_instances(creator_id=None):
|
||||
"""Return created instances in current workfile which will be published.
|
||||
Returns:
|
||||
(list) of dictionaries matching instances format
|
||||
"""
|
||||
|
||||
comp = get_current_comp()
|
||||
tools = comp.GetToolList(False).values()
|
||||
|
||||
instance_signature = {
|
||||
"id": "pyblish.avalon.instance",
|
||||
"identifier": creator_id
|
||||
}
|
||||
instances = []
|
||||
for tool in tools:
|
||||
|
||||
data = tool.GetData('openpype')
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
|
||||
if data.get("id") != instance_signature["id"]:
|
||||
continue
|
||||
|
||||
if creator_id and data.get("identifier") != creator_id:
|
||||
continue
|
||||
|
||||
instances.append(tool)
|
||||
|
||||
return instances
|
||||
|
||||
|
||||
# TODO: Function below is currently unused prototypes
|
||||
def remove_instance(instance):
|
||||
"""Remove instance from current workfile.
|
||||
|
||||
Args:
|
||||
instance (dict): instance representation from subsetmanager model
|
||||
"""
|
||||
# Assume instance is a Fusion tool directly
|
||||
instance["tool"].Delete()
|
||||
|
||||
|
||||
class FusionEventThread(QtCore.QThread):
|
||||
"""QThread which will periodically ping Fusion app for any events.
|
||||
|
||||
The fusion.UIManager must be set up to be notified of events before they'll
|
||||
be reported by this thread, for example:
|
||||
fusion.UIManager.AddNotify("Comp_Save", None)
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
"""Host API required Work Files tool"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
from .lib import get_current_comp
|
||||
|
||||
|
||||
def file_extensions():
|
||||
return [".comp"]
|
||||
|
||||
|
||||
def has_unsaved_changes():
|
||||
comp = get_current_comp()
|
||||
return comp.GetAttrs()["COMPB_Modified"]
|
||||
|
||||
|
||||
def save_file(filepath):
|
||||
comp = get_current_comp()
|
||||
comp.Save(filepath)
|
||||
|
||||
|
||||
def open_file(filepath):
|
||||
# Hack to get fusion, see
|
||||
# openpype.hosts.fusion.api.pipeline.get_current_comp()
|
||||
fusion = getattr(sys.modules["__main__"], "fusion", None)
|
||||
|
||||
return fusion.LoadComp(filepath)
|
||||
|
||||
|
||||
def current_file():
|
||||
comp = get_current_comp()
|
||||
current_filepath = comp.GetAttrs()["COMPS_FileName"]
|
||||
if not current_filepath:
|
||||
return None
|
||||
|
||||
return current_filepath
|
||||
|
||||
|
||||
def work_root(session):
|
||||
work_dir = session["AVALON_WORKDIR"]
|
||||
scene_dir = session.get("AVALON_SCENEDIR")
|
||||
if scene_dir:
|
||||
return os.path.join(work_dir, scene_dir)
|
||||
else:
|
||||
return work_dir
|
||||
|
|
@ -13,11 +13,11 @@ def main(env):
|
|||
# However the contents of that folder can conflict with Qt library dlls
|
||||
# so we make sure to move out of it to avoid DLL Load Failed errors.
|
||||
os.chdir("..")
|
||||
from openpype.hosts.fusion import api
|
||||
from openpype.hosts.fusion.api import FusionHost
|
||||
from openpype.hosts.fusion.api import menu
|
||||
|
||||
# activate resolve from pype
|
||||
install_host(api)
|
||||
install_host(FusionHost())
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
log.info(f"Registered host: {registered_host()}")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
import platform
|
||||
from openpype.lib import PreLaunchHook
|
||||
|
||||
from openpype.lib import PreLaunchHook, ApplicationLaunchFailed
|
||||
from openpype.pipeline.colorspace import get_imageio_config
|
||||
from openpype.pipeline.template_data import get_template_data_with_names
|
||||
|
||||
|
||||
class FusionPreLaunchOCIO(PreLaunchHook):
|
||||
|
|
@ -11,24 +11,22 @@ class FusionPreLaunchOCIO(PreLaunchHook):
|
|||
def execute(self):
|
||||
"""Hook entry method."""
|
||||
|
||||
# get image io
|
||||
project_settings = self.data["project_settings"]
|
||||
template_data = get_template_data_with_names(
|
||||
project_name=self.data["project_name"],
|
||||
asset_name=self.data["asset_name"],
|
||||
task_name=self.data["task_name"],
|
||||
host_name=self.host_name,
|
||||
system_settings=self.data["system_settings"]
|
||||
)
|
||||
|
||||
# make sure anatomy settings are having flame key
|
||||
imageio_fusion = project_settings["fusion"]["imageio"]
|
||||
|
||||
ocio = imageio_fusion.get("ocio")
|
||||
enabled = ocio.get("enabled", False)
|
||||
if not enabled:
|
||||
return
|
||||
|
||||
platform_key = platform.system().lower()
|
||||
ocio_path = ocio["configFilePath"][platform_key]
|
||||
if not ocio_path:
|
||||
raise ApplicationLaunchFailed(
|
||||
"Fusion OCIO is enabled in project settings but no OCIO config"
|
||||
f"path is set for your current platform: {platform_key}"
|
||||
)
|
||||
config_data = get_imageio_config(
|
||||
project_name=self.data["project_name"],
|
||||
host_name=self.host_name,
|
||||
project_settings=self.data["project_settings"],
|
||||
anatomy_data=template_data,
|
||||
anatomy=self.data["anatomy"]
|
||||
)
|
||||
ocio_path = config_data["path"]
|
||||
|
||||
self.log.info(f"Setting OCIO config path: {ocio_path}")
|
||||
self.launch_context.env["OCIO"] = os.pathsep.join(ocio_path)
|
||||
self.launch_context.env["OCIO"] = ocio_path
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class FusionPrelaunch(PreLaunchHook):
|
|||
"Make sure the environment in fusion settings has "
|
||||
"'FUSION_PYTHON3_HOME' set correctly and make sure "
|
||||
"Python 3 is installed in the given path."
|
||||
f"\n\nPYTHON36: {fusion_python3_home}"
|
||||
f"\n\nPYTHON PATH: {fusion_python3_home}"
|
||||
)
|
||||
|
||||
self.log.info(f"Setting {py3_var}: '{py3_dir}'...")
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
import os
|
||||
|
||||
from openpype.pipeline import (
|
||||
LegacyCreator,
|
||||
legacy_io
|
||||
)
|
||||
from openpype.hosts.fusion.api import (
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk
|
||||
)
|
||||
|
||||
|
||||
class CreateOpenEXRSaver(LegacyCreator):
|
||||
|
||||
name = "openexrDefault"
|
||||
label = "Create OpenEXR Saver"
|
||||
hosts = ["fusion"]
|
||||
family = "render"
|
||||
defaults = ["Main"]
|
||||
|
||||
def process(self):
|
||||
|
||||
file_format = "OpenEXRFormat"
|
||||
|
||||
comp = get_current_comp()
|
||||
|
||||
workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"])
|
||||
|
||||
filename = "{}..exr".format(self.name)
|
||||
filepath = os.path.join(workdir, "render", filename)
|
||||
|
||||
with comp_lock_and_undo_chunk(comp):
|
||||
args = (-32768, -32768) # Magical position numbers
|
||||
saver = comp.AddTool("Saver", *args)
|
||||
saver.SetAttrs({"TOOLS_Name": self.name})
|
||||
|
||||
# Setting input attributes is different from basic attributes
|
||||
# Not confused with "MainInputAttributes" which
|
||||
saver["Clip"] = filepath
|
||||
saver["OutputFormat"] = file_format
|
||||
|
||||
# Check file format settings are available
|
||||
if saver[file_format] is None:
|
||||
raise RuntimeError("File format is not set to {}, "
|
||||
"this is a bug".format(file_format))
|
||||
|
||||
# Set file format attributes
|
||||
saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other
|
||||
saver[file_format]["SaveAlpha"] = 0
|
||||
215
openpype/hosts/fusion/plugins/create/create_saver.py
Normal file
215
openpype/hosts/fusion/plugins/create/create_saver.py
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
import os
|
||||
|
||||
import qtawesome
|
||||
|
||||
from openpype.hosts.fusion.api import (
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk
|
||||
)
|
||||
|
||||
from openpype.lib import BoolDef
|
||||
from openpype.pipeline import (
|
||||
legacy_io,
|
||||
Creator,
|
||||
CreatedInstance
|
||||
)
|
||||
from openpype.client import get_asset_by_name
|
||||
|
||||
|
||||
class CreateSaver(Creator):
|
||||
identifier = "io.openpype.creators.fusion.saver"
|
||||
name = "saver"
|
||||
label = "Saver"
|
||||
family = "render"
|
||||
default_variants = ["Main"]
|
||||
|
||||
description = "Fusion Saver to generate image sequence"
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
|
||||
# TODO: Add pre_create attributes to choose file format?
|
||||
file_format = "OpenEXRFormat"
|
||||
|
||||
comp = get_current_comp()
|
||||
with comp_lock_and_undo_chunk(comp):
|
||||
args = (-32768, -32768) # Magical position numbers
|
||||
saver = comp.AddTool("Saver", *args)
|
||||
|
||||
instance_data["subset"] = subset_name
|
||||
self._update_tool_with_data(saver, data=instance_data)
|
||||
|
||||
saver["OutputFormat"] = file_format
|
||||
|
||||
# Check file format settings are available
|
||||
if saver[file_format] is None:
|
||||
raise RuntimeError(
|
||||
f"File format is not set to {file_format}, this is a bug"
|
||||
)
|
||||
|
||||
# Set file format attributes
|
||||
saver[file_format]["Depth"] = 0 # Auto | float16 | float32
|
||||
# TODO Is this needed?
|
||||
saver[file_format]["SaveAlpha"] = 1
|
||||
|
||||
self._imprint(saver, instance_data)
|
||||
|
||||
# Register the CreatedInstance
|
||||
instance = CreatedInstance(
|
||||
family=self.family,
|
||||
subset_name=subset_name,
|
||||
data=instance_data,
|
||||
creator=self)
|
||||
|
||||
# Insert the transient data
|
||||
instance.transient_data["tool"] = saver
|
||||
|
||||
self._add_instance_to_context(instance)
|
||||
|
||||
return instance
|
||||
|
||||
def collect_instances(self):
|
||||
|
||||
comp = get_current_comp()
|
||||
tools = comp.GetToolList(False, "Saver").values()
|
||||
for tool in tools:
|
||||
|
||||
data = self.get_managed_tool_data(tool)
|
||||
if not data:
|
||||
data = self._collect_unmanaged_saver(tool)
|
||||
|
||||
# Add instance
|
||||
created_instance = CreatedInstance.from_existing(data, self)
|
||||
|
||||
# Collect transient data
|
||||
created_instance.transient_data["tool"] = tool
|
||||
|
||||
self._add_instance_to_context(created_instance)
|
||||
|
||||
def get_icon(self):
|
||||
return qtawesome.icon("fa.eye", color="white")
|
||||
|
||||
def update_instances(self, update_list):
|
||||
for created_inst, _changes in update_list:
|
||||
|
||||
new_data = created_inst.data_to_store()
|
||||
tool = created_inst.transient_data["tool"]
|
||||
self._update_tool_with_data(tool, new_data)
|
||||
self._imprint(tool, new_data)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
for instance in instances:
|
||||
# Remove the tool from the scene
|
||||
|
||||
tool = instance.transient_data["tool"]
|
||||
if tool:
|
||||
tool.Delete()
|
||||
|
||||
# Remove the collected CreatedInstance to remove from UI directly
|
||||
self._remove_instance_from_context(instance)
|
||||
|
||||
def _imprint(self, tool, data):
|
||||
# Save all data in a "openpype.{key}" = value data
|
||||
|
||||
active = data.pop("active", None)
|
||||
if active is not None:
|
||||
# Use active value to set the passthrough state
|
||||
tool.SetAttrs({"TOOLB_PassThrough": not active})
|
||||
|
||||
for key, value in data.items():
|
||||
tool.SetData(f"openpype.{key}", value)
|
||||
|
||||
def _update_tool_with_data(self, tool, data):
|
||||
"""Update tool node name and output path based on subset data"""
|
||||
if "subset" not in data:
|
||||
return
|
||||
|
||||
original_subset = tool.GetData("openpype.subset")
|
||||
subset = data["subset"]
|
||||
if original_subset != subset:
|
||||
# Subset change detected
|
||||
# Update output filepath
|
||||
workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"])
|
||||
filename = f"{subset}..exr"
|
||||
filepath = os.path.join(workdir, "render", subset, filename)
|
||||
tool["Clip"] = filepath
|
||||
|
||||
# Rename tool
|
||||
if tool.Name != subset:
|
||||
print(f"Renaming {tool.Name} -> {subset}")
|
||||
tool.SetAttrs({"TOOLS_Name": subset})
|
||||
|
||||
def _collect_unmanaged_saver(self, tool):
|
||||
|
||||
# TODO: this should not be done this way - this should actually
|
||||
# get the data as stored on the tool explicitly (however)
|
||||
# that would disallow any 'regular saver' to be collected
|
||||
# unless the instance data is stored on it to begin with
|
||||
|
||||
print("Collecting unmanaged saver..")
|
||||
comp = tool.Comp()
|
||||
|
||||
# Allow regular non-managed savers to also be picked up
|
||||
project = legacy_io.Session["AVALON_PROJECT"]
|
||||
asset = legacy_io.Session["AVALON_ASSET"]
|
||||
task = legacy_io.Session["AVALON_TASK"]
|
||||
|
||||
asset_doc = get_asset_by_name(project_name=project,
|
||||
asset_name=asset)
|
||||
|
||||
path = tool["Clip"][comp.TIME_UNDEFINED]
|
||||
fname = os.path.basename(path)
|
||||
fname, _ext = os.path.splitext(fname)
|
||||
variant = fname.rstrip(".")
|
||||
subset = self.get_subset_name(
|
||||
variant=variant,
|
||||
task_name=task,
|
||||
asset_doc=asset_doc,
|
||||
project_name=project,
|
||||
)
|
||||
|
||||
attrs = tool.GetAttrs()
|
||||
passthrough = attrs["TOOLB_PassThrough"]
|
||||
return {
|
||||
# Required data
|
||||
"project": project,
|
||||
"asset": asset,
|
||||
"subset": subset,
|
||||
"task": task,
|
||||
"variant": variant,
|
||||
"active": not passthrough,
|
||||
"family": self.family,
|
||||
|
||||
# Unique identifier for instance and this creator
|
||||
"id": "pyblish.avalon.instance",
|
||||
"creator_identifier": self.identifier
|
||||
}
|
||||
|
||||
def get_managed_tool_data(self, tool):
|
||||
"""Return data of the tool if it matches creator identifier"""
|
||||
data = tool.GetData('openpype')
|
||||
if not isinstance(data, dict):
|
||||
return
|
||||
|
||||
required = {
|
||||
"id": "pyblish.avalon.instance",
|
||||
"creator_identifier": self.identifier
|
||||
}
|
||||
for key, value in required.items():
|
||||
if key not in data or data[key] != value:
|
||||
return
|
||||
|
||||
# Get active state from the actual tool state
|
||||
attrs = tool.GetAttrs()
|
||||
passthrough = attrs["TOOLB_PassThrough"]
|
||||
data["active"] = not passthrough
|
||||
|
||||
return data
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
return [
|
||||
BoolDef(
|
||||
"review",
|
||||
default=True,
|
||||
label="Review"
|
||||
)
|
||||
]
|
||||
109
openpype/hosts/fusion/plugins/create/create_workfile.py
Normal file
109
openpype/hosts/fusion/plugins/create/create_workfile.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import qtawesome
|
||||
|
||||
from openpype.hosts.fusion.api import (
|
||||
get_current_comp
|
||||
)
|
||||
from openpype.client import get_asset_by_name
|
||||
from openpype.pipeline import (
|
||||
AutoCreator,
|
||||
CreatedInstance,
|
||||
legacy_io,
|
||||
)
|
||||
|
||||
|
||||
class FusionWorkfileCreator(AutoCreator):
|
||||
identifier = "workfile"
|
||||
family = "workfile"
|
||||
label = "Workfile"
|
||||
|
||||
default_variant = "Main"
|
||||
|
||||
create_allow_context_change = False
|
||||
|
||||
data_key = "openpype_workfile"
|
||||
|
||||
def collect_instances(self):
|
||||
|
||||
comp = get_current_comp()
|
||||
data = comp.GetData(self.data_key)
|
||||
if not data:
|
||||
return
|
||||
|
||||
instance = CreatedInstance(
|
||||
family=self.family,
|
||||
subset_name=data["subset"],
|
||||
data=data,
|
||||
creator=self
|
||||
)
|
||||
instance.transient_data["comp"] = comp
|
||||
|
||||
self._add_instance_to_context(instance)
|
||||
|
||||
def update_instances(self, update_list):
|
||||
for created_inst, _changes in update_list:
|
||||
comp = created_inst.transient_data["comp"]
|
||||
if not hasattr(comp, "SetData"):
|
||||
# Comp is not alive anymore, likely closed by the user
|
||||
self.log.error("Workfile comp not found for existing instance."
|
||||
" Comp might have been closed in the meantime.")
|
||||
continue
|
||||
|
||||
# Imprint data into the comp
|
||||
data = created_inst.data_to_store()
|
||||
comp.SetData(self.data_key, data)
|
||||
|
||||
def create(self, options=None):
|
||||
|
||||
comp = get_current_comp()
|
||||
if not comp:
|
||||
self.log.error("Unable to find current comp")
|
||||
return
|
||||
|
||||
existing_instance = None
|
||||
for instance in self.create_context.instances:
|
||||
if instance.family == self.family:
|
||||
existing_instance = instance
|
||||
break
|
||||
|
||||
project_name = legacy_io.Session["AVALON_PROJECT"]
|
||||
asset_name = legacy_io.Session["AVALON_ASSET"]
|
||||
task_name = legacy_io.Session["AVALON_TASK"]
|
||||
host_name = legacy_io.Session["AVALON_APP"]
|
||||
|
||||
if existing_instance is None:
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
self.default_variant, task_name, asset_doc,
|
||||
project_name, host_name
|
||||
)
|
||||
data = {
|
||||
"asset": asset_name,
|
||||
"task": task_name,
|
||||
"variant": self.default_variant
|
||||
}
|
||||
data.update(self.get_dynamic_data(
|
||||
self.default_variant, task_name, asset_doc,
|
||||
project_name, host_name, None
|
||||
))
|
||||
|
||||
new_instance = CreatedInstance(
|
||||
self.family, subset_name, data, self
|
||||
)
|
||||
new_instance.transient_data["comp"] = comp
|
||||
self._add_instance_to_context(new_instance)
|
||||
|
||||
elif (
|
||||
existing_instance["asset"] != asset_name
|
||||
or existing_instance["task"] != task_name
|
||||
):
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
self.default_variant, task_name, asset_doc,
|
||||
project_name, host_name
|
||||
)
|
||||
existing_instance["asset"] = asset_name
|
||||
existing_instance["task"] = task_name
|
||||
existing_instance["subset"] = subset_name
|
||||
|
||||
def get_icon(self):
|
||||
return qtawesome.icon("fa.file-o", color="white")
|
||||
|
|
@ -15,6 +15,7 @@ class FusionSetFrameRangeLoader(load.LoaderPlugin):
|
|||
"pointcache",
|
||||
"render"]
|
||||
representations = ["*"]
|
||||
extensions = {"*"}
|
||||
|
||||
label = "Set frame range"
|
||||
order = 11
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ class FusionLoadAlembicMesh(load.LoaderPlugin):
|
|||
"""Load Alembic mesh into Fusion"""
|
||||
|
||||
families = ["pointcache", "model"]
|
||||
representations = ["abc"]
|
||||
representations = ["*"]
|
||||
extensions = {"abc"}
|
||||
|
||||
label = "Load alembic mesh"
|
||||
order = -10
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ class FusionLoadFBXMesh(load.LoaderPlugin):
|
|||
"""Load FBX mesh into Fusion"""
|
||||
|
||||
families = ["*"]
|
||||
representations = ["fbx"]
|
||||
representations = ["*"]
|
||||
extensions = {"fbx"}
|
||||
|
||||
label = "Load FBX mesh"
|
||||
order = -10
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
import os
|
||||
import contextlib
|
||||
|
||||
from openpype.client import get_version_by_id
|
||||
from openpype.pipeline import (
|
||||
load,
|
||||
legacy_io,
|
||||
get_representation_path,
|
||||
import openpype.pipeline.load as load
|
||||
from openpype.pipeline.load import (
|
||||
get_representation_context,
|
||||
get_representation_path_from_context
|
||||
)
|
||||
from openpype.hosts.fusion.api import (
|
||||
imprint_container,
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk
|
||||
)
|
||||
from openpype.lib.transcoding import (
|
||||
IMAGE_EXTENSIONS,
|
||||
VIDEO_EXTENSIONS
|
||||
)
|
||||
|
||||
comp = get_current_comp()
|
||||
|
||||
|
|
@ -129,6 +131,9 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
|
||||
families = ["imagesequence", "review", "render", "plate"]
|
||||
representations = ["*"]
|
||||
extensions = set(
|
||||
ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)
|
||||
)
|
||||
|
||||
label = "Load sequence"
|
||||
order = -10
|
||||
|
|
@ -141,7 +146,7 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
namespace = context['asset']['name']
|
||||
|
||||
# Use the first file for now
|
||||
path = self._get_first_image(os.path.dirname(self.fname))
|
||||
path = get_representation_path_from_context(context)
|
||||
|
||||
# Create the Loader with the filename path set
|
||||
comp = get_current_comp()
|
||||
|
|
@ -210,13 +215,11 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
assert tool.ID == "Loader", "Must be Loader"
|
||||
comp = tool.Comp()
|
||||
|
||||
root = os.path.dirname(get_representation_path(representation))
|
||||
path = self._get_first_image(root)
|
||||
context = get_representation_context(representation)
|
||||
path = get_representation_path_from_context(context)
|
||||
|
||||
# Get start frame from version data
|
||||
project_name = legacy_io.active_project()
|
||||
version = get_version_by_id(project_name, representation["parent"])
|
||||
start = self._get_start(version, tool)
|
||||
start = self._get_start(context["version"], tool)
|
||||
|
||||
with comp_lock_and_undo_chunk(comp, "Update Loader"):
|
||||
|
||||
|
|
@ -249,11 +252,6 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
with comp_lock_and_undo_chunk(comp, "Remove Loader"):
|
||||
tool.Delete()
|
||||
|
||||
def _get_first_image(self, root):
|
||||
"""Get first file in representation root"""
|
||||
files = sorted(os.listdir(root))
|
||||
return os.path.join(root, files[0])
|
||||
|
||||
def _get_start(self, version_doc, tool):
|
||||
"""Return real start frame of published files (incl. handles)"""
|
||||
data = version_doc["data"]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.hosts.fusion.api import get_current_comp
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import pyblish.api
|
||||
|
||||
|
||||
def get_comp_render_range(comp):
|
||||
"""Return comp's start-end render range and global start-end range."""
|
||||
comp_attrs = comp.GetAttrs()
|
||||
start = comp_attrs["COMPN_RenderStart"]
|
||||
end = comp_attrs["COMPN_RenderEnd"]
|
||||
global_start = comp_attrs["COMPN_GlobalStart"]
|
||||
global_end = comp_attrs["COMPN_GlobalEnd"]
|
||||
|
||||
# Whenever render ranges are undefined fall back
|
||||
# to the comp's global start and end
|
||||
if start == -1000000000:
|
||||
start = global_start
|
||||
if end == -1000000000:
|
||||
end = global_end
|
||||
|
||||
return start, end, global_start, global_end
|
||||
|
||||
|
||||
class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin):
|
||||
"""Collect current comp"""
|
||||
|
||||
# We run this after CollectorOrder - 0.1 otherwise it gets
|
||||
# overridden by global plug-in `CollectContextEntities`
|
||||
order = pyblish.api.CollectorOrder - 0.05
|
||||
label = "Collect Comp Frame Ranges"
|
||||
hosts = ["fusion"]
|
||||
|
||||
def process(self, context):
|
||||
"""Collect all image sequence tools"""
|
||||
|
||||
comp = context.data["currentComp"]
|
||||
|
||||
# Store comp render ranges
|
||||
start, end, global_start, global_end = get_comp_render_range(comp)
|
||||
context.data["frameStart"] = int(start)
|
||||
context.data["frameEnd"] = int(end)
|
||||
context.data["frameStartHandle"] = int(global_start)
|
||||
context.data["frameEndHandle"] = int(global_end)
|
||||
context.data["handleStart"] = int(start) - int(global_start)
|
||||
context.data["handleEnd"] = int(global_end) - int(end)
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
from bson.objectid import ObjectId
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import registered_host
|
||||
|
|
@ -97,10 +95,15 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin):
|
|||
label = "Collect Inputs"
|
||||
order = pyblish.api.CollectorOrder + 0.2
|
||||
hosts = ["fusion"]
|
||||
families = ["render"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
# Get all upstream and include itself
|
||||
if not any(instance[:]):
|
||||
self.log.debug("No tool found in instance, skipping..")
|
||||
return
|
||||
|
||||
tool = instance[0]
|
||||
nodes = list(iter_upstream(tool))
|
||||
nodes.append(tool)
|
||||
|
|
@ -108,7 +111,6 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin):
|
|||
# Collect containers for the given set of nodes
|
||||
containers = collect_input_containers(nodes)
|
||||
|
||||
inputs = [ObjectId(c["representation"]) for c in containers]
|
||||
inputs = [c["representation"] for c in containers]
|
||||
instance.data["inputRepresentations"] = inputs
|
||||
|
||||
self.log.info("Collected inputs: %s" % inputs)
|
||||
|
|
|
|||
|
|
@ -3,25 +3,7 @@ import os
|
|||
import pyblish.api
|
||||
|
||||
|
||||
def get_comp_render_range(comp):
|
||||
"""Return comp's start-end render range and global start-end range."""
|
||||
comp_attrs = comp.GetAttrs()
|
||||
start = comp_attrs["COMPN_RenderStart"]
|
||||
end = comp_attrs["COMPN_RenderEnd"]
|
||||
global_start = comp_attrs["COMPN_GlobalStart"]
|
||||
global_end = comp_attrs["COMPN_GlobalEnd"]
|
||||
|
||||
# Whenever render ranges are undefined fall back
|
||||
# to the comp's global start and end
|
||||
if start == -1000000000:
|
||||
start = global_start
|
||||
if end == -1000000000:
|
||||
end = global_end
|
||||
|
||||
return start, end, global_start, global_end
|
||||
|
||||
|
||||
class CollectInstances(pyblish.api.ContextPlugin):
|
||||
class CollectInstanceData(pyblish.api.InstancePlugin):
|
||||
"""Collect Fusion saver instances
|
||||
|
||||
This additionally stores the Comp start and end render range in the
|
||||
|
|
@ -30,76 +12,68 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder
|
||||
label = "Collect Instances"
|
||||
label = "Collect Instances Data"
|
||||
hosts = ["fusion"]
|
||||
|
||||
def process(self, context):
|
||||
def process(self, instance):
|
||||
"""Collect all image sequence tools"""
|
||||
|
||||
from openpype.hosts.fusion.api.lib import get_frame_path
|
||||
context = instance.context
|
||||
|
||||
comp = context.data["currentComp"]
|
||||
# Include creator attributes directly as instance data
|
||||
creator_attributes = instance.data["creator_attributes"]
|
||||
instance.data.update(creator_attributes)
|
||||
|
||||
# Get all savers in the comp
|
||||
tools = comp.GetToolList(False).values()
|
||||
savers = [tool for tool in tools if tool.ID == "Saver"]
|
||||
# Include start and end render frame in label
|
||||
subset = instance.data["subset"]
|
||||
start = context.data["frameStart"]
|
||||
end = context.data["frameEnd"]
|
||||
label = "{subset} ({start}-{end})".format(subset=subset,
|
||||
start=int(start),
|
||||
end=int(end))
|
||||
instance.data.update({
|
||||
"label": label,
|
||||
|
||||
start, end, global_start, global_end = get_comp_render_range(comp)
|
||||
context.data["frameStart"] = int(start)
|
||||
context.data["frameEnd"] = int(end)
|
||||
context.data["frameStartHandle"] = int(global_start)
|
||||
context.data["frameEndHandle"] = int(global_end)
|
||||
# todo: Allow custom frame range per instance
|
||||
"frameStart": context.data["frameStart"],
|
||||
"frameEnd": context.data["frameEnd"],
|
||||
"frameStartHandle": context.data["frameStartHandle"],
|
||||
"frameEndHandle": context.data["frameStartHandle"],
|
||||
"handleStart": context.data["handleStart"],
|
||||
"handleEnd": context.data["handleEnd"],
|
||||
"fps": context.data["fps"],
|
||||
})
|
||||
|
||||
for tool in savers:
|
||||
# Add review family if the instance is marked as 'review'
|
||||
# This could be done through a 'review' Creator attribute.
|
||||
if instance.data.get("review", False):
|
||||
self.log.info("Adding review family..")
|
||||
instance.data["families"].append("review")
|
||||
|
||||
if instance.data["family"] == "render":
|
||||
# TODO: This should probably move into a collector of
|
||||
# its own for the "render" family
|
||||
from openpype.hosts.fusion.api.lib import get_frame_path
|
||||
comp = context.data["currentComp"]
|
||||
|
||||
# This is only the case for savers currently but not
|
||||
# for workfile instances. So we assume saver here.
|
||||
tool = instance.data["transientData"]["tool"]
|
||||
path = tool["Clip"][comp.TIME_UNDEFINED]
|
||||
|
||||
tool_attrs = tool.GetAttrs()
|
||||
active = not tool_attrs["TOOLB_PassThrough"]
|
||||
|
||||
if not path:
|
||||
self.log.warning("Skipping saver because it "
|
||||
"has no path set: {}".format(tool.Name))
|
||||
continue
|
||||
|
||||
filename = os.path.basename(path)
|
||||
head, padding, tail = get_frame_path(filename)
|
||||
ext = os.path.splitext(path)[1]
|
||||
assert tail == ext, ("Tail does not match %s" % ext)
|
||||
subset = head.rstrip("_. ") # subset is head of the filename
|
||||
|
||||
# Include start and end render frame in label
|
||||
label = "{subset} ({start}-{end})".format(subset=subset,
|
||||
start=int(start),
|
||||
end=int(end))
|
||||
|
||||
instance = context.create_instance(subset)
|
||||
instance.data.update({
|
||||
"asset": os.environ["AVALON_ASSET"], # todo: not a constant
|
||||
"subset": subset,
|
||||
"path": path,
|
||||
"outputDir": os.path.dirname(path),
|
||||
"ext": ext, # todo: should be redundant
|
||||
"label": label,
|
||||
"frameStart": context.data["frameStart"],
|
||||
"frameEnd": context.data["frameEnd"],
|
||||
"frameStartHandle": context.data["frameStartHandle"],
|
||||
"frameEndHandle": context.data["frameStartHandle"],
|
||||
"fps": context.data["fps"],
|
||||
"families": ["render", "review"],
|
||||
"family": "render",
|
||||
"active": active,
|
||||
"publish": active # backwards compatibility
|
||||
"ext": ext, # todo: should be redundant?
|
||||
|
||||
# Backwards compatibility: embed tool in instance.data
|
||||
"tool": tool
|
||||
})
|
||||
|
||||
# Add tool itself as member
|
||||
instance.append(tool)
|
||||
|
||||
self.log.info("Found: \"%s\" " % path)
|
||||
|
||||
# Sort/grouped by family (preserving local index)
|
||||
context[:] = sorted(context, key=self.sort_by_family)
|
||||
|
||||
return context
|
||||
|
||||
def sort_by_family(self, instance):
|
||||
"""Sort by family"""
|
||||
return instance.data.get("families", instance.data.get("family"))
|
||||
|
|
|
|||
26
openpype/hosts/fusion/plugins/publish/collect_workfile.py
Normal file
26
openpype/hosts/fusion/plugins/publish/collect_workfile.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectFusionWorkfile(pyblish.api.InstancePlugin):
|
||||
"""Collect Fusion workfile representation."""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.1
|
||||
label = "Collect Workfile"
|
||||
hosts = ["fusion"]
|
||||
families = ["workfile"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
current_file = instance.context.data["currentFile"]
|
||||
|
||||
folder, file = os.path.split(current_file)
|
||||
filename, ext = os.path.splitext(file)
|
||||
|
||||
instance.data['representations'] = [{
|
||||
'name': ext.lstrip("."),
|
||||
'ext': ext.lstrip("."),
|
||||
'files': file,
|
||||
"stagingDir": folder,
|
||||
}]
|
||||
|
|
@ -11,7 +11,7 @@ class FusionIncrementCurrentFile(pyblish.api.ContextPlugin):
|
|||
label = "Increment current file"
|
||||
order = pyblish.api.IntegratorOrder + 9.0
|
||||
hosts = ["fusion"]
|
||||
families = ["render.farm"]
|
||||
families = ["workfile"]
|
||||
optional = True
|
||||
|
||||
def process(self, context):
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import os
|
||||
from pprint import pformat
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.fusion.api import comp_lock_and_undo_chunk
|
||||
|
||||
|
||||
class Fusionlocal(pyblish.api.InstancePlugin):
|
||||
class Fusionlocal(pyblish.api.InstancePlugin,
|
||||
publish.ColormanagedPyblishPluginMixin):
|
||||
"""Render the current Fusion composition locally.
|
||||
|
||||
Extract the result of savers by starting a comp render
|
||||
|
|
@ -19,55 +19,82 @@ class Fusionlocal(pyblish.api.InstancePlugin):
|
|||
families = ["render.local"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
# This plug-in runs only once and thus assumes all instances
|
||||
# currently will render the same frame range
|
||||
context = instance.context
|
||||
key = "__hasRun{}".format(self.__class__.__name__)
|
||||
if context.data.get(key, False):
|
||||
return
|
||||
else:
|
||||
context.data[key] = True
|
||||
|
||||
current_comp = context.data["currentComp"]
|
||||
# Start render
|
||||
self.render_once(context)
|
||||
|
||||
# Log render status
|
||||
self.log.info(
|
||||
"Rendered '{nm}' for asset '{ast}' under the task '{tsk}'".format(
|
||||
nm=instance.data["name"],
|
||||
ast=instance.data["asset"],
|
||||
tsk=instance.data["task"],
|
||||
)
|
||||
)
|
||||
|
||||
frame_start = context.data["frameStartHandle"]
|
||||
frame_end = context.data["frameEndHandle"]
|
||||
path = instance.data["path"]
|
||||
output_dir = instance.data["outputDir"]
|
||||
|
||||
ext = os.path.splitext(os.path.basename(path))[-1]
|
||||
basename = os.path.basename(path)
|
||||
head, ext = os.path.splitext(basename)
|
||||
files = [
|
||||
f"{head}{str(frame).zfill(4)}{ext}"
|
||||
for frame in range(frame_start, frame_end + 1)
|
||||
]
|
||||
repre = {
|
||||
"name": ext[1:],
|
||||
"ext": ext[1:],
|
||||
"frameStart": f"%0{len(str(frame_end))}d" % frame_start,
|
||||
"files": files,
|
||||
"stagingDir": output_dir,
|
||||
}
|
||||
|
||||
self.log.info("Starting render")
|
||||
self.log.info("Start frame: {}".format(frame_start))
|
||||
self.log.info("End frame: {}".format(frame_end))
|
||||
|
||||
with comp_lock_and_undo_chunk(current_comp):
|
||||
result = current_comp.Render({
|
||||
"Start": frame_start,
|
||||
"End": frame_end,
|
||||
"Wait": True
|
||||
})
|
||||
self.set_representation_colorspace(
|
||||
representation=repre,
|
||||
context=context,
|
||||
)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
collected_frames = os.listdir(output_dir)
|
||||
repre = {
|
||||
'name': ext[1:],
|
||||
'ext': ext[1:],
|
||||
'frameStart': "%0{}d".format(len(str(frame_end))) % frame_start,
|
||||
'files': collected_frames,
|
||||
"stagingDir": output_dir,
|
||||
}
|
||||
instance.data["representations"].append(repre)
|
||||
|
||||
# review representation
|
||||
repre_preview = repre.copy()
|
||||
repre_preview["name"] = repre_preview["ext"] = "mp4"
|
||||
repre_preview["tags"] = ["review", "preview", "ftrackreview", "delete"]
|
||||
instance.data["representations"].append(repre_preview)
|
||||
if instance.data.get("review", False):
|
||||
repre["tags"] = ["review", "ftrackreview"]
|
||||
|
||||
self.log.debug(f"_ instance.data: {pformat(instance.data)}")
|
||||
def render_once(self, context):
|
||||
"""Render context comp only once, even with more render instances"""
|
||||
|
||||
if not result:
|
||||
# This plug-in assumes all render nodes get rendered at the same time
|
||||
# to speed up the rendering. The check below makes sure that we only
|
||||
# execute the rendering once and not for each instance.
|
||||
key = f"__hasRun{self.__class__.__name__}"
|
||||
if key not in context.data:
|
||||
# We initialize as false to indicate it wasn't successful yet
|
||||
# so we can keep track of whether Fusion succeeded
|
||||
context.data[key] = False
|
||||
|
||||
current_comp = context.data["currentComp"]
|
||||
frame_start = context.data["frameStartHandle"]
|
||||
frame_end = context.data["frameEndHandle"]
|
||||
|
||||
self.log.info("Starting Fusion render")
|
||||
self.log.info(f"Start frame: {frame_start}")
|
||||
self.log.info(f"End frame: {frame_end}")
|
||||
|
||||
with comp_lock_and_undo_chunk(current_comp):
|
||||
result = current_comp.Render(
|
||||
{
|
||||
"Start": frame_start,
|
||||
"End": frame_end,
|
||||
"Wait": True,
|
||||
}
|
||||
)
|
||||
|
||||
context.data[key] = bool(result)
|
||||
|
||||
if context.data[key] is False:
|
||||
raise RuntimeError("Comp render failed")
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ class FusionSaveComp(pyblish.api.ContextPlugin):
|
|||
label = "Save current file"
|
||||
order = pyblish.api.ExtractorOrder - 0.49
|
||||
hosts = ["fusion"]
|
||||
families = ["render"]
|
||||
families = ["render", "workfile"]
|
||||
|
||||
def process(self, context):
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import RepairAction
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
from openpype.hosts.fusion.api.action import SelectInvalidAction
|
||||
|
||||
|
||||
class ValidateBackgroundDepth(pyblish.api.InstancePlugin):
|
||||
|
|
@ -8,11 +11,12 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin):
|
|||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Background Depth 32 bit"
|
||||
actions = [RepairAction]
|
||||
hosts = ["fusion"]
|
||||
families = ["render"]
|
||||
optional = True
|
||||
|
||||
actions = [SelectInvalidAction, RepairAction]
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
|
||||
|
|
@ -29,8 +33,10 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin):
|
|||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError("Found %i nodes which are not set to float32"
|
||||
% len(invalid))
|
||||
raise PublishValidationError(
|
||||
"Found {} Backgrounds tools which"
|
||||
" are not set to float32".format(len(invalid)),
|
||||
title=self.label)
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
|
||||
class ValidateFusionCompSaved(pyblish.api.ContextPlugin):
|
||||
|
|
@ -19,10 +20,12 @@ class ValidateFusionCompSaved(pyblish.api.ContextPlugin):
|
|||
|
||||
filename = attrs["COMPS_FileName"]
|
||||
if not filename:
|
||||
raise RuntimeError("Comp is not saved.")
|
||||
raise PublishValidationError("Comp is not saved.",
|
||||
title=self.label)
|
||||
|
||||
if not os.path.exists(filename):
|
||||
raise RuntimeError("Comp file does not exist: %s" % filename)
|
||||
raise PublishValidationError(
|
||||
"Comp file does not exist: %s" % filename, title=self.label)
|
||||
|
||||
if attrs["COMPB_Modified"]:
|
||||
self.log.warning("Comp is modified. Save your comp to ensure your "
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import RepairAction
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
from openpype.hosts.fusion.api.action import SelectInvalidAction
|
||||
|
||||
|
||||
class ValidateCreateFolderChecked(pyblish.api.InstancePlugin):
|
||||
|
|
@ -15,6 +18,7 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin):
|
|||
label = "Validate Create Folder Checked"
|
||||
families = ["render"]
|
||||
hosts = ["fusion"]
|
||||
actions = [SelectInvalidAction]
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
|
|
@ -31,8 +35,9 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin):
|
|||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError("Found Saver with Create Folder During "
|
||||
"Render checked off")
|
||||
raise PublishValidationError(
|
||||
"Found Saver with Create Folder During Render checked off",
|
||||
title=self.label)
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
from openpype.hosts.fusion.api.action import SelectInvalidAction
|
||||
|
||||
|
||||
class ValidateFilenameHasExtension(pyblish.api.InstancePlugin):
|
||||
|
|
@ -16,11 +19,13 @@ class ValidateFilenameHasExtension(pyblish.api.InstancePlugin):
|
|||
label = "Validate Filename Has Extension"
|
||||
families = ["render"]
|
||||
hosts = ["fusion"]
|
||||
actions = [SelectInvalidAction]
|
||||
|
||||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError("Found Saver without an extension")
|
||||
raise PublishValidationError("Found Saver without an extension",
|
||||
title=self.label)
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
from openpype.hosts.fusion.api.action import SelectInvalidAction
|
||||
|
||||
|
||||
class ValidateSaverHasInput(pyblish.api.InstancePlugin):
|
||||
|
|
@ -12,6 +15,7 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin):
|
|||
label = "Validate Saver Has Input"
|
||||
families = ["render"]
|
||||
hosts = ["fusion"]
|
||||
actions = [SelectInvalidAction]
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
|
|
@ -25,5 +29,8 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin):
|
|||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError("Saver has no incoming connection: "
|
||||
"{} ({})".format(instance, invalid[0].Name))
|
||||
saver_name = invalid[0].Name
|
||||
raise PublishValidationError(
|
||||
"Saver has no incoming connection: {} ({})".format(instance,
|
||||
saver_name),
|
||||
title=self.label)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
from openpype.hosts.fusion.api.action import SelectInvalidAction
|
||||
|
||||
|
||||
class ValidateSaverPassthrough(pyblish.api.ContextPlugin):
|
||||
|
|
@ -8,6 +11,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin):
|
|||
label = "Validate Saver Passthrough"
|
||||
families = ["render"]
|
||||
hosts = ["fusion"]
|
||||
actions = [SelectInvalidAction]
|
||||
|
||||
def process(self, context):
|
||||
|
||||
|
|
@ -27,8 +31,9 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin):
|
|||
if invalid_instances:
|
||||
self.log.info("Reset pyblish to collect your current scene state, "
|
||||
"that should fix error.")
|
||||
raise RuntimeError("Invalid instances: "
|
||||
"{0}".format(invalid_instances))
|
||||
raise PublishValidationError(
|
||||
"Invalid instances: {0}".format(invalid_instances),
|
||||
title=self.label)
|
||||
|
||||
def is_invalid(self, instance):
|
||||
|
||||
|
|
@ -36,7 +41,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin):
|
|||
attr = saver.GetAttrs()
|
||||
active = not attr["TOOLB_PassThrough"]
|
||||
|
||||
if active != instance.data["publish"]:
|
||||
if active != instance.data.get("publish", True):
|
||||
self.log.info("Saver has different passthrough state than "
|
||||
"Pyblish: {} ({})".format(instance, saver.Name))
|
||||
return [saver]
|
||||
|
|
|
|||
|
|
@ -1,29 +1,55 @@
|
|||
from collections import defaultdict
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
from openpype.hosts.fusion.api.action import SelectInvalidAction
|
||||
|
||||
|
||||
class ValidateUniqueSubsets(pyblish.api.InstancePlugin):
|
||||
class ValidateUniqueSubsets(pyblish.api.ContextPlugin):
|
||||
"""Ensure all instances have a unique subset name"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Unique Subsets"
|
||||
families = ["render"]
|
||||
hosts = ["fusion"]
|
||||
actions = [SelectInvalidAction]
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
def get_invalid(cls, context):
|
||||
|
||||
context = instance.context
|
||||
subset = instance.data["subset"]
|
||||
for other_instance in context:
|
||||
if other_instance == instance:
|
||||
continue
|
||||
# Collect instances per subset per asset
|
||||
instances_per_subset_asset = defaultdict(lambda: defaultdict(list))
|
||||
for instance in context:
|
||||
asset = instance.data.get("asset", context.data.get("asset"))
|
||||
subset = instance.data.get("subset", context.data.get("subset"))
|
||||
instances_per_subset_asset[asset][subset].append(instance)
|
||||
|
||||
if other_instance.data["subset"] == subset:
|
||||
return [instance] # current instance is invalid
|
||||
# Find which asset + subset combination has more than one instance
|
||||
# Those are considered invalid because they'd integrate to the same
|
||||
# destination.
|
||||
invalid = []
|
||||
for asset, instances_per_subset in instances_per_subset_asset.items():
|
||||
for subset, instances in instances_per_subset.items():
|
||||
if len(instances) > 1:
|
||||
cls.log.warning(
|
||||
"{asset} > {subset} used by more than "
|
||||
"one instance: {instances}".format(
|
||||
asset=asset,
|
||||
subset=subset,
|
||||
instances=instances
|
||||
)
|
||||
)
|
||||
invalid.extend(instances)
|
||||
|
||||
return []
|
||||
# Return tools for the invalid instances so they can be selected
|
||||
invalid = [instance.data["tool"] for instance in invalid]
|
||||
|
||||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
return invalid
|
||||
|
||||
def process(self, context):
|
||||
invalid = self.get_invalid(context)
|
||||
if invalid:
|
||||
raise RuntimeError("Animation content is invalid. See log.")
|
||||
raise PublishValidationError("Multiple instances are set to "
|
||||
"the same asset > subset.",
|
||||
title=self.label)
|
||||
|
|
|
|||
|
|
@ -126,10 +126,6 @@ def check_inventory():
|
|||
|
||||
def application_launch(event):
|
||||
"""Event that is executed after Harmony is launched."""
|
||||
# FIXME: This is breaking server <-> client communication.
|
||||
# It is now moved so it it manually called.
|
||||
# ensure_scene_settings()
|
||||
# check_inventory()
|
||||
# fills OPENPYPE_HARMONY_JS
|
||||
pype_harmony_path = Path(__file__).parent.parent / "js" / "PypeHarmony.js"
|
||||
pype_harmony_js = pype_harmony_path.read_text()
|
||||
|
|
@ -146,6 +142,9 @@ def application_launch(event):
|
|||
harmony.send({"script": script})
|
||||
inject_avalon_js()
|
||||
|
||||
ensure_scene_settings()
|
||||
check_inventory()
|
||||
|
||||
|
||||
def export_template(backdrops, nodes, filepath):
|
||||
"""Export Template to file.
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ class Server(threading.Thread):
|
|||
|
||||
# Create a TCP/IP socket
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
# Bind the socket to the port
|
||||
server_address = ("127.0.0.1", port)
|
||||
|
|
@ -91,7 +92,13 @@ class Server(threading.Thread):
|
|||
self.log.info("wait ttt")
|
||||
# Receive the data in small chunks and retransmit it
|
||||
request = None
|
||||
header = self.connection.recv(10)
|
||||
try:
|
||||
header = self.connection.recv(10)
|
||||
except OSError:
|
||||
# could happen on MacOS
|
||||
self.log.info("")
|
||||
break
|
||||
|
||||
if len(header) == 0:
|
||||
# null data received, socket is closing.
|
||||
self.log.info(f"[{self.timestamp()}] Connection closing.")
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ class ImageSequenceLoader(load.LoaderPlugin):
|
|||
Stores the imported asset in a container named after the asset.
|
||||
"""
|
||||
|
||||
families = ["shot", "render", "image", "plate", "reference"]
|
||||
representations = ["jpeg", "png", "jpg"]
|
||||
families = ["shot", "render", "image", "plate", "reference", "review"]
|
||||
representations = ["*"]
|
||||
extensions = {"jpeg", "png", "jpg"}
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
"""Plugin entry point.
|
||||
|
|
|
|||
|
|
@ -108,9 +108,9 @@ class ExtractRender(pyblish.api.InstancePlugin):
|
|||
output = process.communicate()[0]
|
||||
|
||||
if process.returncode != 0:
|
||||
raise ValueError(output.decode("utf-8"))
|
||||
raise ValueError(output.decode("utf-8", errors="backslashreplace"))
|
||||
|
||||
self.log.debug(output.decode("utf-8"))
|
||||
self.log.debug(output.decode("utf-8", errors="backslashreplace"))
|
||||
|
||||
# Generate representations.
|
||||
extension = collection.tail[1:]
|
||||
|
|
|
|||
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