mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into instance_collect_no_asset_framerange
This commit is contained in:
commit
25bb76226b
100 changed files with 1971 additions and 757 deletions
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -1,33 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
**Running version**
|
||||
[ex. 3.14.1-nightly.2]
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. windows]
|
||||
- Host: [e.g. Maya, Nuke, Houdini]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
183
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
183
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
name: Bug Report
|
||||
description: File a bug report
|
||||
title: 'Bug: '
|
||||
labels:
|
||||
- 'type: bug'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: >-
|
||||
Please search to see if an issue already exists for the bug you
|
||||
encountered.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 'Current Behavior:'
|
||||
description: A concise description of what you're experiencing.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 'Expected Behavior:'
|
||||
description: A concise description of what you expected to happen.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: _version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version are you running? Look to OpenPype Tray
|
||||
options:
|
||||
- 3.15.4-nightly.3
|
||||
- 3.15.4-nightly.2
|
||||
- 3.15.4-nightly.1
|
||||
- 3.15.3
|
||||
- 3.15.3-nightly.4
|
||||
- 3.15.3-nightly.3
|
||||
- 3.15.3-nightly.2
|
||||
- 3.15.3-nightly.1
|
||||
- 3.15.2
|
||||
- 3.15.2-nightly.6
|
||||
- 3.15.2-nightly.5
|
||||
- 3.15.2-nightly.4
|
||||
- 3.15.2-nightly.3
|
||||
- 3.15.2-nightly.2
|
||||
- 3.15.2-nightly.1
|
||||
- 3.15.1
|
||||
- 3.15.1-nightly.6
|
||||
- 3.15.1-nightly.5
|
||||
- 3.15.1-nightly.4
|
||||
- 3.15.1-nightly.3
|
||||
- 3.15.1-nightly.2
|
||||
- 3.15.1-nightly.1
|
||||
- 3.15.0
|
||||
- 3.15.0-nightly.1
|
||||
- 3.14.11-nightly.4
|
||||
- 3.14.11-nightly.3
|
||||
- 3.14.11-nightly.2
|
||||
- 3.14.11-nightly.1
|
||||
- 3.14.10
|
||||
- 3.14.10-nightly.9
|
||||
- 3.14.10-nightly.8
|
||||
- 3.14.10-nightly.7
|
||||
- 3.14.10-nightly.6
|
||||
- 3.14.10-nightly.5
|
||||
- 3.14.10-nightly.4
|
||||
- 3.14.10-nightly.3
|
||||
- 3.14.10-nightly.2
|
||||
- 3.14.10-nightly.1
|
||||
- 3.14.9
|
||||
- 3.14.9-nightly.5
|
||||
- 3.14.9-nightly.4
|
||||
- 3.14.9-nightly.3
|
||||
- 3.14.9-nightly.2
|
||||
- 3.14.9-nightly.1
|
||||
- 3.14.8
|
||||
- 3.14.8-nightly.4
|
||||
- 3.14.8-nightly.3
|
||||
- 3.14.8-nightly.2
|
||||
- 3.14.8-nightly.1
|
||||
- 3.14.7
|
||||
- 3.14.7-nightly.8
|
||||
- 3.14.7-nightly.7
|
||||
- 3.14.7-nightly.6
|
||||
- 3.14.7-nightly.5
|
||||
- 3.14.7-nightly.4
|
||||
- 3.14.7-nightly.3
|
||||
- 3.14.7-nightly.2
|
||||
- 3.14.7-nightly.1
|
||||
- 3.14.6
|
||||
- 3.14.6-nightly.3
|
||||
- 3.14.6-nightly.2
|
||||
- 3.14.6-nightly.1
|
||||
- 3.14.5
|
||||
- 3.14.5-nightly.3
|
||||
- 3.14.5-nightly.2
|
||||
- 3.14.5-nightly.1
|
||||
- 3.14.4
|
||||
- 3.14.4-nightly.4
|
||||
- 3.14.4-nightly.3
|
||||
- 3.14.4-nightly.2
|
||||
- 3.14.4-nightly.1
|
||||
- 3.14.3
|
||||
- 3.14.3-nightly.7
|
||||
- 3.14.3-nightly.6
|
||||
- 3.14.3-nightly.5
|
||||
- 3.14.3-nightly.4
|
||||
- 3.14.3-nightly.3
|
||||
- 3.14.3-nightly.2
|
||||
- 3.14.3-nightly.1
|
||||
- 3.14.2
|
||||
- 3.14.2-nightly.5
|
||||
- 3.14.2-nightly.4
|
||||
- 3.14.2-nightly.3
|
||||
- 3.14.2-nightly.2
|
||||
- 3.14.2-nightly.1
|
||||
- 3.14.1
|
||||
- 3.14.1-nightly.4
|
||||
- 3.14.1-nightly.3
|
||||
- 3.14.1-nightly.2
|
||||
- 3.14.1-nightly.1
|
||||
- 3.14.0
|
||||
- 3.14.0-nightly.1
|
||||
- 3.13.1-nightly.3
|
||||
- 3.13.1-nightly.2
|
||||
- 3.13.1-nightly.1
|
||||
- 3.13.0
|
||||
- 3.13.0-nightly.1
|
||||
- 3.12.3-nightly.3
|
||||
- 3.12.3-nightly.2
|
||||
- 3.12.3-nightly.1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: What platform you are running OpenPype on?
|
||||
description: |
|
||||
Please specify the operating systems you are running OpenPype with.
|
||||
multiple: true
|
||||
options:
|
||||
- Windows
|
||||
- Linux / Centos
|
||||
- Linux / Ubuntu
|
||||
- Linux / RedHat
|
||||
- MacOS
|
||||
- type: textarea
|
||||
id: to-reproduce
|
||||
attributes:
|
||||
label: 'Steps To Reproduce:'
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. How did the configuration look like
|
||||
2. What type of action was made
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Are there any labels you wish to add?
|
||||
description: Please search labels and identify those related to your bug.
|
||||
options:
|
||||
- label: I have added the relevant labels to the bug report.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: 'Relevant log output:'
|
||||
description: >-
|
||||
Please copy and paste any relevant log output. This will be
|
||||
automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: 'Additional context:'
|
||||
description: Add any other context about the problem here.
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Ynput Discord Server
|
||||
url: https://discord.gg/ynput
|
||||
about: For community quick chats.
|
||||
52
.github/ISSUE_TEMPLATE/enhancement_request.yml
vendored
Normal file
52
.github/ISSUE_TEMPLATE/enhancement_request.yml
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
name: Enhancement Request
|
||||
description: Create a report to help us enhance a particular feature
|
||||
title: "Enhancement: "
|
||||
labels:
|
||||
- "type: enhancement"
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this enhancement request report!
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
options:
|
||||
- label: I have searched the existing issues.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: related-feature
|
||||
attributes:
|
||||
label: Please describe the feature you have in mind and explain what the current shortcomings are?
|
||||
description: A clear and concise description of what the problem is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: enhancement-proposal
|
||||
attributes:
|
||||
label: How would you imagine the implementation of the feature?
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Are there any labels you wish to add?
|
||||
description: Please search labels and identify those related to your enhancement.
|
||||
options:
|
||||
- label: I have added the relevant labels to the enhancement request.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: "Describe alternatives you've considered:"
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: "Additional context:"
|
||||
description: Add any other context or screenshots about the enhancement request here.
|
||||
validations:
|
||||
required: false
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
2
.github/pr-branch-labeler.yml
vendored
2
.github/pr-branch-labeler.yml
vendored
|
|
@ -12,4 +12,4 @@
|
|||
|
||||
# Apply label "release" if base matches "release/*"
|
||||
'Bump Minor':
|
||||
base: "release/next-minor"
|
||||
base: "release/next-minor"
|
||||
|
|
|
|||
2
.github/workflows/documentation.yml
vendored
2
.github/workflows/documentation.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: documentation
|
||||
name: 📜 Documentation
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
|
|
|||
2
.github/workflows/milestone_assign.yml
vendored
2
.github/workflows/milestone_assign.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: Milestone - assign to PRs
|
||||
name: 👉🏻 Milestone - assign to PRs
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
|
|
|
|||
2
.github/workflows/milestone_create.yml
vendored
2
.github/workflows/milestone_create.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: Milestone - create default
|
||||
name: ➕ Milestone - create default
|
||||
|
||||
on:
|
||||
milestone:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
name: Milestone Release [trigger]
|
||||
name: 🚩 Milestone Release [trigger]
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
|
|
|||
2
.github/workflows/nightly_merge.yml
vendored
2
.github/workflows/nightly_merge.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: Dev -> Main
|
||||
name: 🔀 Dev -> Main
|
||||
|
||||
on:
|
||||
schedule:
|
||||
|
|
|
|||
49
.github/workflows/pr_labels.yml
vendored
Normal file
49
.github/workflows/pr_labels.yml
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
name: 🔖 PR labels
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, assigned]
|
||||
|
||||
jobs:
|
||||
size-label:
|
||||
name: pr_size_label
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'assigned' || github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Add size label
|
||||
uses: "pascalgn/size-label-action@v0.4.3"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}"
|
||||
IGNORED: ".gitignore\n*.md\n*.json"
|
||||
with:
|
||||
sizes: >
|
||||
{
|
||||
"0": "XS",
|
||||
"100": "S",
|
||||
"500": "M",
|
||||
"1000": "L",
|
||||
"1500": "XL",
|
||||
"2500": "XXL"
|
||||
}
|
||||
|
||||
label_prs_branch:
|
||||
name: pr_branch_label
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'assigned' || github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Label PRs - Branch name detection
|
||||
uses: ffittschen/pr-branch-labeler@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
|
||||
label_prs_globe:
|
||||
name: pr_globe_label
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'assigned' || github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Label PRs - Globe detection
|
||||
uses: actions/labeler@v4.0.3
|
||||
with:
|
||||
repo-token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
configuration-path: ".github/pr-glob-labeler.yml"
|
||||
sync-labels: false
|
||||
2
.github/workflows/prerelease.yml
vendored
2
.github/workflows/prerelease.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: Nightly Prerelease
|
||||
name: ⏳ Nightly Prerelease
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
name: project-actions
|
||||
name: 📊 Project task statuses
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
issue_comment:
|
||||
|
|
@ -20,11 +18,16 @@ jobs:
|
|||
# - PR issue comment which is not form Ynbot
|
||||
# - PR review comment which is not Hound (or any other bot)
|
||||
# - PR review submitted which is not from Hound (or any other bot) and is not 'Changes requested'
|
||||
# - make sure it only runs if not forked repo
|
||||
# -----------------------------
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && github.event.comment.user.id != 82967070) ||
|
||||
(github.event_name == 'pull_request_review_comment' && github.event.comment.user.type != 'Bot') ||
|
||||
(github.event_name == 'pull_request_review' && github.event.review.state != 'changes_requested' && github.event.review.user.type != 'Bot')
|
||||
(github.event_name == 'issue_comment' && github.event.pull_request.head.repo.owner.login == 'ynput' && github.event.comment.user.id != 82967070) ||
|
||||
(github.event_name == 'pull_request_review_comment' && github.event.pull_request.head.repo.owner.login == 'ynput' && github.event.comment.user.type != 'Bot') ||
|
||||
(github.event_name == 'pull_request_review' &&
|
||||
github.event.pull_request.head.repo.owner.login == 'ynput' &&
|
||||
github.event.review.state != 'changes_requested' &&
|
||||
github.event.review.state != 'approved' &&
|
||||
github.event.review.user.type != 'Bot')
|
||||
steps:
|
||||
- name: Move PR to 'Review In Progress'
|
||||
uses: leonsteinhaeuser/project-beta-automations@v2.1.0
|
||||
|
|
@ -42,7 +45,7 @@ jobs:
|
|||
# -----------------------------
|
||||
name: pr_review_requested
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request_review' && github.event.review.state == 'changes_requested'
|
||||
if: github.event_name == 'pull_request_review' && github.event.pull_request.head.repo.owner.login == 'ynput' && github.event.review.state == 'changes_requested'
|
||||
steps:
|
||||
- name: Set branch env
|
||||
run: echo "BRANCH_NAME=${{ github.event.pull_request.head.ref}}" >> $GITHUB_ENV
|
||||
|
|
@ -65,53 +68,3 @@ jobs:
|
|||
-d '{
|
||||
"status": "in progress"
|
||||
}'
|
||||
|
||||
size-label:
|
||||
name: pr_size_label
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && github.event.action == 'assigned') ||
|
||||
(github.event_name == 'pull_request' && github.event.action == 'opened')
|
||||
|
||||
steps:
|
||||
- name: Add size label
|
||||
uses: "pascalgn/size-label-action@v0.4.3"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}"
|
||||
IGNORED: ".gitignore\n*.md\n*.json"
|
||||
with:
|
||||
sizes: >
|
||||
{
|
||||
"0": "XS",
|
||||
"100": "S",
|
||||
"500": "M",
|
||||
"1000": "L",
|
||||
"1500": "XL",
|
||||
"2500": "XXL"
|
||||
}
|
||||
|
||||
label_prs_branch:
|
||||
name: pr_branch_label
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && github.event.action == 'assigned') ||
|
||||
(github.event_name == 'pull_request' && github.event.action == 'opened')
|
||||
steps:
|
||||
- name: Label PRs - Branch name detection
|
||||
uses: ffittschen/pr-branch-labeler@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
|
||||
label_prs_globe:
|
||||
name: pr_globe_label
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && github.event.action == 'assigned') ||
|
||||
(github.event_name == 'pull_request' && github.event.action == 'opened')
|
||||
steps:
|
||||
- name: Label PRs - Globe detection
|
||||
uses: actions/labeler@v4.0.3
|
||||
with:
|
||||
repo-token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
configuration-path: ".github/pr-glob-labeler.yml"
|
||||
sync-labels: false
|
||||
2
.github/workflows/test_build.yml
vendored
2
.github/workflows/test_build.yml
vendored
|
|
@ -1,7 +1,7 @@
|
|||
# This workflow will upload a Python Package using Twine when a release is created
|
||||
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
|
||||
|
||||
name: Test Build
|
||||
name: 🏗️ Test Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
|
|
|||
25
.github/workflows/update_bug_report.yml
vendored
Normal file
25
.github/workflows/update_bug_report.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
name: 🐞 Update Bug Report
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#release
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
update-bug-report:
|
||||
runs-on: ubuntu-latest
|
||||
name: Update bug report
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.release.target_commitish }}
|
||||
- name: Update version
|
||||
uses: ynput/gha-populate-form-version@main
|
||||
with:
|
||||
github_token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
registry: github
|
||||
dropdown: _version
|
||||
limit_to: 100
|
||||
form: .github/ISSUE_TEMPLATE/bug_report.yml
|
||||
commit_message: 'chore(): update bug report / version'
|
||||
|
|
@ -72,8 +72,7 @@ class FusionSetFrameRangeWithHandlesLoader(load.LoaderPlugin):
|
|||
return
|
||||
|
||||
# Include handles
|
||||
handles = version_data.get("handles", 0)
|
||||
start -= handles
|
||||
end += handles
|
||||
start -= version_data.get("handleStart", 0)
|
||||
end += version_data.get("handleEnd", 0)
|
||||
|
||||
lib.update_frame_range(start, end)
|
||||
|
|
|
|||
|
|
@ -242,9 +242,15 @@ def launch_zip_file(filepath):
|
|||
print(f"Localizing {filepath}")
|
||||
|
||||
temp_path = get_local_harmony_path(filepath)
|
||||
scene_name = os.path.basename(temp_path)
|
||||
if os.path.exists(os.path.join(temp_path, scene_name)):
|
||||
# unzipped with duplicated scene_name
|
||||
temp_path = os.path.join(temp_path, scene_name)
|
||||
|
||||
scene_path = os.path.join(
|
||||
temp_path, os.path.basename(temp_path) + ".xstage"
|
||||
temp_path, scene_name + ".xstage"
|
||||
)
|
||||
|
||||
unzip = False
|
||||
if os.path.exists(scene_path):
|
||||
# Check remote scene is newer than local.
|
||||
|
|
@ -262,6 +268,10 @@ def launch_zip_file(filepath):
|
|||
with _ZipFile(filepath, "r") as zip_ref:
|
||||
zip_ref.extractall(temp_path)
|
||||
|
||||
if os.path.exists(os.path.join(temp_path, scene_name)):
|
||||
# unzipped with duplicated scene_name
|
||||
temp_path = os.path.join(temp_path, scene_name)
|
||||
|
||||
# Close existing scene.
|
||||
if ProcessContext.pid:
|
||||
os.kill(ProcessContext.pid, signal.SIGTERM)
|
||||
|
|
@ -309,7 +319,7 @@ def launch_zip_file(filepath):
|
|||
)
|
||||
|
||||
if not os.path.exists(scene_path):
|
||||
print("error: cannot determine scene file")
|
||||
print("error: cannot determine scene file {}".format(scene_path))
|
||||
ProcessContext.server.stop()
|
||||
return
|
||||
|
||||
|
|
|
|||
185
openpype/hosts/houdini/api/creator_node_shelves.py
Normal file
185
openpype/hosts/houdini/api/creator_node_shelves.py
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
"""Library to register OpenPype Creators for Houdini TAB node search menu.
|
||||
|
||||
This can be used to install custom houdini tools for the TAB search
|
||||
menu which will trigger a publish instance to be created interactively.
|
||||
|
||||
The Creators are automatically registered on launch of Houdini through the
|
||||
Houdini integration's `host.install()` method.
|
||||
|
||||
"""
|
||||
import contextlib
|
||||
import tempfile
|
||||
import logging
|
||||
import os
|
||||
|
||||
from openpype.pipeline import registered_host
|
||||
from openpype.pipeline.create import CreateContext
|
||||
from openpype.resources import get_openpype_icon_filepath
|
||||
|
||||
import hou
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
CREATE_SCRIPT = """
|
||||
from openpype.hosts.houdini.api.creator_node_shelves import create_interactive
|
||||
create_interactive("{identifier}")
|
||||
"""
|
||||
|
||||
|
||||
def create_interactive(creator_identifier):
|
||||
"""Create a Creator using its identifier interactively.
|
||||
|
||||
This is used by the generated shelf tools as callback when a user selects
|
||||
the creator from the node tab search menu.
|
||||
|
||||
Args:
|
||||
creator_identifier (str): The creator identifier of the Creator plugin
|
||||
to create.
|
||||
|
||||
Return:
|
||||
list: The created instances.
|
||||
|
||||
"""
|
||||
|
||||
# TODO Use Qt instead
|
||||
result, variant = hou.ui.readInput('Define variant name',
|
||||
buttons=("Ok", "Cancel"),
|
||||
initial_contents='Main',
|
||||
title="Define variant",
|
||||
help="Set the variant for the "
|
||||
"publish instance",
|
||||
close_choice=1)
|
||||
if result == 1:
|
||||
# User interrupted
|
||||
return
|
||||
variant = variant.strip()
|
||||
if not variant:
|
||||
raise RuntimeError("Empty variant value entered.")
|
||||
|
||||
host = registered_host()
|
||||
context = CreateContext(host)
|
||||
|
||||
before = context.instances_by_id.copy()
|
||||
|
||||
# Create the instance
|
||||
context.create(
|
||||
creator_identifier=creator_identifier,
|
||||
variant=variant,
|
||||
pre_create_data={"use_selection": True}
|
||||
)
|
||||
|
||||
# For convenience we set the new node as current since that's much more
|
||||
# familiar to the artist when creating a node interactively
|
||||
# TODO Allow to disable auto-select in studio settings or user preferences
|
||||
after = context.instances_by_id
|
||||
new = set(after) - set(before)
|
||||
if new:
|
||||
# Select the new instance
|
||||
for instance_id in new:
|
||||
instance = after[instance_id]
|
||||
node = hou.node(instance.get("instance_node"))
|
||||
node.setCurrent(True)
|
||||
|
||||
return list(new)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def shelves_change_block():
|
||||
"""Write shelf changes at the end of the context."""
|
||||
hou.shelves.beginChangeBlock()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
hou.shelves.endChangeBlock()
|
||||
|
||||
|
||||
def install():
|
||||
"""Install the Creator plug-ins to show in Houdini's TAB node search menu.
|
||||
|
||||
This function is re-entrant and can be called again to reinstall and
|
||||
update the node definitions. For example during development it can be
|
||||
useful to call it manually:
|
||||
>>> from openpype.hosts.houdini.api.creator_node_shelves import install
|
||||
>>> install()
|
||||
|
||||
Returns:
|
||||
list: List of `hou.Tool` instances
|
||||
|
||||
"""
|
||||
|
||||
host = registered_host()
|
||||
|
||||
# Store the filepath on the host
|
||||
# TODO: Define a less hacky static shelf path for current houdini session
|
||||
filepath_attr = "_creator_node_shelf_filepath"
|
||||
filepath = getattr(host, filepath_attr, None)
|
||||
if filepath is None:
|
||||
f = tempfile.NamedTemporaryFile(prefix="houdini_creator_nodes_",
|
||||
suffix=".shelf",
|
||||
delete=False)
|
||||
f.close()
|
||||
filepath = f.name
|
||||
setattr(host, filepath_attr, filepath)
|
||||
elif os.path.exists(filepath):
|
||||
# Remove any existing shelf file so that we can completey regenerate
|
||||
# and update the tools file if creator identifiers change
|
||||
os.remove(filepath)
|
||||
|
||||
icon = get_openpype_icon_filepath()
|
||||
|
||||
# Create context only to get creator plugins, so we don't reset and only
|
||||
# populate what we need to retrieve the list of creator plugins
|
||||
create_context = CreateContext(host, reset=False)
|
||||
create_context.reset_current_context()
|
||||
create_context._reset_creator_plugins()
|
||||
|
||||
log.debug("Writing OpenPype Creator nodes to shelf: {}".format(filepath))
|
||||
tools = []
|
||||
with shelves_change_block():
|
||||
for identifier, creator in create_context.manual_creators.items():
|
||||
|
||||
# TODO: Allow the creator plug-in itself to override the categories
|
||||
# for where they are shown, by e.g. defining
|
||||
# `Creator.get_network_categories()`
|
||||
|
||||
key = "openpype_create.{}".format(identifier)
|
||||
log.debug(f"Registering {key}")
|
||||
script = CREATE_SCRIPT.format(identifier=identifier)
|
||||
data = {
|
||||
"script": script,
|
||||
"language": hou.scriptLanguage.Python,
|
||||
"icon": icon,
|
||||
"help": "Create OpenPype publish instance for {}".format(
|
||||
creator.label
|
||||
),
|
||||
"help_url": None,
|
||||
"network_categories": [
|
||||
hou.ropNodeTypeCategory(),
|
||||
hou.sopNodeTypeCategory()
|
||||
],
|
||||
"viewer_categories": [],
|
||||
"cop_viewer_categories": [],
|
||||
"network_op_type": None,
|
||||
"viewer_op_type": None,
|
||||
"locations": ["OpenPype"]
|
||||
}
|
||||
|
||||
label = "Create {}".format(creator.label)
|
||||
tool = hou.shelves.tool(key)
|
||||
if tool:
|
||||
tool.setData(**data)
|
||||
tool.setLabel(label)
|
||||
else:
|
||||
tool = hou.shelves.newTool(
|
||||
file_path=filepath,
|
||||
name=key,
|
||||
label=label,
|
||||
**data
|
||||
)
|
||||
|
||||
tools.append(tool)
|
||||
|
||||
# Ensure the shelf is reloaded
|
||||
hou.shelves.loadFile(filepath)
|
||||
|
||||
return tools
|
||||
|
|
@ -127,6 +127,8 @@ def get_output_parameter(node):
|
|||
return node.parm("filename")
|
||||
elif node_type == "comp":
|
||||
return node.parm("copoutput")
|
||||
elif node_type == "opengl":
|
||||
return node.parm("picture")
|
||||
elif node_type == "arnold":
|
||||
if node.evalParm("ar_ass_export_enable"):
|
||||
return node.parm("ar_ass_file")
|
||||
|
|
@ -479,23 +481,13 @@ def reset_framerange():
|
|||
|
||||
frame_start = asset_data.get("frameStart")
|
||||
frame_end = asset_data.get("frameEnd")
|
||||
# Backwards compatibility
|
||||
if frame_start is None or frame_end is None:
|
||||
frame_start = asset_data.get("edit_in")
|
||||
frame_end = asset_data.get("edit_out")
|
||||
|
||||
if frame_start is None or frame_end is None:
|
||||
log.warning("No edit information found for %s" % asset_name)
|
||||
return
|
||||
|
||||
handles = asset_data.get("handles") or 0
|
||||
handle_start = asset_data.get("handleStart")
|
||||
if handle_start is None:
|
||||
handle_start = handles
|
||||
|
||||
handle_end = asset_data.get("handleEnd")
|
||||
if handle_end is None:
|
||||
handle_end = handles
|
||||
handle_start = asset_data.get("handleStart", 0)
|
||||
handle_end = asset_data.get("handleEnd", 0)
|
||||
|
||||
frame_start -= int(handle_start)
|
||||
frame_end += int(handle_end)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from openpype.pipeline import (
|
|||
)
|
||||
from openpype.pipeline.load import any_outdated_containers
|
||||
from openpype.hosts.houdini import HOUDINI_HOST_DIR
|
||||
from openpype.hosts.houdini.api import lib, shelves
|
||||
from openpype.hosts.houdini.api import lib, shelves, creator_node_shelves
|
||||
|
||||
from openpype.lib import (
|
||||
register_event_callback,
|
||||
|
|
@ -83,6 +83,10 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
|||
_set_context_settings()
|
||||
shelves.generate_shelves()
|
||||
|
||||
if not IS_HEADLESS:
|
||||
import hdefereval # noqa, hdefereval is only available in ui mode
|
||||
hdefereval.executeDeferred(creator_node_shelves.install)
|
||||
|
||||
def has_unsaved_changes(self):
|
||||
return hou.hipFile.hasUnsavedChanges()
|
||||
|
||||
|
|
@ -144,13 +148,10 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
|||
|
||||
"""
|
||||
obj_network = hou.node("/obj")
|
||||
op_ctx = obj_network.createNode("null", node_name="OpenPypeContext")
|
||||
|
||||
# A null in houdini by default comes with content inside to visualize
|
||||
# the null. However since we explicitly want to hide the node lets
|
||||
# remove the content and disable the display flag of the node
|
||||
for node in op_ctx.children():
|
||||
node.destroy()
|
||||
op_ctx = obj_network.createNode("subnet",
|
||||
node_name="OpenPypeContext",
|
||||
run_init_scripts=False,
|
||||
load_contents=False)
|
||||
|
||||
op_ctx.moveToGoodPosition()
|
||||
op_ctx.setBuiltExplicitly(False)
|
||||
|
|
|
|||
125
openpype/hosts/houdini/plugins/create/create_review.py
Normal file
125
openpype/hosts/houdini/plugins/create/create_review.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating openGL reviews."""
|
||||
from openpype.hosts.houdini.api import plugin
|
||||
from openpype.lib import EnumDef, BoolDef, NumberDef
|
||||
|
||||
|
||||
class CreateReview(plugin.HoudiniCreator):
|
||||
"""Review with OpenGL ROP"""
|
||||
|
||||
identifier = "io.openpype.creators.houdini.review"
|
||||
label = "Review"
|
||||
family = "review"
|
||||
icon = "video-camera"
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
import hou
|
||||
|
||||
instance_data.pop("active", None)
|
||||
instance_data.update({"node_type": "opengl"})
|
||||
instance_data["imageFormat"] = pre_create_data.get("imageFormat")
|
||||
instance_data["keepImages"] = pre_create_data.get("keepImages")
|
||||
|
||||
instance = super(CreateReview, self).create(
|
||||
subset_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
instance_node = hou.node(instance.get("instance_node"))
|
||||
|
||||
frame_range = hou.playbar.frameRange()
|
||||
|
||||
filepath = "{root}/{subset}/{subset}.$F4.{ext}".format(
|
||||
root=hou.text.expandString("$HIP/pyblish"),
|
||||
subset="`chs(\"subset\")`", # keep dynamic link to subset
|
||||
ext=pre_create_data.get("image_format") or "png"
|
||||
)
|
||||
|
||||
parms = {
|
||||
"picture": filepath,
|
||||
|
||||
"trange": 1,
|
||||
|
||||
# Unlike many other ROP nodes the opengl node does not default
|
||||
# to expression of $FSTART and $FEND so we preserve that behavior
|
||||
# but do set the range to the frame range of the playbar
|
||||
"f1": frame_range[0],
|
||||
"f2": frame_range[1],
|
||||
}
|
||||
|
||||
override_resolution = pre_create_data.get("override_resolution")
|
||||
if override_resolution:
|
||||
parms.update({
|
||||
"tres": override_resolution,
|
||||
"res1": pre_create_data.get("resx"),
|
||||
"res2": pre_create_data.get("resy"),
|
||||
"aspect": pre_create_data.get("aspect"),
|
||||
})
|
||||
|
||||
if self.selected_nodes:
|
||||
# The first camera found in selection we will use as camera
|
||||
# Other node types we set in force objects
|
||||
camera = None
|
||||
force_objects = []
|
||||
for node in self.selected_nodes:
|
||||
path = node.path()
|
||||
if node.type().name() == "cam":
|
||||
if camera:
|
||||
continue
|
||||
camera = path
|
||||
else:
|
||||
force_objects.append(path)
|
||||
|
||||
if not camera:
|
||||
self.log.warning("No camera found in selection.")
|
||||
|
||||
parms.update({
|
||||
"camera": camera or "",
|
||||
"scenepath": "/obj",
|
||||
"forceobjects": " ".join(force_objects),
|
||||
"vobjects": "" # clear candidate objects from '*' value
|
||||
})
|
||||
|
||||
instance_node.setParms(parms)
|
||||
|
||||
to_lock = ["id", "family"]
|
||||
|
||||
self.lock_parameters(instance_node, to_lock)
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
attrs = super(CreateReview, self).get_pre_create_attr_defs()
|
||||
|
||||
image_format_enum = [
|
||||
"bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png",
|
||||
"rad", "rat", "rta", "sgi", "tga", "tif",
|
||||
]
|
||||
|
||||
return attrs + [
|
||||
BoolDef("keepImages",
|
||||
label="Keep Image Sequences",
|
||||
default=False),
|
||||
EnumDef("imageFormat",
|
||||
image_format_enum,
|
||||
default="png",
|
||||
label="Image Format Options"),
|
||||
BoolDef("override_resolution",
|
||||
label="Override resolution",
|
||||
tooltip="When disabled the resolution set on the camera "
|
||||
"is used instead.",
|
||||
default=True),
|
||||
NumberDef("resx",
|
||||
label="Resolution Width",
|
||||
default=1280,
|
||||
minimum=2,
|
||||
decimals=0),
|
||||
NumberDef("resy",
|
||||
label="Resolution Height",
|
||||
default=720,
|
||||
minimum=2,
|
||||
decimals=0),
|
||||
NumberDef("aspect",
|
||||
label="Aspect Ratio",
|
||||
default=1.0,
|
||||
minimum=0.0001,
|
||||
decimals=3)
|
||||
]
|
||||
|
|
@ -14,7 +14,7 @@ class CollectFrames(pyblish.api.InstancePlugin):
|
|||
|
||||
order = pyblish.api.CollectorOrder
|
||||
label = "Collect Frames"
|
||||
families = ["vdbcache", "imagesequence", "ass", "redshiftproxy"]
|
||||
families = ["vdbcache", "imagesequence", "ass", "redshiftproxy", "review"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
import hou
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectHoudiniReviewData(pyblish.api.InstancePlugin):
|
||||
"""Collect Review Data."""
|
||||
|
||||
label = "Collect Review Data"
|
||||
order = pyblish.api.CollectorOrder + 0.1
|
||||
hosts = ["houdini"]
|
||||
families = ["review"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
# This fixes the burnin having the incorrect start/end timestamps
|
||||
# because without this it would take it from the context instead
|
||||
# which isn't the actual frame range that this instance renders.
|
||||
instance.data["handleStart"] = 0
|
||||
instance.data["handleEnd"] = 0
|
||||
|
||||
# Get the camera from the rop node to collect the focal length
|
||||
ropnode_path = instance.data["instance_node"]
|
||||
ropnode = hou.node(ropnode_path)
|
||||
|
||||
camera_path = ropnode.parm("camera").eval()
|
||||
camera_node = hou.node(camera_path)
|
||||
if not camera_node:
|
||||
raise RuntimeError("No valid camera node found on review node: "
|
||||
"{}".format(camera_path))
|
||||
|
||||
# Collect focal length.
|
||||
focal_length_parm = camera_node.parm("focal")
|
||||
if not focal_length_parm:
|
||||
self.log.warning("No 'focal' (focal length) parameter found on "
|
||||
"camera: {}".format(camera_path))
|
||||
return
|
||||
|
||||
if focal_length_parm.isTimeDependent():
|
||||
start = instance.data["frameStart"]
|
||||
end = instance.data["frameEnd"] + 1
|
||||
focal_length = [
|
||||
focal_length_parm.evalAsFloatAtFrame(t)
|
||||
for t in range(int(start), int(end))
|
||||
]
|
||||
else:
|
||||
focal_length = focal_length_parm.evalAsFloat()
|
||||
|
||||
# Store focal length in `burninDataMembers`
|
||||
burnin_members = instance.data.setdefault("burninDataMembers", {})
|
||||
burnin_members["focalLength"] = focal_length
|
||||
|
||||
instance.data.setdefault("families", []).append('ftrack')
|
||||
58
openpype/hosts/houdini/plugins/publish/extract_opengl.py
Normal file
58
openpype/hosts/houdini/plugins/publish/extract_opengl.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import (
|
||||
publish,
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
from openpype.hosts.houdini.api.lib import render_rop
|
||||
|
||||
import hou
|
||||
|
||||
|
||||
class ExtractOpenGL(publish.Extractor,
|
||||
OptionalPyblishPluginMixin):
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.01
|
||||
label = "Extract OpenGL"
|
||||
families = ["review"]
|
||||
hosts = ["houdini"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
ropnode = hou.node(instance.data.get("instance_node"))
|
||||
|
||||
output = ropnode.evalParm("picture")
|
||||
staging_dir = os.path.normpath(os.path.dirname(output))
|
||||
instance.data["stagingDir"] = staging_dir
|
||||
file_name = os.path.basename(output)
|
||||
|
||||
self.log.info("Extracting '%s' to '%s'" % (file_name,
|
||||
staging_dir))
|
||||
|
||||
render_rop(ropnode)
|
||||
|
||||
output = instance.data["frames"]
|
||||
|
||||
tags = ["review"]
|
||||
if not instance.data.get("keepImages"):
|
||||
tags.append("delete")
|
||||
|
||||
representation = {
|
||||
"name": instance.data["imageFormat"],
|
||||
"ext": instance.data["imageFormat"],
|
||||
"files": output,
|
||||
"stagingDir": staging_dir,
|
||||
"frameStart": instance.data["frameStart"],
|
||||
"frameEnd": instance.data["frameEnd"],
|
||||
"tags": tags,
|
||||
"preview": True,
|
||||
"camera_name": instance.data.get("review_camera")
|
||||
}
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
instance.data["representations"].append(representation)
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
import hou
|
||||
|
||||
|
||||
class ValidateSceneReview(pyblish.api.InstancePlugin):
|
||||
"""Validator Some Scene Settings before publishing the review
|
||||
1. Scene Path
|
||||
2. Resolution
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["review"]
|
||||
hosts = ["houdini"]
|
||||
label = "Scene Setting for review"
|
||||
|
||||
def process(self, instance):
|
||||
invalid = self.get_invalid_scene_path(instance)
|
||||
|
||||
report = []
|
||||
if invalid:
|
||||
report.append(
|
||||
"Scene path does not exist: '%s'" % invalid[0],
|
||||
)
|
||||
|
||||
invalid = self.get_invalid_resolution(instance)
|
||||
if invalid:
|
||||
report.extend(invalid)
|
||||
|
||||
if report:
|
||||
raise PublishValidationError(
|
||||
"\n\n".join(report),
|
||||
title=self.label)
|
||||
|
||||
def get_invalid_scene_path(self, instance):
|
||||
|
||||
node = hou.node(instance.data.get("instance_node"))
|
||||
scene_path_parm = node.parm("scenepath")
|
||||
scene_path_node = scene_path_parm.evalAsNode()
|
||||
if not scene_path_node:
|
||||
return [scene_path_parm.evalAsString()]
|
||||
|
||||
def get_invalid_resolution(self, instance):
|
||||
node = hou.node(instance.data.get("instance_node"))
|
||||
|
||||
# The resolution setting is only used when Override Camera Resolution
|
||||
# is enabled. So we skip validation if it is disabled.
|
||||
override = node.parm("tres").eval()
|
||||
if not override:
|
||||
return
|
||||
|
||||
invalid = []
|
||||
res_width = node.parm("res1").eval()
|
||||
res_height = node.parm("res2").eval()
|
||||
if res_width == 0:
|
||||
invalid.append("Override Resolution width is set to zero.")
|
||||
if res_height == 0:
|
||||
invalid.append("Override Resolution height is set to zero")
|
||||
|
||||
return invalid
|
||||
|
|
@ -209,19 +209,12 @@ def get_frame_range() -> dict:
|
|||
asset = get_current_project_asset()
|
||||
frame_start = asset["data"].get("frameStart")
|
||||
frame_end = asset["data"].get("frameEnd")
|
||||
# Backwards compatibility
|
||||
if frame_start is None or frame_end is None:
|
||||
frame_start = asset["data"].get("edit_in")
|
||||
frame_end = asset["data"].get("edit_out")
|
||||
|
||||
if frame_start is None or frame_end is None:
|
||||
return
|
||||
handles = asset["data"].get("handles") or 0
|
||||
handle_start = asset["data"].get("handleStart")
|
||||
if handle_start is None:
|
||||
handle_start = handles
|
||||
handle_end = asset["data"].get("handleEnd")
|
||||
if handle_end is None:
|
||||
handle_end = handles
|
||||
|
||||
handle_start = asset["data"].get("handleStart", 0)
|
||||
handle_end = asset["data"].get("handleEnd", 0)
|
||||
return {
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,12 @@ from openpype.pipeline import (
|
|||
load_container,
|
||||
registered_host,
|
||||
)
|
||||
from openpype.pipeline.context_tools import get_current_project_asset
|
||||
from openpype.pipeline.context_tools import (
|
||||
get_current_asset_name,
|
||||
get_current_project_asset,
|
||||
get_current_project_name,
|
||||
get_current_task_name
|
||||
)
|
||||
|
||||
|
||||
self = sys.modules[__name__]
|
||||
|
|
@ -292,15 +297,20 @@ def collect_animation_data(fps=False):
|
|||
"""
|
||||
|
||||
# get scene values as defaults
|
||||
start = cmds.playbackOptions(query=True, animationStartTime=True)
|
||||
end = cmds.playbackOptions(query=True, animationEndTime=True)
|
||||
frame_start = cmds.playbackOptions(query=True, minTime=True)
|
||||
frame_end = cmds.playbackOptions(query=True, maxTime=True)
|
||||
handle_start = cmds.playbackOptions(query=True, animationStartTime=True)
|
||||
handle_end = cmds.playbackOptions(query=True, animationEndTime=True)
|
||||
|
||||
handle_start = frame_start - handle_start
|
||||
handle_end = handle_end - frame_end
|
||||
|
||||
# build attributes
|
||||
data = OrderedDict()
|
||||
data["frameStart"] = start
|
||||
data["frameEnd"] = end
|
||||
data["handleStart"] = 0
|
||||
data["handleEnd"] = 0
|
||||
data["frameStart"] = frame_start
|
||||
data["frameEnd"] = frame_end
|
||||
data["handleStart"] = handle_start
|
||||
data["handleEnd"] = handle_end
|
||||
data["step"] = 1.0
|
||||
|
||||
if fps:
|
||||
|
|
@ -1367,6 +1377,71 @@ def set_id(node, unique_id, overwrite=False):
|
|||
cmds.setAttr(attr, unique_id, type="string")
|
||||
|
||||
|
||||
def get_attribute(plug,
|
||||
asString=False,
|
||||
expandEnvironmentVariables=False,
|
||||
**kwargs):
|
||||
"""Maya getAttr with some fixes based on `pymel.core.general.getAttr()`.
|
||||
|
||||
Like Pymel getAttr this applies some changes to `maya.cmds.getAttr`
|
||||
- maya pointlessly returned vector results as a tuple wrapped in a list
|
||||
(ex. '[(1,2,3)]'). This command unpacks the vector for you.
|
||||
- when getting a multi-attr, maya would raise an error, but this will
|
||||
return a list of values for the multi-attr
|
||||
- added support for getting message attributes by returning the
|
||||
connections instead
|
||||
|
||||
Note that the asString + expandEnvironmentVariables argument naming
|
||||
convention matches the `maya.cmds.getAttr` arguments so that it can
|
||||
act as a direct replacement for it.
|
||||
|
||||
Args:
|
||||
plug (str): Node's attribute plug as `node.attribute`
|
||||
asString (bool): Return string value for enum attributes instead
|
||||
of the index. Note that the return value can be dependent on the
|
||||
UI language Maya is running in.
|
||||
expandEnvironmentVariables (bool): Expand any environment variable and
|
||||
(tilde characters on UNIX) found in string attributes which are
|
||||
returned.
|
||||
|
||||
Kwargs:
|
||||
Supports the keyword arguments of `maya.cmds.getAttr`
|
||||
|
||||
Returns:
|
||||
object: The value of the maya attribute.
|
||||
|
||||
"""
|
||||
attr_type = cmds.getAttr(plug, type=True)
|
||||
if asString:
|
||||
kwargs["asString"] = True
|
||||
if expandEnvironmentVariables:
|
||||
kwargs["expandEnvironmentVariables"] = True
|
||||
try:
|
||||
res = cmds.getAttr(plug, **kwargs)
|
||||
except RuntimeError:
|
||||
if attr_type == "message":
|
||||
return cmds.listConnections(plug)
|
||||
|
||||
node, attr = plug.split(".", 1)
|
||||
children = cmds.attributeQuery(attr, node=node, listChildren=True)
|
||||
if children:
|
||||
return [
|
||||
get_attribute("{}.{}".format(node, child))
|
||||
for child in children
|
||||
]
|
||||
|
||||
raise
|
||||
|
||||
# Convert vector result wrapped in tuple
|
||||
if isinstance(res, list) and len(res):
|
||||
if isinstance(res[0], tuple) and len(res):
|
||||
if attr_type in {'pointArray', 'vectorArray'}:
|
||||
return res
|
||||
return res[0]
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def set_attribute(attribute, value, node):
|
||||
"""Adjust attributes based on the value from the attribute data
|
||||
|
||||
|
|
@ -1881,6 +1956,12 @@ def remove_other_uv_sets(mesh):
|
|||
cmds.removeMultiInstance(attr, b=True)
|
||||
|
||||
|
||||
def get_node_parent(node):
|
||||
"""Return full path name for parent of node"""
|
||||
parents = cmds.listRelatives(node, parent=True, fullPath=True)
|
||||
return parents[0] if parents else None
|
||||
|
||||
|
||||
def get_id_from_sibling(node, history_only=True):
|
||||
"""Return first node id in the history chain that matches this node.
|
||||
|
||||
|
|
@ -1904,10 +1985,6 @@ def get_id_from_sibling(node, history_only=True):
|
|||
|
||||
"""
|
||||
|
||||
def _get_parent(node):
|
||||
"""Return full path name for parent of node"""
|
||||
return cmds.listRelatives(node, parent=True, fullPath=True)
|
||||
|
||||
node = cmds.ls(node, long=True)[0]
|
||||
|
||||
# Find all similar nodes in history
|
||||
|
|
@ -1919,8 +1996,8 @@ def get_id_from_sibling(node, history_only=True):
|
|||
similar_nodes = [x for x in similar_nodes if x != node]
|
||||
|
||||
# The node *must be* under the same parent
|
||||
parent = _get_parent(node)
|
||||
similar_nodes = [i for i in similar_nodes if _get_parent(i) == parent]
|
||||
parent = get_node_parent(node)
|
||||
similar_nodes = [i for i in similar_nodes if get_node_parent(i) == parent]
|
||||
|
||||
# Check all of the remaining similar nodes and take the first one
|
||||
# with an id and assume it's the original.
|
||||
|
|
@ -2067,29 +2144,43 @@ def get_frame_range():
|
|||
"""Get the current assets frame range and handles."""
|
||||
|
||||
# Set frame start/end
|
||||
project_name = legacy_io.active_project()
|
||||
asset_name = legacy_io.Session["AVALON_ASSET"]
|
||||
project_name = get_current_project_name()
|
||||
task_name = get_current_task_name()
|
||||
asset_name = get_current_asset_name()
|
||||
asset = get_asset_by_name(project_name, asset_name)
|
||||
settings = get_project_settings(project_name)
|
||||
include_handles_settings = settings["maya"]["include_handles"]
|
||||
current_task = asset.get("data").get("tasks").get(task_name)
|
||||
|
||||
frame_start = asset["data"].get("frameStart")
|
||||
frame_end = asset["data"].get("frameEnd")
|
||||
# Backwards compatibility
|
||||
if frame_start is None or frame_end is None:
|
||||
frame_start = asset["data"].get("edit_in")
|
||||
frame_end = asset["data"].get("edit_out")
|
||||
|
||||
if frame_start is None or frame_end is None:
|
||||
cmds.warning("No edit information found for %s" % asset_name)
|
||||
return
|
||||
|
||||
handles = asset["data"].get("handles") or 0
|
||||
handle_start = asset["data"].get("handleStart")
|
||||
if handle_start is None:
|
||||
handle_start = handles
|
||||
handle_start = asset["data"].get("handleStart") or 0
|
||||
handle_end = asset["data"].get("handleEnd") or 0
|
||||
|
||||
handle_end = asset["data"].get("handleEnd")
|
||||
if handle_end is None:
|
||||
handle_end = handles
|
||||
animation_start = frame_start
|
||||
animation_end = frame_end
|
||||
|
||||
include_handles = include_handles_settings["include_handles_default"]
|
||||
for item in include_handles_settings["per_task_type"]:
|
||||
if current_task["type"] in item["task_type"]:
|
||||
include_handles = item["include_handles"]
|
||||
break
|
||||
if include_handles:
|
||||
animation_start -= int(handle_start)
|
||||
animation_end += int(handle_end)
|
||||
|
||||
cmds.playbackOptions(
|
||||
minTime=frame_start,
|
||||
maxTime=frame_end,
|
||||
animationStartTime=animation_start,
|
||||
animationEndTime=animation_end
|
||||
)
|
||||
cmds.currentTime(frame_start)
|
||||
|
||||
return {
|
||||
"frameStart": frame_start,
|
||||
|
|
@ -2109,7 +2200,6 @@ def reset_frame_range(playback=True, render=True, fps=True):
|
|||
Defaults to True.
|
||||
fps (bool, Optional): Whether to set scene FPS. Defaults to True.
|
||||
"""
|
||||
|
||||
if fps:
|
||||
fps = convert_to_maya_fps(
|
||||
float(legacy_io.Session.get("AVALON_FPS", 25))
|
||||
|
|
@ -3176,38 +3266,78 @@ def set_colorspace():
|
|||
def parent_nodes(nodes, parent=None):
|
||||
# type: (list, str) -> list
|
||||
"""Context manager to un-parent provided nodes and return them back."""
|
||||
import pymel.core as pm # noqa
|
||||
|
||||
parent_node = None
|
||||
def _as_mdagpath(node):
|
||||
"""Return MDagPath for node path."""
|
||||
if not node:
|
||||
return
|
||||
sel = OpenMaya.MSelectionList()
|
||||
sel.add(node)
|
||||
return sel.getDagPath(0)
|
||||
|
||||
# We can only parent dag nodes so we ensure input contains only dag nodes
|
||||
nodes = cmds.ls(nodes, type="dagNode", long=True)
|
||||
if not nodes:
|
||||
# opt-out early
|
||||
yield
|
||||
return
|
||||
|
||||
parent_node_path = None
|
||||
delete_parent = False
|
||||
|
||||
if parent:
|
||||
if not cmds.objExists(parent):
|
||||
parent_node = pm.createNode("transform", n=parent, ss=False)
|
||||
parent_node = cmds.createNode("transform",
|
||||
name=parent,
|
||||
skipSelect=False)
|
||||
delete_parent = True
|
||||
else:
|
||||
parent_node = pm.PyNode(parent)
|
||||
parent_node = parent
|
||||
parent_node_path = cmds.ls(parent_node, long=True)[0]
|
||||
|
||||
# Store original parents
|
||||
node_parents = []
|
||||
for node in nodes:
|
||||
n = pm.PyNode(node)
|
||||
try:
|
||||
root = pm.listRelatives(n, parent=1)[0]
|
||||
except IndexError:
|
||||
root = None
|
||||
node_parents.append((n, root))
|
||||
node_parent = get_node_parent(node)
|
||||
node_parents.append((_as_mdagpath(node), _as_mdagpath(node_parent)))
|
||||
|
||||
try:
|
||||
for node in node_parents:
|
||||
if not parent:
|
||||
node[0].setParent(world=True)
|
||||
for node, node_parent in node_parents:
|
||||
node_parent_path = node_parent.fullPathName() if node_parent else None # noqa
|
||||
if node_parent_path == parent_node_path:
|
||||
# Already a child
|
||||
continue
|
||||
|
||||
if parent_node_path:
|
||||
cmds.parent(node.fullPathName(), parent_node_path)
|
||||
else:
|
||||
node[0].setParent(parent_node)
|
||||
cmds.parent(node.fullPathName(), world=True)
|
||||
|
||||
yield
|
||||
finally:
|
||||
for node in node_parents:
|
||||
if node[1]:
|
||||
node[0].setParent(node[1])
|
||||
# Reparent to original parents
|
||||
for node, original_parent in node_parents:
|
||||
node_path = node.fullPathName()
|
||||
if not node_path:
|
||||
# Node must have been deleted
|
||||
continue
|
||||
|
||||
node_parent_path = get_node_parent(node_path)
|
||||
|
||||
original_parent_path = None
|
||||
if original_parent:
|
||||
original_parent_path = original_parent.fullPathName()
|
||||
if not original_parent_path:
|
||||
# Original parent node must have been deleted
|
||||
continue
|
||||
|
||||
if node_parent_path != original_parent_path:
|
||||
if not original_parent_path:
|
||||
cmds.parent(node_path, world=True)
|
||||
else:
|
||||
cmds.parent(node_path, original_parent_path)
|
||||
|
||||
if delete_parent:
|
||||
pm.delete(parent_node)
|
||||
cmds.delete(parent_node_path)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
|
@ -3558,7 +3688,17 @@ def get_color_management_preferences():
|
|||
# Split view and display from view_transform. view_transform comes in
|
||||
# format of "{view} ({display})".
|
||||
regex = re.compile(r"^(?P<view>.+) \((?P<display>.+)\)$")
|
||||
if int(cmds.about(version=True)) <= 2020:
|
||||
# view_transform comes in format of "{view} {display}" in 2020.
|
||||
regex = re.compile(r"^(?P<view>.+) (?P<display>.+)$")
|
||||
|
||||
match = regex.match(data["view_transform"])
|
||||
if not match:
|
||||
raise ValueError(
|
||||
"Unable to parse view and display from Maya view transform: '{}' "
|
||||
"using regex '{}'".format(data["view_transform"], regex.pattern)
|
||||
)
|
||||
|
||||
data.update({
|
||||
"display": match.group("display"),
|
||||
"view": match.group("view")
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ from maya.app.renderSetup.model.override import (
|
|||
UniqueOverride
|
||||
)
|
||||
|
||||
from openpype.hosts.maya.api.lib import get_attribute
|
||||
|
||||
EXACT_MATCH = 0
|
||||
PARENT_MATCH = 1
|
||||
CLIENT_MATCH = 2
|
||||
|
|
@ -96,9 +98,6 @@ def get_attr_in_layer(node_attr, layer):
|
|||
|
||||
"""
|
||||
|
||||
# Delay pymel import to here because it's slow to load
|
||||
import pymel.core as pm
|
||||
|
||||
def _layer_needs_update(layer):
|
||||
"""Return whether layer needs updating."""
|
||||
# Use `getattr` as e.g. DEFAULT_RENDER_LAYER does not have
|
||||
|
|
@ -125,7 +124,7 @@ def get_attr_in_layer(node_attr, layer):
|
|||
node = history_overrides[-1] if history_overrides else override
|
||||
node_attr_ = node + ".original"
|
||||
|
||||
return pm.getAttr(node_attr_, asString=True)
|
||||
return get_attribute(node_attr_, asString=True)
|
||||
|
||||
layer = get_rendersetup_layer(layer)
|
||||
rs = renderSetup.instance()
|
||||
|
|
@ -145,7 +144,7 @@ def get_attr_in_layer(node_attr, layer):
|
|||
# we will let it error out.
|
||||
rs.switchToLayer(current_layer)
|
||||
|
||||
return pm.getAttr(node_attr, asString=True)
|
||||
return get_attribute(node_attr, asString=True)
|
||||
|
||||
overrides = get_attr_overrides(node_attr, layer)
|
||||
default_layer_value = get_default_layer_value(node_attr)
|
||||
|
|
@ -156,7 +155,7 @@ def get_attr_in_layer(node_attr, layer):
|
|||
for match, layer_override, index in overrides:
|
||||
if isinstance(layer_override, AbsOverride):
|
||||
# Absolute override
|
||||
value = pm.getAttr(layer_override.name() + ".attrValue")
|
||||
value = get_attribute(layer_override.name() + ".attrValue")
|
||||
if match == EXACT_MATCH:
|
||||
# value = value
|
||||
pass
|
||||
|
|
@ -168,8 +167,8 @@ def get_attr_in_layer(node_attr, layer):
|
|||
elif isinstance(layer_override, RelOverride):
|
||||
# Relative override
|
||||
# Value = Original * Multiply + Offset
|
||||
multiply = pm.getAttr(layer_override.name() + ".multiply")
|
||||
offset = pm.getAttr(layer_override.name() + ".offset")
|
||||
multiply = get_attribute(layer_override.name() + ".multiply")
|
||||
offset = get_attribute(layer_override.name() + ".offset")
|
||||
|
||||
if match == EXACT_MATCH:
|
||||
value = value * multiply + offset
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import re
|
||||
|
||||
from maya import cmds
|
||||
|
||||
|
|
@ -12,6 +13,7 @@ from openpype.pipeline import (
|
|||
AVALON_CONTAINER_ID,
|
||||
Anatomy,
|
||||
)
|
||||
from openpype.pipeline.load import LoadError
|
||||
from openpype.settings import get_project_settings
|
||||
from .pipeline import containerise
|
||||
from . import lib
|
||||
|
|
@ -82,6 +84,44 @@ def get_reference_node_parents(ref):
|
|||
return parents
|
||||
|
||||
|
||||
def get_custom_namespace(custom_namespace):
|
||||
"""Return unique namespace.
|
||||
|
||||
The input namespace can contain a single group
|
||||
of '#' number tokens to indicate where the namespace's
|
||||
unique index should go. The amount of tokens defines
|
||||
the zero padding of the number, e.g ### turns into 001.
|
||||
|
||||
Warning: Note that a namespace will always be
|
||||
prefixed with a _ if it starts with a digit
|
||||
|
||||
Example:
|
||||
>>> get_custom_namespace("myspace_##_")
|
||||
# myspace_01_
|
||||
>>> get_custom_namespace("##_myspace")
|
||||
# _01_myspace
|
||||
>>> get_custom_namespace("myspace##")
|
||||
# myspace01
|
||||
|
||||
"""
|
||||
split = re.split("([#]+)", custom_namespace, 1)
|
||||
|
||||
if len(split) == 3:
|
||||
base, padding, suffix = split
|
||||
padding = "%0{}d".format(len(padding))
|
||||
else:
|
||||
base = split[0]
|
||||
padding = "%02d" # default padding
|
||||
suffix = ""
|
||||
|
||||
return lib.unique_namespace(
|
||||
base,
|
||||
format=padding,
|
||||
prefix="_" if not base or base[0].isdigit() else "",
|
||||
suffix=suffix
|
||||
)
|
||||
|
||||
|
||||
class Creator(LegacyCreator):
|
||||
defaults = ['Main']
|
||||
|
||||
|
|
@ -143,15 +183,46 @@ class ReferenceLoader(Loader):
|
|||
assert os.path.exists(self.fname), "%s does not exist." % self.fname
|
||||
|
||||
asset = context['asset']
|
||||
subset = context['subset']
|
||||
settings = get_project_settings(context['project']['name'])
|
||||
custom_naming = settings['maya']['load']['reference_loader']
|
||||
loaded_containers = []
|
||||
|
||||
count = options.get("count") or 1
|
||||
for c in range(0, count):
|
||||
namespace = namespace or lib.unique_namespace(
|
||||
"{}_{}_".format(asset["name"], context["subset"]["name"]),
|
||||
prefix="_" if asset["name"][0].isdigit() else "",
|
||||
suffix="_",
|
||||
if not custom_naming['namespace']:
|
||||
raise LoadError("No namespace specified in "
|
||||
"Maya ReferenceLoader settings")
|
||||
elif not custom_naming['group_name']:
|
||||
raise LoadError("No group name specified in "
|
||||
"Maya ReferenceLoader settings")
|
||||
|
||||
formatting_data = {
|
||||
"asset_name": asset['name'],
|
||||
"asset_type": asset['type'],
|
||||
"subset": subset['name'],
|
||||
"family": (
|
||||
subset['data'].get('family') or
|
||||
subset['data']['families'][0]
|
||||
)
|
||||
}
|
||||
|
||||
custom_namespace = custom_naming['namespace'].format(
|
||||
**formatting_data
|
||||
)
|
||||
|
||||
custom_group_name = custom_naming['group_name'].format(
|
||||
**formatting_data
|
||||
)
|
||||
|
||||
count = options.get("count") or 1
|
||||
|
||||
for c in range(0, count):
|
||||
namespace = get_custom_namespace(custom_namespace)
|
||||
group_name = "{}:{}".format(
|
||||
namespace,
|
||||
custom_group_name
|
||||
)
|
||||
|
||||
options['group_name'] = group_name
|
||||
|
||||
# Offset loaded subset
|
||||
if "offset" in options:
|
||||
|
|
@ -187,7 +258,7 @@ class ReferenceLoader(Loader):
|
|||
|
||||
return loaded_containers
|
||||
|
||||
def process_reference(self, context, name, namespace, data):
|
||||
def process_reference(self, context, name, namespace, options):
|
||||
"""To be implemented by subclass"""
|
||||
raise NotImplementedError("Must be implemented by subclass")
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def process_reference(self, context, name, namespace, data):
|
||||
def process_reference(self, context, name, namespace, options):
|
||||
|
||||
import maya.cmds as cmds
|
||||
from openpype.hosts.maya.api.lib import unique_namespace
|
||||
|
|
@ -41,7 +41,7 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
namespace=namespace,
|
||||
sharedReferenceFile=False,
|
||||
groupReference=True,
|
||||
groupName="{}:{}".format(namespace, name),
|
||||
groupName=options['group_name'],
|
||||
reference=True,
|
||||
returnNewNodes=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from openpype.pipeline import (
|
|||
get_representation_path,
|
||||
)
|
||||
from openpype.hosts.maya.api.pipeline import containerise
|
||||
from openpype.hosts.maya.api.lib import unique_namespace
|
||||
from openpype.hosts.maya.api.lib import unique_namespace, get_container_members
|
||||
|
||||
|
||||
class AudioLoader(load.LoaderPlugin):
|
||||
|
|
@ -52,17 +52,15 @@ class AudioLoader(load.LoaderPlugin):
|
|||
)
|
||||
|
||||
def update(self, container, representation):
|
||||
import pymel.core as pm
|
||||
|
||||
audio_node = None
|
||||
for node in pm.PyNode(container["objectName"]).members():
|
||||
if node.nodeType() == "audio":
|
||||
audio_node = node
|
||||
members = get_container_members(container)
|
||||
audio_nodes = cmds.ls(members, type="audio")
|
||||
|
||||
assert audio_node is not None, "Audio node not found."
|
||||
assert audio_nodes is not None, "Audio node not found."
|
||||
audio_node = audio_nodes[0]
|
||||
|
||||
path = get_representation_path(representation)
|
||||
audio_node.filename.set(path)
|
||||
cmds.setAttr("{}.filename".format(audio_node), path, type="string")
|
||||
cmds.setAttr(
|
||||
container["objectName"] + ".representation",
|
||||
str(representation["_id"]),
|
||||
|
|
@ -80,8 +78,12 @@ class AudioLoader(load.LoaderPlugin):
|
|||
asset = get_asset_by_id(
|
||||
project_name, subset["parent"], fields=["parent"]
|
||||
)
|
||||
audio_node.sourceStart.set(1 - asset["data"]["frameStart"])
|
||||
audio_node.sourceEnd.set(asset["data"]["frameEnd"])
|
||||
|
||||
source_start = 1 - asset["data"]["frameStart"]
|
||||
source_end = asset["data"]["frameEnd"]
|
||||
|
||||
cmds.setAttr("{}.sourceStart".format(audio_node), source_start)
|
||||
cmds.setAttr("{}.sourceEnd".format(audio_node), source_end)
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
|
|
|||
|
|
@ -11,11 +11,26 @@ from openpype.pipeline import (
|
|||
get_representation_path
|
||||
)
|
||||
from openpype.hosts.maya.api.pipeline import containerise
|
||||
from openpype.hosts.maya.api.lib import unique_namespace
|
||||
from openpype.hosts.maya.api.lib import (
|
||||
unique_namespace,
|
||||
namespaced,
|
||||
pairwise,
|
||||
get_container_members
|
||||
)
|
||||
|
||||
from maya import cmds
|
||||
|
||||
|
||||
def disconnect_inputs(plug):
|
||||
overrides = cmds.listConnections(plug,
|
||||
source=True,
|
||||
destination=False,
|
||||
plugs=True,
|
||||
connections=True) or []
|
||||
for dest, src in pairwise(overrides):
|
||||
cmds.disconnectAttr(src, dest)
|
||||
|
||||
|
||||
class CameraWindow(QtWidgets.QDialog):
|
||||
|
||||
def __init__(self, cameras):
|
||||
|
|
@ -74,6 +89,7 @@ class CameraWindow(QtWidgets.QDialog):
|
|||
self.camera = None
|
||||
self.close()
|
||||
|
||||
|
||||
class ImagePlaneLoader(load.LoaderPlugin):
|
||||
"""Specific loader of plate for image planes on selected camera."""
|
||||
|
||||
|
|
@ -84,9 +100,7 @@ class ImagePlaneLoader(load.LoaderPlugin):
|
|||
color = "orange"
|
||||
|
||||
def load(self, context, name, namespace, data, options=None):
|
||||
import pymel.core as pm
|
||||
|
||||
new_nodes = []
|
||||
image_plane_depth = 1000
|
||||
asset = context['asset']['name']
|
||||
namespace = namespace or unique_namespace(
|
||||
|
|
@ -96,16 +110,20 @@ class ImagePlaneLoader(load.LoaderPlugin):
|
|||
)
|
||||
|
||||
# Get camera from user selection.
|
||||
camera = None
|
||||
# is_static_image_plane = None
|
||||
# is_in_all_views = None
|
||||
if data:
|
||||
camera = pm.PyNode(data.get("camera"))
|
||||
camera = data.get("camera") if data else None
|
||||
|
||||
if not camera:
|
||||
cameras = pm.ls(type="camera")
|
||||
camera_names = {x.getParent().name(): x for x in cameras}
|
||||
camera_names["Create new camera."] = "create_camera"
|
||||
cameras = cmds.ls(type="camera")
|
||||
|
||||
# Cameras by names
|
||||
camera_names = {}
|
||||
for camera in cameras:
|
||||
parent = cmds.listRelatives(camera, parent=True, path=True)[0]
|
||||
camera_names[parent] = camera
|
||||
|
||||
camera_names["Create new camera."] = "create-camera"
|
||||
window = CameraWindow(camera_names.keys())
|
||||
window.exec_()
|
||||
# Skip if no camera was selected (Dialog was closed)
|
||||
|
|
@ -113,43 +131,48 @@ class ImagePlaneLoader(load.LoaderPlugin):
|
|||
return
|
||||
camera = camera_names[window.camera]
|
||||
|
||||
if camera == "create_camera":
|
||||
camera = pm.createNode("camera")
|
||||
if camera == "create-camera":
|
||||
camera = cmds.createNode("camera")
|
||||
|
||||
if camera is None:
|
||||
return
|
||||
|
||||
try:
|
||||
camera.displayResolution.set(1)
|
||||
camera.farClipPlane.set(image_plane_depth * 10)
|
||||
cmds.setAttr("{}.displayResolution".format(camera), True)
|
||||
cmds.setAttr("{}.farClipPlane".format(camera),
|
||||
image_plane_depth * 10)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
# Create image plane
|
||||
image_plane_transform, image_plane_shape = pm.imagePlane(
|
||||
fileName=context["representation"]["data"]["path"],
|
||||
camera=camera)
|
||||
image_plane_shape.depth.set(image_plane_depth)
|
||||
with namespaced(namespace):
|
||||
# Create inside the namespace
|
||||
image_plane_transform, image_plane_shape = cmds.imagePlane(
|
||||
fileName=context["representation"]["data"]["path"],
|
||||
camera=camera
|
||||
)
|
||||
start_frame = cmds.playbackOptions(query=True, min=True)
|
||||
end_frame = cmds.playbackOptions(query=True, max=True)
|
||||
|
||||
|
||||
start_frame = pm.playbackOptions(q=True, min=True)
|
||||
end_frame = pm.playbackOptions(q=True, max=True)
|
||||
|
||||
image_plane_shape.frameOffset.set(0)
|
||||
image_plane_shape.frameIn.set(start_frame)
|
||||
image_plane_shape.frameOut.set(end_frame)
|
||||
image_plane_shape.frameCache.set(end_frame)
|
||||
image_plane_shape.useFrameExtension.set(1)
|
||||
for attr, value in {
|
||||
"depth": image_plane_depth,
|
||||
"frameOffset": 0,
|
||||
"frameIn": start_frame,
|
||||
"frameOut": end_frame,
|
||||
"frameCache": end_frame,
|
||||
"useFrameExtension": True
|
||||
}.items():
|
||||
plug = "{}.{}".format(image_plane_shape, attr)
|
||||
cmds.setAttr(plug, value)
|
||||
|
||||
movie_representations = ["mov", "preview"]
|
||||
if context["representation"]["name"] in movie_representations:
|
||||
# Need to get "type" by string, because its a method as well.
|
||||
pm.Attribute(image_plane_shape + ".type").set(2)
|
||||
cmds.setAttr(image_plane_shape + ".type", 2)
|
||||
|
||||
# Ask user whether to use sequence or still image.
|
||||
if context["representation"]["name"] == "exr":
|
||||
# Ensure OpenEXRLoader plugin is loaded.
|
||||
pm.loadPlugin("OpenEXRLoader.mll", quiet=True)
|
||||
cmds.loadPlugin("OpenEXRLoader", quiet=True)
|
||||
|
||||
message = (
|
||||
"Hold image sequence on first frame?"
|
||||
|
|
@ -161,32 +184,18 @@ class ImagePlaneLoader(load.LoaderPlugin):
|
|||
None,
|
||||
"Frame Hold.",
|
||||
message,
|
||||
QtWidgets.QMessageBox.Ok,
|
||||
QtWidgets.QMessageBox.Cancel
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No
|
||||
)
|
||||
if reply == QtWidgets.QMessageBox.Ok:
|
||||
# find the input and output of frame extension
|
||||
expressions = image_plane_shape.frameExtension.inputs()
|
||||
frame_ext_output = image_plane_shape.frameExtension.outputs()
|
||||
if expressions:
|
||||
# the "time1" node is non-deletable attr
|
||||
# in Maya, use disconnectAttr instead
|
||||
pm.disconnectAttr(expressions, frame_ext_output)
|
||||
if reply == QtWidgets.QMessageBox.Yes:
|
||||
frame_extension_plug = "{}.frameExtension".format(image_plane_shape) # noqa
|
||||
|
||||
if not image_plane_shape.frameExtension.isFreeToChange():
|
||||
raise RuntimeError("Can't set frame extension for {}".format(image_plane_shape)) # noqa
|
||||
# get the node of time instead and set the time for it.
|
||||
image_plane_shape.frameExtension.set(start_frame)
|
||||
# Remove current frame expression
|
||||
disconnect_inputs(frame_extension_plug)
|
||||
|
||||
new_nodes.extend(
|
||||
[
|
||||
image_plane_transform.longName().split("|")[-1],
|
||||
image_plane_shape.longName().split("|")[-1]
|
||||
]
|
||||
)
|
||||
cmds.setAttr(frame_extension_plug, start_frame)
|
||||
|
||||
for node in new_nodes:
|
||||
pm.rename(node, "{}:{}".format(namespace, node))
|
||||
new_nodes = [image_plane_transform, image_plane_shape]
|
||||
|
||||
return containerise(
|
||||
name=name,
|
||||
|
|
@ -197,21 +206,19 @@ class ImagePlaneLoader(load.LoaderPlugin):
|
|||
)
|
||||
|
||||
def update(self, container, representation):
|
||||
import pymel.core as pm
|
||||
image_plane_shape = None
|
||||
for node in pm.PyNode(container["objectName"]).members():
|
||||
if node.nodeType() == "imagePlane":
|
||||
image_plane_shape = node
|
||||
|
||||
assert image_plane_shape is not None, "Image plane not found."
|
||||
members = get_container_members(container)
|
||||
image_planes = cmds.ls(members, type="imagePlane")
|
||||
assert image_planes, "Image plane not found."
|
||||
image_plane_shape = image_planes[0]
|
||||
|
||||
path = get_representation_path(representation)
|
||||
image_plane_shape.imageName.set(path)
|
||||
cmds.setAttr(
|
||||
container["objectName"] + ".representation",
|
||||
str(representation["_id"]),
|
||||
type="string"
|
||||
)
|
||||
cmds.setAttr("{}.imageName".format(image_plane_shape),
|
||||
path,
|
||||
type="string")
|
||||
cmds.setAttr("{}.representation".format(container["objectName"]),
|
||||
str(representation["_id"]),
|
||||
type="string")
|
||||
|
||||
# Set frame range.
|
||||
project_name = legacy_io.active_project()
|
||||
|
|
@ -227,10 +234,14 @@ class ImagePlaneLoader(load.LoaderPlugin):
|
|||
start_frame = asset["data"]["frameStart"]
|
||||
end_frame = asset["data"]["frameEnd"]
|
||||
|
||||
image_plane_shape.frameOffset.set(0)
|
||||
image_plane_shape.frameIn.set(start_frame)
|
||||
image_plane_shape.frameOut.set(end_frame)
|
||||
image_plane_shape.frameCache.set(end_frame)
|
||||
for attr, value in {
|
||||
"frameOffset": 0,
|
||||
"frameIn": start_frame,
|
||||
"frameOut": end_frame,
|
||||
"frameCache": end_frame
|
||||
}:
|
||||
plug = "{}.{}".format(image_plane_shape, attr)
|
||||
cmds.setAttr(plug, value)
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ from openpype.pipeline.create import (
|
|||
import openpype.hosts.maya.api.plugin
|
||||
from openpype.hosts.maya.api.lib import (
|
||||
maintained_selection,
|
||||
get_container_members
|
||||
get_container_members,
|
||||
parent_nodes
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -118,21 +119,21 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
|
||||
def process_reference(self, context, name, namespace, options):
|
||||
import maya.cmds as cmds
|
||||
import pymel.core as pm
|
||||
|
||||
try:
|
||||
family = context["representation"]["context"]["family"]
|
||||
except ValueError:
|
||||
family = "model"
|
||||
|
||||
group_name = "{}:_GRP".format(namespace)
|
||||
# True by default to keep legacy behaviours
|
||||
attach_to_root = options.get("attach_to_root", True)
|
||||
group_name = options["group_name"]
|
||||
|
||||
with maintained_selection():
|
||||
cmds.loadPlugin("AbcImport.mll", quiet=True)
|
||||
file_url = self.prepare_root_value(self.fname,
|
||||
context["project"]["name"])
|
||||
|
||||
nodes = cmds.file(file_url,
|
||||
namespace=namespace,
|
||||
sharedReferenceFile=False,
|
||||
|
|
@ -148,7 +149,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
# if there are cameras, try to lock their transforms
|
||||
self._lock_camera_transforms(new_nodes)
|
||||
|
||||
current_namespace = pm.namespaceInfo(currentNamespace=True)
|
||||
current_namespace = cmds.namespaceInfo(currentNamespace=True)
|
||||
|
||||
if current_namespace != ":":
|
||||
group_name = current_namespace + ":" + group_name
|
||||
|
|
@ -158,37 +159,29 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
self[:] = new_nodes
|
||||
|
||||
if attach_to_root:
|
||||
group_node = pm.PyNode(group_name)
|
||||
roots = set()
|
||||
roots = cmds.listRelatives(group_name,
|
||||
children=True,
|
||||
fullPath=True) or []
|
||||
|
||||
for node in new_nodes:
|
||||
try:
|
||||
roots.add(pm.PyNode(node).getAllParents()[-2])
|
||||
except: # noqa: E722
|
||||
pass
|
||||
if family not in {"layout", "setdress",
|
||||
"mayaAscii", "mayaScene"}:
|
||||
# QUESTION Why do we need to exclude these families?
|
||||
with parent_nodes(roots, parent=None):
|
||||
cmds.xform(group_name, zeroTransformPivots=True)
|
||||
|
||||
if family not in ["layout", "setdress",
|
||||
"mayaAscii", "mayaScene"]:
|
||||
for root in roots:
|
||||
root.setParent(world=True)
|
||||
|
||||
group_node.zeroTransformPivots()
|
||||
for root in roots:
|
||||
root.setParent(group_node)
|
||||
|
||||
cmds.setAttr(group_name + ".displayHandle", 1)
|
||||
cmds.setAttr("{}.displayHandle".format(group_name), 1)
|
||||
|
||||
settings = get_project_settings(os.environ['AVALON_PROJECT'])
|
||||
colors = settings['maya']['load']['colors']
|
||||
c = colors.get(family)
|
||||
if c is not None:
|
||||
group_node.useOutlinerColor.set(1)
|
||||
group_node.outlinerColor.set(
|
||||
(float(c[0]) / 255),
|
||||
(float(c[1]) / 255),
|
||||
(float(c[2]) / 255))
|
||||
cmds.setAttr("{}.useOutlinerColor".format(group_name), 1)
|
||||
cmds.setAttr("{}.outlinerColor".format(group_name),
|
||||
(float(c[0]) / 255),
|
||||
(float(c[1]) / 255),
|
||||
(float(c[2]) / 255))
|
||||
|
||||
cmds.setAttr(group_name + ".displayHandle", 1)
|
||||
cmds.setAttr("{}.displayHandle".format(group_name), 1)
|
||||
# get bounding box
|
||||
bbox = cmds.exactWorldBoundingBox(group_name)
|
||||
# get pivot position on world space
|
||||
|
|
@ -202,15 +195,16 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
cy = cy + pivot[1]
|
||||
cz = cz + pivot[2]
|
||||
# set selection handle offset to center of bounding box
|
||||
cmds.setAttr(group_name + ".selectHandleX", cx)
|
||||
cmds.setAttr(group_name + ".selectHandleY", cy)
|
||||
cmds.setAttr(group_name + ".selectHandleZ", cz)
|
||||
cmds.setAttr("{}.selectHandleX".format(group_name), cx)
|
||||
cmds.setAttr("{}.selectHandleY".format(group_name), cy)
|
||||
cmds.setAttr("{}.selectHandleZ".format(group_name), cz)
|
||||
|
||||
if family == "rig":
|
||||
self._post_process_rig(name, namespace, context, options)
|
||||
else:
|
||||
if "translate" in options:
|
||||
cmds.setAttr(group_name + ".t", *options["translate"])
|
||||
cmds.setAttr("{}.translate".format(group_name),
|
||||
*options["translate"])
|
||||
return new_nodes
|
||||
|
||||
def switch(self, container, representation):
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@ class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
def process_reference(
|
||||
self, context, name=None, namespace=None, options=None
|
||||
):
|
||||
|
||||
group_name = "{}:{}".format(namespace, name)
|
||||
group_name = options['group_name']
|
||||
with lib.maintained_selection():
|
||||
file_url = self.prepare_root_value(
|
||||
self.fname, context["project"]["name"]
|
||||
|
|
|
|||
|
|
@ -109,13 +109,6 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
|
||||
# Append start frame and end frame to label if present
|
||||
if "frameStart" and "frameEnd" in data:
|
||||
|
||||
# Backwards compatibility for 'handles' data
|
||||
if "handles" in data:
|
||||
data["handleStart"] = data["handles"]
|
||||
data["handleEnd"] = data["handles"]
|
||||
data.pop('handles')
|
||||
|
||||
# Take handles from context if not set locally on the instance
|
||||
for key in ["handleStart", "handleEnd"]:
|
||||
if key not in data:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from maya import cmds, mel
|
||||
import pymel.core as pm
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
|
@ -83,7 +82,6 @@ class CollectReview(pyblish.api.InstancePlugin):
|
|||
data['frameEndHandle'] = instance.data["frameEndHandle"]
|
||||
data["frameStart"] = instance.data["frameStart"]
|
||||
data["frameEnd"] = instance.data["frameEnd"]
|
||||
data['handles'] = instance.data.get('handles', None)
|
||||
data['step'] = instance.data['step']
|
||||
data['fps'] = instance.data['fps']
|
||||
data['review_width'] = instance.data['review_width']
|
||||
|
|
@ -124,43 +122,42 @@ class CollectReview(pyblish.api.InstancePlugin):
|
|||
|
||||
# Collect audio
|
||||
playback_slider = mel.eval('$tmpVar=$gPlayBackSlider')
|
||||
audio_name = cmds.timeControl(playback_slider, q=True, s=True)
|
||||
audio_name = cmds.timeControl(playback_slider,
|
||||
query=True,
|
||||
sound=True)
|
||||
display_sounds = cmds.timeControl(
|
||||
playback_slider, q=True, displaySound=True
|
||||
playback_slider, query=True, displaySound=True
|
||||
)
|
||||
|
||||
audio_nodes = []
|
||||
def get_audio_node_data(node):
|
||||
return {
|
||||
"offset": cmds.getAttr("{}.offset".format(node)),
|
||||
"filename": cmds.getAttr("{}.filename".format(node))
|
||||
}
|
||||
|
||||
audio_data = []
|
||||
|
||||
if audio_name:
|
||||
audio_nodes.append(pm.PyNode(audio_name))
|
||||
audio_data.append(get_audio_node_data(audio_name))
|
||||
|
||||
if not audio_name and display_sounds:
|
||||
start_frame = int(pm.playbackOptions(q=True, min=True))
|
||||
end_frame = float(pm.playbackOptions(q=True, max=True))
|
||||
frame_range = range(int(start_frame), int(end_frame))
|
||||
elif display_sounds:
|
||||
start_frame = int(cmds.playbackOptions(query=True, min=True))
|
||||
end_frame = int(cmds.playbackOptions(query=True, max=True))
|
||||
|
||||
for node in pm.ls(type="audio"):
|
||||
for node in cmds.ls(type="audio"):
|
||||
# Check if frame range and audio range intersections,
|
||||
# for whether to include this audio node or not.
|
||||
start_audio = node.offset.get()
|
||||
end_audio = node.offset.get() + node.duration.get()
|
||||
audio_range = range(int(start_audio), int(end_audio))
|
||||
duration = cmds.getAttr("{}.duration".format(node))
|
||||
start_audio = cmds.getAttr("{}.offset".format(node))
|
||||
end_audio = start_audio + duration
|
||||
|
||||
if bool(set(frame_range).intersection(audio_range)):
|
||||
audio_nodes.append(node)
|
||||
if start_audio <= end_frame and end_audio > start_frame:
|
||||
audio_data.append(get_audio_node_data(node))
|
||||
|
||||
instance.data["audio"] = []
|
||||
for node in audio_nodes:
|
||||
instance.data["audio"].append(
|
||||
{
|
||||
"offset": node.offset.get(),
|
||||
"filename": node.filename.get()
|
||||
}
|
||||
)
|
||||
instance.data["audio"] = audio_data
|
||||
|
||||
# Collect focal length.
|
||||
attr = camera + ".focalLength"
|
||||
focal_length = None
|
||||
if get_attribute_input(attr):
|
||||
start = instance.data["frameStart"]
|
||||
end = instance.data["frameEnd"] + 1
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ HARDLINK = 2
|
|||
|
||||
|
||||
@attr.s
|
||||
class TextureResult:
|
||||
class TextureResult(object):
|
||||
"""The resulting texture of a processed file for a resource"""
|
||||
# Path to the file
|
||||
path = attr.ib()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from openpype.pipeline import publish
|
|||
from openpype.hosts.maya.api import lib
|
||||
|
||||
from maya import cmds
|
||||
import pymel.core as pm
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
|
@ -110,11 +109,11 @@ class ExtractPlayblast(publish.Extractor):
|
|||
preset["filename"] = path
|
||||
preset["overwrite"] = True
|
||||
|
||||
pm.refresh(f=True)
|
||||
cmds.refresh(force=True)
|
||||
|
||||
refreshFrameInt = int(pm.playbackOptions(q=True, minTime=True))
|
||||
pm.currentTime(refreshFrameInt - 1, edit=True)
|
||||
pm.currentTime(refreshFrameInt, edit=True)
|
||||
refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True))
|
||||
cmds.currentTime(refreshFrameInt - 1, edit=True)
|
||||
cmds.currentTime(refreshFrameInt, edit=True)
|
||||
|
||||
# Override transparency if requested.
|
||||
transparency = instance.data.get("transparency", 0)
|
||||
|
|
@ -226,7 +225,7 @@ class ExtractPlayblast(publish.Extractor):
|
|||
tags.append("delete")
|
||||
|
||||
# Add camera node name to representation data
|
||||
camera_node_name = pm.ls(camera)[0].getTransform().name()
|
||||
camera_node_name = cmds.listRelatives(camera, parent=True)[0]
|
||||
|
||||
collected_files = list(frame_collection)
|
||||
# single frame file shouldn't be in list, only as a string
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ from openpype.pipeline import publish
|
|||
from openpype.hosts.maya.api import lib
|
||||
|
||||
from maya import cmds
|
||||
import pymel.core as pm
|
||||
|
||||
|
||||
class ExtractThumbnail(publish.Extractor):
|
||||
|
|
@ -99,11 +98,11 @@ class ExtractThumbnail(publish.Extractor):
|
|||
preset["filename"] = path
|
||||
preset["overwrite"] = True
|
||||
|
||||
pm.refresh(f=True)
|
||||
cmds.refresh(force=True)
|
||||
|
||||
refreshFrameInt = int(pm.playbackOptions(q=True, minTime=True))
|
||||
pm.currentTime(refreshFrameInt - 1, edit=True)
|
||||
pm.currentTime(refreshFrameInt, edit=True)
|
||||
refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True))
|
||||
cmds.currentTime(refreshFrameInt - 1, edit=True)
|
||||
cmds.currentTime(refreshFrameInt, edit=True)
|
||||
|
||||
# Override transparency if requested.
|
||||
transparency = instance.data.get("transparency", 0)
|
||||
|
|
|
|||
|
|
@ -30,9 +30,7 @@ class ExtractVRayProxy(publish.Extractor):
|
|||
# non-animated subsets
|
||||
keys = ["frameStart", "frameEnd",
|
||||
"handleStart", "handleEnd",
|
||||
"frameStartHandle", "frameEndHandle",
|
||||
# Backwards compatibility
|
||||
"handles"]
|
||||
"frameStartHandle", "frameEndHandle"]
|
||||
for key in keys:
|
||||
instance.data.pop(key, None)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
import pymel.core as pm
|
||||
from collections import defaultdict
|
||||
|
||||
from maya import cmds
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.hosts.maya.api.lib import set_attribute
|
||||
from openpype.pipeline.publish import (
|
||||
RepairContextAction,
|
||||
ValidateContentsOrder,
|
||||
)
|
||||
|
||||
|
||||
class ValidateAttributes(pyblish.api.ContextPlugin):
|
||||
class ValidateAttributes(pyblish.api.InstancePlugin):
|
||||
"""Ensure attributes are consistent.
|
||||
|
||||
Attributes to validate and their values comes from the
|
||||
|
|
@ -27,86 +31,80 @@ class ValidateAttributes(pyblish.api.ContextPlugin):
|
|||
|
||||
attributes = None
|
||||
|
||||
def process(self, context):
|
||||
def process(self, instance):
|
||||
# Check for preset existence.
|
||||
|
||||
if not self.attributes:
|
||||
return
|
||||
|
||||
invalid = self.get_invalid(context, compute=True)
|
||||
invalid = self.get_invalid(instance, compute=True)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
"Found attributes with invalid values: {}".format(invalid)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, context, compute=False):
|
||||
invalid = context.data.get("invalid_attributes", [])
|
||||
def get_invalid(cls, instance, compute=False):
|
||||
if compute:
|
||||
invalid = cls.get_invalid_attributes(context)
|
||||
|
||||
return invalid
|
||||
return cls.get_invalid_attributes(instance)
|
||||
else:
|
||||
return instance.data.get("invalid_attributes", [])
|
||||
|
||||
@classmethod
|
||||
def get_invalid_attributes(cls, context):
|
||||
def get_invalid_attributes(cls, instance):
|
||||
invalid_attributes = []
|
||||
for instance in context:
|
||||
# Filter publisable instances.
|
||||
if not instance.data["publish"]:
|
||||
|
||||
# Filter families.
|
||||
families = [instance.data["family"]]
|
||||
families += instance.data.get("families", [])
|
||||
families = set(families) & set(cls.attributes.keys())
|
||||
if not families:
|
||||
return []
|
||||
|
||||
# Get all attributes to validate.
|
||||
attributes = defaultdict(dict)
|
||||
for family in families:
|
||||
if family not in cls.attributes:
|
||||
# No attributes to validate for family
|
||||
continue
|
||||
|
||||
# Filter families.
|
||||
families = [instance.data["family"]]
|
||||
families += instance.data.get("families", [])
|
||||
families = list(set(families) & set(cls.attributes.keys()))
|
||||
if not families:
|
||||
for preset_attr, preset_value in cls.attributes[family].items():
|
||||
node_name, attribute_name = preset_attr.split(".", 1)
|
||||
attributes[node_name][attribute_name] = preset_value
|
||||
|
||||
if not attributes:
|
||||
return []
|
||||
|
||||
# Get invalid attributes.
|
||||
nodes = cmds.ls(long=True)
|
||||
for node in nodes:
|
||||
node_name = node.rsplit("|", 1)[-1].rsplit(":", 1)[-1]
|
||||
if node_name not in attributes:
|
||||
continue
|
||||
|
||||
# Get all attributes to validate.
|
||||
attributes = {}
|
||||
for family in families:
|
||||
for preset in cls.attributes[family]:
|
||||
[node_name, attribute_name] = preset.split(".")
|
||||
try:
|
||||
attributes[node_name].update(
|
||||
{attribute_name: cls.attributes[family][preset]}
|
||||
)
|
||||
except KeyError:
|
||||
attributes.update({
|
||||
node_name: {
|
||||
attribute_name: cls.attributes[family][preset]
|
||||
}
|
||||
})
|
||||
for attr_name, expected in attributes.items():
|
||||
|
||||
# Get invalid attributes.
|
||||
nodes = pm.ls()
|
||||
for node in nodes:
|
||||
name = node.name(stripNamespace=True)
|
||||
if name not in attributes.keys():
|
||||
# Skip if attribute does not exist
|
||||
if not cmds.attributeQuery(attr_name, node=node, exists=True):
|
||||
continue
|
||||
|
||||
presets_to_validate = attributes[name]
|
||||
for attribute in node.listAttr():
|
||||
names = [attribute.shortName(), attribute.longName()]
|
||||
attribute_name = list(
|
||||
set(names) & set(presets_to_validate.keys())
|
||||
plug = "{}.{}".format(node, attr_name)
|
||||
value = cmds.getAttr(plug)
|
||||
if value != expected:
|
||||
invalid_attributes.append(
|
||||
{
|
||||
"attribute": plug,
|
||||
"expected": expected,
|
||||
"current": value
|
||||
}
|
||||
)
|
||||
if attribute_name:
|
||||
expected = presets_to_validate[attribute_name[0]]
|
||||
if attribute.get() != expected:
|
||||
invalid_attributes.append(
|
||||
{
|
||||
"attribute": attribute,
|
||||
"expected": expected,
|
||||
"current": attribute.get()
|
||||
}
|
||||
)
|
||||
|
||||
context.data["invalid_attributes"] = invalid_attributes
|
||||
instance.data["invalid_attributes"] = invalid_attributes
|
||||
return invalid_attributes
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
invalid = cls.get_invalid(instance)
|
||||
for data in invalid:
|
||||
data["attribute"].set(data["expected"])
|
||||
node, attr = data["attribute"].split(".", 1)
|
||||
value = data["expected"]
|
||||
set_attribute(node=node, attribute=attr, value=value)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from maya import cmds
|
|||
from openpype.pipeline.publish import (
|
||||
RepairAction,
|
||||
ValidateContentsOrder,
|
||||
PublishValidationError
|
||||
)
|
||||
from openpype.hosts.maya.api.lib_rendersetup import (
|
||||
get_attr_overrides,
|
||||
|
|
@ -49,7 +50,6 @@ class ValidateFrameRange(pyblish.api.InstancePlugin):
|
|||
|
||||
frame_start_handle = int(context.data.get("frameStartHandle"))
|
||||
frame_end_handle = int(context.data.get("frameEndHandle"))
|
||||
handles = int(context.data.get("handles"))
|
||||
handle_start = int(context.data.get("handleStart"))
|
||||
handle_end = int(context.data.get("handleEnd"))
|
||||
frame_start = int(context.data.get("frameStart"))
|
||||
|
|
@ -66,8 +66,6 @@ class ValidateFrameRange(pyblish.api.InstancePlugin):
|
|||
assert frame_start_handle <= frame_end_handle, (
|
||||
"start frame is lower then end frame")
|
||||
|
||||
assert handles >= 0, ("handles cannot have negative values")
|
||||
|
||||
# compare with data on instance
|
||||
errors = []
|
||||
if [ef for ef in self.exclude_families
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import pymel.core as pc
|
||||
from maya import cmds
|
||||
import pyblish.api
|
||||
|
||||
import openpype.hosts.maya.api.action
|
||||
from openpype.hosts.maya.api.lib import maintained_selection
|
||||
from openpype.hosts.maya.api.lib import (
|
||||
maintained_selection,
|
||||
delete_after,
|
||||
undo_chunk,
|
||||
get_attribute,
|
||||
set_attribute
|
||||
)
|
||||
from openpype.pipeline.publish import (
|
||||
RepairAction,
|
||||
ValidateMeshOrder,
|
||||
|
|
@ -31,60 +37,68 @@ class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin):
|
|||
else:
|
||||
active = False
|
||||
|
||||
@classmethod
|
||||
def get_default_attributes(cls):
|
||||
# Get default arnold attribute values for mesh type.
|
||||
defaults = {}
|
||||
with delete_after() as tmp:
|
||||
transform = cmds.createNode("transform")
|
||||
tmp.append(transform)
|
||||
|
||||
mesh = cmds.createNode("mesh", parent=transform)
|
||||
for attr in cmds.listAttr(mesh, string="ai*"):
|
||||
plug = "{}.{}".format(mesh, attr)
|
||||
try:
|
||||
defaults[attr] = get_attribute(plug)
|
||||
except RuntimeError:
|
||||
cls.log.debug("Ignoring arnold attribute: {}".format(attr))
|
||||
|
||||
return defaults
|
||||
|
||||
@classmethod
|
||||
def get_invalid_attributes(cls, instance, compute=False):
|
||||
invalid = []
|
||||
|
||||
if compute:
|
||||
# Get default arnold attributes.
|
||||
temp_transform = pc.polyCube()[0]
|
||||
|
||||
for shape in pc.ls(instance, type="mesh"):
|
||||
for attr in temp_transform.getShape().listAttr():
|
||||
if not attr.attrName().startswith("ai"):
|
||||
continue
|
||||
meshes = cmds.ls(instance, type="mesh", long=True)
|
||||
if not meshes:
|
||||
return []
|
||||
|
||||
target_attr = pc.PyNode(
|
||||
"{}.{}".format(shape.name(), attr.attrName())
|
||||
)
|
||||
if attr.get() != target_attr.get():
|
||||
invalid.append(target_attr)
|
||||
|
||||
pc.delete(temp_transform)
|
||||
# Compare the values against the defaults
|
||||
defaults = cls.get_default_attributes()
|
||||
for mesh in meshes:
|
||||
for attr_name, default_value in defaults.items():
|
||||
plug = "{}.{}".format(mesh, attr_name)
|
||||
if get_attribute(plug) != default_value:
|
||||
invalid.append(plug)
|
||||
|
||||
instance.data["nondefault_arnold_attributes"] = invalid
|
||||
else:
|
||||
invalid.extend(instance.data["nondefault_arnold_attributes"])
|
||||
|
||||
return invalid
|
||||
return instance.data.get("nondefault_arnold_attributes", [])
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
invalid = []
|
||||
|
||||
for attr in cls.get_invalid_attributes(instance, compute=False):
|
||||
invalid.append(attr.node().name())
|
||||
|
||||
return invalid
|
||||
invalid_attrs = cls.get_invalid_attributes(instance, compute=False)
|
||||
invalid_nodes = set(attr.split(".", 1)[0] for attr in invalid_attrs)
|
||||
return sorted(invalid_nodes)
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
with maintained_selection():
|
||||
with pc.UndoChunk():
|
||||
temp_transform = pc.polyCube()[0]
|
||||
|
||||
with undo_chunk():
|
||||
defaults = cls.get_default_attributes()
|
||||
attributes = cls.get_invalid_attributes(
|
||||
instance, compute=False
|
||||
)
|
||||
for attr in attributes:
|
||||
source = pc.PyNode(
|
||||
"{}.{}".format(
|
||||
temp_transform.getShape(), attr.attrName()
|
||||
)
|
||||
node, attr_name = attr.split(".", 1)
|
||||
value = defaults[attr_name]
|
||||
set_attribute(
|
||||
node=node,
|
||||
attribute=attr_name,
|
||||
value=value
|
||||
)
|
||||
attr.set(source.get())
|
||||
|
||||
pc.delete(temp_transform)
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import pyblish.api
|
||||
import openpype.hosts.maya.api.action
|
||||
import math
|
||||
import maya.api.OpenMaya as om
|
||||
import pymel.core as pm
|
||||
|
||||
from six.moves import xrange
|
||||
|
||||
from maya import cmds
|
||||
import maya.api.OpenMaya as om
|
||||
import pyblish.api
|
||||
|
||||
import openpype.hosts.maya.api.action
|
||||
from openpype.pipeline.publish import ValidateMeshOrder
|
||||
|
||||
|
||||
|
|
@ -185,8 +186,7 @@ class GetOverlappingUVs(object):
|
|||
|
||||
center, radius = self._createBoundingCircle(meshfn)
|
||||
for i in xrange(meshfn.numPolygons): # noqa: F821
|
||||
rayb1, face1Orig, face1Vec = self._createRayGivenFace(
|
||||
meshfn, i)
|
||||
rayb1, face1Orig, face1Vec = self._createRayGivenFace(meshfn, i)
|
||||
if not rayb1:
|
||||
continue
|
||||
cui = center[2*i]
|
||||
|
|
@ -206,8 +206,8 @@ class GetOverlappingUVs(object):
|
|||
if (dsqr >= (ri + rj) * (ri + rj)):
|
||||
continue
|
||||
|
||||
rayb2, face2Orig, face2Vec = self._createRayGivenFace(
|
||||
meshfn, j)
|
||||
rayb2, face2Orig, face2Vec = self._createRayGivenFace(meshfn,
|
||||
j)
|
||||
if not rayb2:
|
||||
continue
|
||||
# Exclude the degenerate face
|
||||
|
|
@ -240,37 +240,45 @@ class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin):
|
|||
optional = True
|
||||
|
||||
@classmethod
|
||||
def _get_overlapping_uvs(cls, node):
|
||||
""" Check if mesh has overlapping UVs.
|
||||
def _get_overlapping_uvs(cls, mesh):
|
||||
"""Return overlapping UVs of mesh.
|
||||
|
||||
Args:
|
||||
mesh (str): Mesh node name
|
||||
|
||||
Returns:
|
||||
list: Overlapping uvs for the input mesh in all uv sets.
|
||||
|
||||
:param node: node to check
|
||||
:type node: str
|
||||
:returns: True is has overlapping UVs, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
ovl = GetOverlappingUVs()
|
||||
|
||||
# Store original uv set
|
||||
original_current_uv_set = cmds.polyUVSet(mesh,
|
||||
query=True,
|
||||
currentUVSet=True)[0]
|
||||
|
||||
overlapping_faces = []
|
||||
for i, uv in enumerate(pm.polyUVSet(node, q=1, auv=1)):
|
||||
pm.polyUVSet(node, cuv=1, uvSet=uv)
|
||||
overlapping_faces.extend(ovl._getOverlapUVFaces(str(node)))
|
||||
for uv_set in cmds.polyUVSet(mesh, query=True, allUVSets=True):
|
||||
cmds.polyUVSet(mesh, currentUVSet=True, uvSet=uv_set)
|
||||
overlapping_faces.extend(ovl._getOverlapUVFaces(mesh))
|
||||
|
||||
# Restore original uv set
|
||||
cmds.polyUVSet(mesh, currentUVSet=True, uvSet=original_current_uv_set)
|
||||
|
||||
return overlapping_faces
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance, compute=False):
|
||||
invalid = []
|
||||
|
||||
if compute:
|
||||
instance.data["overlapping_faces"] = []
|
||||
for node in pm.ls(instance, type="mesh"):
|
||||
invalid = []
|
||||
for node in cmds.ls(instance, type="mesh"):
|
||||
faces = cls._get_overlapping_uvs(node)
|
||||
invalid.extend(faces)
|
||||
# Store values for later.
|
||||
instance.data["overlapping_faces"].extend(faces)
|
||||
else:
|
||||
invalid.extend(instance.data["overlapping_faces"])
|
||||
|
||||
return invalid
|
||||
instance.data["overlapping_faces"] = invalid
|
||||
|
||||
return instance.data.get("overlapping_faces", [])
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import pymel.core as pm
|
||||
import maya.cmds as cmds
|
||||
|
||||
import pyblish.api
|
||||
|
|
@ -12,7 +11,7 @@ import openpype.hosts.maya.api.action
|
|||
|
||||
def get_namespace(node_name):
|
||||
# ensure only node's name (not parent path)
|
||||
node_name = node_name.rsplit("|")[-1]
|
||||
node_name = node_name.rsplit("|", 1)[-1]
|
||||
# ensure only namespace
|
||||
return node_name.rpartition(":")[0]
|
||||
|
||||
|
|
@ -45,13 +44,11 @@ class ValidateNoNamespace(pyblish.api.InstancePlugin):
|
|||
|
||||
invalid = cls.get_invalid(instance)
|
||||
|
||||
# Get nodes with pymel since we'll be renaming them
|
||||
# Since we don't want to keep checking the hierarchy
|
||||
# or full paths
|
||||
nodes = pm.ls(invalid)
|
||||
# Iterate over the nodes by long to short names to iterate the lowest
|
||||
# in hierarchy nodes first. This way we avoid having renamed parents
|
||||
# before renaming children nodes
|
||||
for node in sorted(invalid, key=len, reverse=True):
|
||||
|
||||
for node in nodes:
|
||||
namespace = node.namespace()
|
||||
if namespace:
|
||||
name = node.nodeName()
|
||||
node.rename(name[len(namespace):])
|
||||
node_name = node.rsplit("|", 1)[-1]
|
||||
node_name_without_namespace = node_name.rsplit(":")[-1]
|
||||
cmds.rename(node, node_name_without_namespace)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,22 @@
|
|||
import pymel.core as pc
|
||||
from collections import defaultdict
|
||||
|
||||
from maya import cmds
|
||||
|
||||
import pyblish.api
|
||||
|
||||
import openpype.hosts.maya.api.action
|
||||
from openpype.hosts.maya.api.lib import get_id, set_id
|
||||
from openpype.pipeline.publish import (
|
||||
RepairAction,
|
||||
ValidateContentsOrder,
|
||||
)
|
||||
|
||||
|
||||
def get_basename(node):
|
||||
"""Return node short name without namespace"""
|
||||
return node.rsplit("|", 1)[-1].rsplit(":", 1)[-1]
|
||||
|
||||
|
||||
class ValidateRigOutputIds(pyblish.api.InstancePlugin):
|
||||
"""Validate rig output ids.
|
||||
|
||||
|
|
@ -30,43 +38,48 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin):
|
|||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance, compute=False):
|
||||
invalid = cls.get_invalid_matches(instance, compute=compute)
|
||||
return [x["node"].longName() for x in invalid]
|
||||
invalid_matches = cls.get_invalid_matches(instance, compute=compute)
|
||||
return list(invalid_matches.keys())
|
||||
|
||||
@classmethod
|
||||
def get_invalid_matches(cls, instance, compute=False):
|
||||
invalid = []
|
||||
invalid = {}
|
||||
|
||||
if compute:
|
||||
out_set = next(x for x in instance if x.endswith("out_SET"))
|
||||
instance_nodes = pc.sets(out_set, query=True)
|
||||
instance_nodes.extend(
|
||||
[x.getShape() for x in instance_nodes if x.getShape()])
|
||||
|
||||
scene_nodes = pc.ls(type="transform") + pc.ls(type="mesh")
|
||||
instance_nodes = cmds.sets(out_set, query=True, nodesOnly=True)
|
||||
instance_nodes = cmds.ls(instance_nodes, long=True)
|
||||
for node in instance_nodes:
|
||||
shapes = cmds.listRelatives(node, shapes=True, fullPath=True)
|
||||
if shapes:
|
||||
instance_nodes.extend(shapes)
|
||||
|
||||
scene_nodes = cmds.ls(type="transform") + cmds.ls(type="mesh")
|
||||
scene_nodes = set(scene_nodes) - set(instance_nodes)
|
||||
|
||||
scene_nodes_by_basename = defaultdict(list)
|
||||
for node in scene_nodes:
|
||||
basename = get_basename(node)
|
||||
scene_nodes_by_basename[basename].append(node)
|
||||
|
||||
for instance_node in instance_nodes:
|
||||
matches = []
|
||||
basename = instance_node.name(stripNamespace=True)
|
||||
for scene_node in scene_nodes:
|
||||
if scene_node.name(stripNamespace=True) == basename:
|
||||
matches.append(scene_node)
|
||||
basename = get_basename(instance_node)
|
||||
if basename not in scene_nodes_by_basename:
|
||||
continue
|
||||
|
||||
if matches:
|
||||
ids = [instance_node.cbId.get()]
|
||||
ids.extend([x.cbId.get() for x in matches])
|
||||
ids = set(ids)
|
||||
matches = scene_nodes_by_basename[basename]
|
||||
|
||||
if len(ids) > 1:
|
||||
cls.log.error(
|
||||
"\"{}\" id mismatch to: {}".format(
|
||||
instance_node.longName(), matches
|
||||
)
|
||||
)
|
||||
invalid.append(
|
||||
{"node": instance_node, "matches": matches}
|
||||
ids = set(get_id(node) for node in matches)
|
||||
ids.add(get_id(instance_node))
|
||||
|
||||
if len(ids) > 1:
|
||||
cls.log.error(
|
||||
"\"{}\" id mismatch to: {}".format(
|
||||
instance_node.longName(), matches
|
||||
)
|
||||
)
|
||||
invalid[instance_node] = matches
|
||||
|
||||
instance.data["mismatched_output_ids"] = invalid
|
||||
else:
|
||||
|
|
@ -76,19 +89,21 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin):
|
|||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
invalid = cls.get_invalid_matches(instance)
|
||||
invalid_matches = cls.get_invalid_matches(instance)
|
||||
|
||||
multiple_ids_match = []
|
||||
for data in invalid:
|
||||
ids = [x.cbId.get() for x in data["matches"]]
|
||||
for instance_node, matches in invalid_matches.items():
|
||||
ids = set(get_id(node) for node in matches)
|
||||
|
||||
# If there are multiple scene ids matched, and error needs to be
|
||||
# raised for manual correction.
|
||||
if len(ids) > 1:
|
||||
multiple_ids_match.append(data)
|
||||
multiple_ids_match.append({"node": instance_node,
|
||||
"matches": matches})
|
||||
continue
|
||||
|
||||
data["node"].cbId.set(ids[0])
|
||||
id_to_set = next(iter(ids))
|
||||
set_id(instance_node, id_to_set, overwrite=True)
|
||||
|
||||
if multiple_ids_match:
|
||||
raise RuntimeError(
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ from openpype.client import (
|
|||
|
||||
from openpype.host import HostDirmap
|
||||
from openpype.tools.utils import host_tools
|
||||
from openpype.pipeline.workfile.workfile_template_builder import (
|
||||
TemplateProfileNotFound
|
||||
)
|
||||
from openpype.lib import (
|
||||
env_value_to_bool,
|
||||
Logger,
|
||||
|
|
@ -2684,7 +2687,10 @@ def start_workfile_template_builder():
|
|||
|
||||
# to avoid looping of the callback, remove it!
|
||||
log.info("Starting workfile template builder...")
|
||||
build_workfile_template(workfile_creation_enabled=True)
|
||||
try:
|
||||
build_workfile_template(workfile_creation_enabled=True)
|
||||
except TemplateProfileNotFound:
|
||||
log.warning("Template profile not found. Skipping...")
|
||||
|
||||
# remove callback since it would be duplicating the workfile
|
||||
nuke.removeOnCreate(start_workfile_template_builder, nodeClass="Root")
|
||||
|
|
|
|||
|
|
@ -208,6 +208,12 @@ class NukeCreator(NewCreator):
|
|||
|
||||
def collect_instances(self):
|
||||
cached_instances = _collect_and_cache_nodes(self)
|
||||
attr_def_keys = {
|
||||
attr_def.key
|
||||
for attr_def in self.get_instance_attr_defs()
|
||||
}
|
||||
attr_def_keys.discard(None)
|
||||
|
||||
for (node, data) in cached_instances[self.identifier]:
|
||||
created_instance = CreatedInstance.from_existing(
|
||||
data, self
|
||||
|
|
@ -215,6 +221,12 @@ class NukeCreator(NewCreator):
|
|||
created_instance.transient_data["node"] = node
|
||||
self._add_instance_to_context(created_instance)
|
||||
|
||||
for key in (
|
||||
set(created_instance["creator_attributes"].keys())
|
||||
- attr_def_keys
|
||||
):
|
||||
created_instance["creator_attributes"].pop(key)
|
||||
|
||||
def update_instances(self, update_list):
|
||||
for created_inst, _changes in update_list:
|
||||
instance_node = created_inst.transient_data["node"]
|
||||
|
|
@ -301,8 +313,11 @@ class NukeWriteCreator(NukeCreator):
|
|||
def get_instance_attr_defs(self):
|
||||
attr_defs = [
|
||||
self._get_render_target_enum(),
|
||||
self._get_reviewable_bool()
|
||||
]
|
||||
# add reviewable attribute
|
||||
if "reviewable" in self.instance_attributes:
|
||||
attr_defs.append(self._get_reviewable_bool())
|
||||
|
||||
return attr_defs
|
||||
|
||||
def _get_render_target_enum(self):
|
||||
|
|
@ -322,7 +337,7 @@ class NukeWriteCreator(NukeCreator):
|
|||
def _get_reviewable_bool(self):
|
||||
return BoolDef(
|
||||
"review",
|
||||
default=("reviewable" in self.instance_attributes),
|
||||
default=True,
|
||||
label="Review"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -219,14 +219,17 @@ class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin):
|
|||
|
||||
# fix the problem of z_order for backdrops
|
||||
self._fix_z_order(placeholder)
|
||||
self._imprint_siblings(placeholder)
|
||||
|
||||
if placeholder.data.get("keep_placeholder"):
|
||||
self._imprint_siblings(placeholder)
|
||||
|
||||
if placeholder.data["nb_children"] == 0:
|
||||
# save initial nodes positions and dimensions, update them
|
||||
# and set inputs and outputs of loaded nodes
|
||||
if placeholder.data.get("keep_placeholder"):
|
||||
self._imprint_inits()
|
||||
self._update_nodes(placeholder, nuke.allNodes(), nodes_loaded)
|
||||
|
||||
self._imprint_inits()
|
||||
self._update_nodes(placeholder, nuke.allNodes(), nodes_loaded)
|
||||
self._set_loaded_connections(placeholder)
|
||||
|
||||
elif placeholder.data["siblings"]:
|
||||
|
|
@ -629,14 +632,18 @@ class NukePlaceholderCreatePlugin(
|
|||
|
||||
# fix the problem of z_order for backdrops
|
||||
self._fix_z_order(placeholder)
|
||||
self._imprint_siblings(placeholder)
|
||||
|
||||
if placeholder.data.get("keep_placeholder"):
|
||||
self._imprint_siblings(placeholder)
|
||||
|
||||
if placeholder.data["nb_children"] == 0:
|
||||
# save initial nodes positions and dimensions, update them
|
||||
# and set inputs and outputs of created nodes
|
||||
|
||||
self._imprint_inits()
|
||||
self._update_nodes(placeholder, nuke.allNodes(), nodes_created)
|
||||
if placeholder.data.get("keep_placeholder"):
|
||||
self._imprint_inits()
|
||||
self._update_nodes(placeholder, nuke.allNodes(), nodes_created)
|
||||
|
||||
self._set_created_connections(placeholder)
|
||||
|
||||
elif placeholder.data["siblings"]:
|
||||
|
|
|
|||
|
|
@ -63,13 +63,6 @@ class CreateWriteImage(napi.NukeWriteCreator):
|
|||
default=nuke.frame()
|
||||
)
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
attr_defs = [
|
||||
self._get_render_target_enum(),
|
||||
self._get_reviewable_bool()
|
||||
]
|
||||
return attr_defs
|
||||
|
||||
def create_instance_node(self, subset_name, instance_data):
|
||||
linked_knobs_ = []
|
||||
if "use_range_limit" in self.instance_attributes:
|
||||
|
|
|
|||
|
|
@ -41,13 +41,6 @@ class CreateWritePrerender(napi.NukeWriteCreator):
|
|||
]
|
||||
return attr_defs
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
attr_defs = [
|
||||
self._get_render_target_enum(),
|
||||
self._get_reviewable_bool()
|
||||
]
|
||||
return attr_defs
|
||||
|
||||
def create_instance_node(self, subset_name, instance_data):
|
||||
linked_knobs_ = []
|
||||
if "use_range_limit" in self.instance_attributes:
|
||||
|
|
|
|||
|
|
@ -38,13 +38,6 @@ class CreateWriteRender(napi.NukeWriteCreator):
|
|||
]
|
||||
return attr_defs
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
attr_defs = [
|
||||
self._get_render_target_enum(),
|
||||
self._get_reviewable_bool()
|
||||
]
|
||||
return attr_defs
|
||||
|
||||
def create_instance_node(self, subset_name, instance_data):
|
||||
# add fpath_template
|
||||
write_data = {
|
||||
|
|
|
|||
|
|
@ -74,8 +74,7 @@ class SetFrameRangeWithHandlesLoader(load.LoaderPlugin):
|
|||
return
|
||||
|
||||
# Include handles
|
||||
handles = version_data.get("handles", 0)
|
||||
start -= handles
|
||||
end += handles
|
||||
start -= version_data.get("handleStart", 0)
|
||||
end += version_data.get("handleEnd", 0)
|
||||
|
||||
lib.update_frame_range(start, end)
|
||||
|
|
|
|||
|
|
@ -138,7 +138,6 @@ class LinkAsGroup(load.LoaderPlugin):
|
|||
"version": version_doc.get("name"),
|
||||
"colorspace": version_data.get("colorspace"),
|
||||
"source": version_data.get("source"),
|
||||
"handles": version_data.get("handles"),
|
||||
"fps": version_data.get("fps"),
|
||||
"author": version_data.get("author")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -49,8 +49,6 @@ class CollectContextData(pyblish.api.ContextPlugin):
|
|||
"resolutionHeight": resolution_height,
|
||||
"pixelAspect": pixel_aspect,
|
||||
|
||||
# backward compatibility handles
|
||||
"handles": handle_start,
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"step": 1,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ class CollectGizmo(pyblish.api.InstancePlugin):
|
|||
|
||||
# Add version data to instance
|
||||
version_data = {
|
||||
"handles": handle_start,
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"frameStart": first_frame + handle_start,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ class CollectModel(pyblish.api.InstancePlugin):
|
|||
|
||||
# Add version data to instance
|
||||
version_data = {
|
||||
"handles": handle_start,
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"frameStart": first_frame + handle_start,
|
||||
|
|
|
|||
|
|
@ -103,7 +103,6 @@ class CollectNukeReads(pyblish.api.InstancePlugin):
|
|||
|
||||
# Add version data to instance
|
||||
version_data = {
|
||||
"handles": handle_start,
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"frameStart": first_frame + handle_start,
|
||||
|
|
@ -123,7 +122,8 @@ class CollectNukeReads(pyblish.api.InstancePlugin):
|
|||
"frameStart": first_frame,
|
||||
"frameEnd": last_frame,
|
||||
"colorspace": colorspace,
|
||||
"handles": int(asset_doc["data"].get("handles", 0)),
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"step": 1,
|
||||
"fps": int(nuke.root()['fps'].value())
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ import openpype.hosts.nuke.api.lib as nlib
|
|||
from openpype.pipeline.publish import (
|
||||
ValidateContentsOrder,
|
||||
PublishXmlValidationError,
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
|
||||
|
||||
class SelectInvalidInstances(pyblish.api.Action):
|
||||
"""Select invalid instances in Outliner."""
|
||||
|
||||
|
|
@ -92,7 +92,10 @@ class RepairSelectInvalidInstances(pyblish.api.Action):
|
|||
nlib.set_node_data(node, nlib.INSTANCE_DATA_KNOB, node_data)
|
||||
|
||||
|
||||
class ValidateCorrectAssetName(pyblish.api.InstancePlugin):
|
||||
class ValidateCorrectAssetName(
|
||||
pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin
|
||||
):
|
||||
"""Validator to check if instance asset match context asset.
|
||||
|
||||
When working in per-shot style you always publish data in context of
|
||||
|
|
@ -111,6 +114,9 @@ class ValidateCorrectAssetName(pyblish.api.InstancePlugin):
|
|||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
asset = instance.data.get("asset")
|
||||
context_asset = instance.context.data["assetEntity"]["name"]
|
||||
node = instance.data["transientData"]["node"]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import nuke
|
||||
import pyblish
|
||||
from openpype.hosts.nuke import api as napi
|
||||
from openpype.pipeline import PublishXmlValidationError
|
||||
|
||||
from openpype.pipeline.publish import (
|
||||
ValidateContentsOrder,
|
||||
PublishXmlValidationError,
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
|
||||
class SelectCenterInNodeGraph(pyblish.api.Action):
|
||||
"""
|
||||
|
|
@ -46,12 +50,15 @@ class SelectCenterInNodeGraph(pyblish.api.Action):
|
|||
nuke.zoom(2, [min(all_xC), min(all_yC)])
|
||||
|
||||
|
||||
class ValidateBackdrop(pyblish.api.InstancePlugin):
|
||||
class ValidateBackdrop(
|
||||
pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin
|
||||
):
|
||||
""" Validate amount of nodes on backdrop node in case user
|
||||
forgotten to add nodes above the publishing backdrop node.
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
order = ValidateContentsOrder
|
||||
optional = True
|
||||
families = ["nukenodes"]
|
||||
label = "Validate Backdrop"
|
||||
|
|
@ -59,6 +66,9 @@ class ValidateBackdrop(pyblish.api.InstancePlugin):
|
|||
actions = [SelectCenterInNodeGraph]
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
child_nodes = instance.data["transientData"]["childNodes"]
|
||||
connections_out = instance.data["transientData"]["nodeConnectionsOut"]
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class ValidateScriptAttributes(
|
|||
|
||||
order = pyblish.api.ValidatorOrder + 0.1
|
||||
families = ["workfile"]
|
||||
label = "Validatte script attributes"
|
||||
label = "Validate script attributes"
|
||||
hosts = ["nuke"]
|
||||
optional = True
|
||||
actions = [RepairAction]
|
||||
|
|
|
|||
|
|
@ -504,14 +504,9 @@ def set_context_settings(project_name, asset_doc):
|
|||
print("Frame range was not found!")
|
||||
return
|
||||
|
||||
handles = asset_doc["data"].get("handles") or 0
|
||||
handle_start = asset_doc["data"].get("handleStart")
|
||||
handle_end = asset_doc["data"].get("handleEnd")
|
||||
|
||||
if handle_start is None or handle_end is None:
|
||||
handle_start = handles
|
||||
handle_end = handles
|
||||
|
||||
# Always start from 0 Mark In and set only Mark Out
|
||||
mark_in = 0
|
||||
mark_out = mark_in + (frame_end - frame_start) + handle_start + handle_end
|
||||
|
|
|
|||
|
|
@ -256,17 +256,18 @@ class TemplatesDict(object):
|
|||
elif isinstance(templates, dict):
|
||||
self._raw_templates = copy.deepcopy(templates)
|
||||
self._templates = templates
|
||||
self._objected_templates = self.create_ojected_templates(templates)
|
||||
self._objected_templates = self.create_objected_templates(
|
||||
templates)
|
||||
else:
|
||||
raise TypeError("<{}> argument must be a dict, not {}.".format(
|
||||
self.__class__.__name__, str(type(templates))
|
||||
))
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.templates[key]
|
||||
return self.objected_templates[key]
|
||||
|
||||
def get(self, key, *args, **kwargs):
|
||||
return self.templates.get(key, *args, **kwargs)
|
||||
return self.objected_templates.get(key, *args, **kwargs)
|
||||
|
||||
@property
|
||||
def raw_templates(self):
|
||||
|
|
@ -280,8 +281,21 @@ class TemplatesDict(object):
|
|||
def objected_templates(self):
|
||||
return self._objected_templates
|
||||
|
||||
@classmethod
|
||||
def create_ojected_templates(cls, templates):
|
||||
def _create_template_object(self, template):
|
||||
"""Create template object from a template string.
|
||||
|
||||
Separated into method to give option change class of templates.
|
||||
|
||||
Args:
|
||||
template (str): Template string.
|
||||
|
||||
Returns:
|
||||
StringTemplate: Object of template.
|
||||
"""
|
||||
|
||||
return StringTemplate(template)
|
||||
|
||||
def create_objected_templates(self, templates):
|
||||
if not isinstance(templates, dict):
|
||||
raise TypeError("Expected dict object, got {}".format(
|
||||
str(type(templates))
|
||||
|
|
@ -297,7 +311,7 @@ class TemplatesDict(object):
|
|||
for key in tuple(item.keys()):
|
||||
value = item[key]
|
||||
if isinstance(value, six.string_types):
|
||||
item[key] = StringTemplate(value)
|
||||
item[key] = self._create_template_object(value)
|
||||
elif isinstance(value, dict):
|
||||
inner_queue.append(value)
|
||||
return objected_templates
|
||||
|
|
|
|||
|
|
@ -142,10 +142,8 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline):
|
|||
|
||||
job_info.Pool = instance.data.get("primaryPool")
|
||||
job_info.SecondaryPool = instance.data.get("secondaryPool")
|
||||
job_info.ChunkSize = instance.data.get("chunkSize", 10)
|
||||
job_info.Comment = context.data.get("comment")
|
||||
job_info.Priority = instance.data.get("priority", self.priority)
|
||||
job_info.FramesPerTask = instance.data.get("framesPerTask", 1)
|
||||
|
||||
if self.group != "none" and self.group:
|
||||
job_info.Group = self.group
|
||||
|
|
|
|||
|
|
@ -76,6 +76,11 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin,
|
|||
"use_gpu",
|
||||
default=cls.use_gpu,
|
||||
label="Use GPU"
|
||||
),
|
||||
BoolDef(
|
||||
"suspend_publish",
|
||||
default=False,
|
||||
label="Suspend publish"
|
||||
)
|
||||
]
|
||||
|
||||
|
|
@ -87,6 +92,10 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin,
|
|||
instance.data["attributeValues"] = self.get_attr_values_from_data(
|
||||
instance.data)
|
||||
|
||||
# add suspend_publish attributeValue to instance data
|
||||
instance.data["suspend_publish"] = instance.data["attributeValues"][
|
||||
"suspend_publish"]
|
||||
|
||||
instance.data["toBeRenderedOn"] = "deadline"
|
||||
families = instance.data["families"]
|
||||
|
||||
|
|
|
|||
|
|
@ -944,17 +944,28 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
# we cannot attach AOVs to other subsets as we consider every
|
||||
# AOV subset of its own.
|
||||
|
||||
config = instance.data["colorspaceConfig"]
|
||||
additional_data = {
|
||||
"renderProducts": instance.data["renderProducts"],
|
||||
"colorspaceConfig": instance.data["colorspaceConfig"],
|
||||
"display": instance.data["colorspaceDisplay"],
|
||||
"view": instance.data["colorspaceView"],
|
||||
"colorspaceTemplate": config.replace(
|
||||
str(context.data["anatomy"].roots["work"]), "{root[work]}"
|
||||
)
|
||||
"view": instance.data["colorspaceView"]
|
||||
}
|
||||
|
||||
# Get templated path from absolute config path.
|
||||
anatomy = instance.context.data["anatomy"]
|
||||
colorspaceTemplate = instance.data["colorspaceConfig"]
|
||||
success, rootless_staging_dir = (
|
||||
anatomy.find_root_template_from_path(colorspaceTemplate)
|
||||
)
|
||||
if success:
|
||||
colorspaceTemplate = rootless_staging_dir
|
||||
else:
|
||||
self.log.warning((
|
||||
"Could not find root path for remapping \"{}\"."
|
||||
" This may cause issues on farm."
|
||||
).format(colorspaceTemplate))
|
||||
additional_data["colorspaceTemplate"] = colorspaceTemplate
|
||||
|
||||
if len(data.get("attachTo")) > 0:
|
||||
assert len(data.get("expectedFiles")[0].keys()) == 1, (
|
||||
"attaching multiple AOVs or renderable cameras to "
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from openpype.client import get_project
|
|||
from openpype.lib.path_templates import (
|
||||
TemplateUnsolved,
|
||||
TemplateResult,
|
||||
StringTemplate,
|
||||
TemplatesDict,
|
||||
FormatObject,
|
||||
)
|
||||
|
|
@ -606,6 +607,32 @@ class AnatomyTemplateResult(TemplateResult):
|
|||
return self.__class__(tmp, self.rootless)
|
||||
|
||||
|
||||
class AnatomyStringTemplate(StringTemplate):
|
||||
"""String template which has access to anatomy."""
|
||||
|
||||
def __init__(self, anatomy_templates, template):
|
||||
self.anatomy_templates = anatomy_templates
|
||||
super(AnatomyStringTemplate, self).__init__(template)
|
||||
|
||||
def format(self, data):
|
||||
"""Format template and add 'root' key to data if not available.
|
||||
|
||||
Args:
|
||||
data (dict[str, Any]): Formatting data for template.
|
||||
|
||||
Returns:
|
||||
AnatomyTemplateResult: Formatting result.
|
||||
"""
|
||||
|
||||
anatomy_templates = self.anatomy_templates
|
||||
if not data.get("root"):
|
||||
data = copy.deepcopy(data)
|
||||
data["root"] = anatomy_templates.anatomy.roots
|
||||
result = StringTemplate.format(self, data)
|
||||
rootless_path = anatomy_templates.rootless_path_from_result(result)
|
||||
return AnatomyTemplateResult(result, rootless_path)
|
||||
|
||||
|
||||
class AnatomyTemplates(TemplatesDict):
|
||||
inner_key_pattern = re.compile(r"(\{@.*?[^{}0]*\})")
|
||||
inner_key_name_pattern = re.compile(r"\{@(.*?[^{}0]*)\}")
|
||||
|
|
@ -615,12 +642,6 @@ class AnatomyTemplates(TemplatesDict):
|
|||
self.anatomy = anatomy
|
||||
self.loaded_project = None
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.templates[key]
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self.templates.get(key, default)
|
||||
|
||||
def reset(self):
|
||||
self._raw_templates = None
|
||||
self._templates = None
|
||||
|
|
@ -655,12 +676,7 @@ class AnatomyTemplates(TemplatesDict):
|
|||
def _format_value(self, value, data):
|
||||
if isinstance(value, RootItem):
|
||||
return self._solve_dict(value, data)
|
||||
|
||||
result = super(AnatomyTemplates, self)._format_value(value, data)
|
||||
if isinstance(result, TemplateResult):
|
||||
rootless_path = self._rootless_path(result, data)
|
||||
result = AnatomyTemplateResult(result, rootless_path)
|
||||
return result
|
||||
return super(AnatomyTemplates, self)._format_value(value, data)
|
||||
|
||||
def set_templates(self, templates):
|
||||
if not templates:
|
||||
|
|
@ -689,10 +705,13 @@ class AnatomyTemplates(TemplatesDict):
|
|||
|
||||
solved_templates = self.solve_template_inner_links(templates)
|
||||
self._templates = solved_templates
|
||||
self._objected_templates = self.create_ojected_templates(
|
||||
self._objected_templates = self.create_objected_templates(
|
||||
solved_templates
|
||||
)
|
||||
|
||||
def _create_template_object(self, template):
|
||||
return AnatomyStringTemplate(self, template)
|
||||
|
||||
def default_templates(self):
|
||||
"""Return default templates data with solved inner keys."""
|
||||
return self.solve_template_inner_links(
|
||||
|
|
@ -886,7 +905,8 @@ class AnatomyTemplates(TemplatesDict):
|
|||
|
||||
return keys_by_subkey
|
||||
|
||||
def _dict_to_subkeys_list(self, subdict, pre_keys=None):
|
||||
@classmethod
|
||||
def _dict_to_subkeys_list(cls, subdict, pre_keys=None):
|
||||
if pre_keys is None:
|
||||
pre_keys = []
|
||||
output = []
|
||||
|
|
@ -895,7 +915,7 @@ class AnatomyTemplates(TemplatesDict):
|
|||
result = list(pre_keys)
|
||||
result.append(key)
|
||||
if isinstance(value, dict):
|
||||
for item in self._dict_to_subkeys_list(value, result):
|
||||
for item in cls._dict_to_subkeys_list(value, result):
|
||||
output.append(item)
|
||||
else:
|
||||
output.append(result)
|
||||
|
|
@ -908,7 +928,17 @@ class AnatomyTemplates(TemplatesDict):
|
|||
return {key_list[0]: value}
|
||||
return {key_list[0]: self._keys_to_dicts(key_list[1:], value)}
|
||||
|
||||
def _rootless_path(self, result, final_data):
|
||||
@classmethod
|
||||
def rootless_path_from_result(cls, result):
|
||||
"""Calculate rootless path from formatting result.
|
||||
|
||||
Args:
|
||||
result (TemplateResult): Result of StringTemplate formatting.
|
||||
|
||||
Returns:
|
||||
str: Rootless path if result contains one of anatomy roots.
|
||||
"""
|
||||
|
||||
used_values = result.used_values
|
||||
missing_keys = result.missing_keys
|
||||
template = result.template
|
||||
|
|
@ -924,7 +954,7 @@ class AnatomyTemplates(TemplatesDict):
|
|||
if "root" in invalid_type:
|
||||
return
|
||||
|
||||
root_keys = self._dict_to_subkeys_list({"root": used_values["root"]})
|
||||
root_keys = cls._dict_to_subkeys_list({"root": used_values["root"]})
|
||||
if not root_keys:
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -596,7 +596,14 @@ class AttributeValues(object):
|
|||
self[_key] = _value
|
||||
|
||||
def pop(self, key, default=None):
|
||||
return self._data.pop(key, default)
|
||||
value = self._data.pop(key, default)
|
||||
# Remove attribute definition if is 'UnknownDef'
|
||||
# - gives option to get rid of unknown values
|
||||
attr_def = self._attr_defs_by_key.get(key)
|
||||
if isinstance(attr_def, UnknownDef):
|
||||
self._attr_defs_by_key.pop(key)
|
||||
self._attr_defs.remove(attr_def)
|
||||
return value
|
||||
|
||||
def reset_values(self):
|
||||
self._data = {}
|
||||
|
|
|
|||
|
|
@ -354,6 +354,61 @@ def publish_plugins_discover(paths=None):
|
|||
return result
|
||||
|
||||
|
||||
def _get_plugin_settings(host_name, project_settings, plugin, log):
|
||||
"""Get plugin settings based on host name and plugin name.
|
||||
|
||||
Args:
|
||||
host_name (str): Name of host.
|
||||
project_settings (dict[str, Any]): Project settings.
|
||||
plugin (pyliblish.Plugin): Plugin where settings are applied.
|
||||
log (logging.Logger): Logger to log messages.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Plugin settings {'attribute': 'value'}.
|
||||
"""
|
||||
|
||||
# Use project settings from host name category when available
|
||||
try:
|
||||
return (
|
||||
project_settings
|
||||
[host_name]
|
||||
["publish"]
|
||||
[plugin.__name__]
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Settings category determined from path
|
||||
# - usually path is './<category>/plugins/publish/<plugin file>'
|
||||
# - category can be host name of addon name ('maya', 'deadline', ...)
|
||||
filepath = os.path.normpath(inspect.getsourcefile(plugin))
|
||||
|
||||
split_path = filepath.rsplit(os.path.sep, 5)
|
||||
if len(split_path) < 4:
|
||||
log.warning(
|
||||
'plugin path too short to extract host {}'.format(filepath)
|
||||
)
|
||||
return {}
|
||||
|
||||
category_from_file = split_path[-4]
|
||||
plugin_kind = split_path[-2]
|
||||
|
||||
# TODO: change after all plugins are moved one level up
|
||||
if category_from_file == "openpype":
|
||||
category_from_file = "global"
|
||||
|
||||
try:
|
||||
return (
|
||||
project_settings
|
||||
[category_from_file]
|
||||
[plugin_kind]
|
||||
[plugin.__name__]
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
def filter_pyblish_plugins(plugins):
|
||||
"""Pyblish plugin filter which applies OpenPype settings.
|
||||
|
||||
|
|
@ -372,21 +427,21 @@ def filter_pyblish_plugins(plugins):
|
|||
# TODO: Don't use host from 'pyblish.api' but from defined host by us.
|
||||
# - kept becau on farm is probably used host 'shell' which propably
|
||||
# affect how settings are applied there
|
||||
host = pyblish.api.current_host()
|
||||
host_name = pyblish.api.current_host()
|
||||
project_name = os.environ.get("AVALON_PROJECT")
|
||||
|
||||
project_setting = get_project_settings(project_name)
|
||||
project_settings = get_project_settings(project_name)
|
||||
system_settings = get_system_settings()
|
||||
|
||||
# iterate over plugins
|
||||
for plugin in plugins[:]:
|
||||
# Apply settings to plugins
|
||||
if hasattr(plugin, "apply_settings"):
|
||||
# Use classmethod 'apply_settings'
|
||||
# - can be used to target settings from custom settings place
|
||||
# - skip default behavior when successful
|
||||
try:
|
||||
# Use classmethod 'apply_settings'
|
||||
# - can be used to target settings from custom settings place
|
||||
# - skip default behavior when successful
|
||||
plugin.apply_settings(project_setting, system_settings)
|
||||
continue
|
||||
plugin.apply_settings(project_settings, system_settings)
|
||||
|
||||
except Exception:
|
||||
log.warning(
|
||||
|
|
@ -395,53 +450,20 @@ def filter_pyblish_plugins(plugins):
|
|||
).format(plugin.__name__),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
try:
|
||||
config_data = (
|
||||
project_setting
|
||||
[host]
|
||||
["publish"]
|
||||
[plugin.__name__]
|
||||
else:
|
||||
# Automated
|
||||
plugin_settins = _get_plugin_settings(
|
||||
host_name, project_settings, plugin, log
|
||||
)
|
||||
except KeyError:
|
||||
# host determined from path
|
||||
file = os.path.normpath(inspect.getsourcefile(plugin))
|
||||
file = os.path.normpath(file)
|
||||
|
||||
split_path = file.split(os.path.sep)
|
||||
if len(split_path) < 4:
|
||||
log.warning(
|
||||
'plugin path too short to extract host {}'.format(file)
|
||||
)
|
||||
continue
|
||||
|
||||
host_from_file = split_path[-4]
|
||||
plugin_kind = split_path[-2]
|
||||
|
||||
# TODO: change after all plugins are moved one level up
|
||||
if host_from_file == "openpype":
|
||||
host_from_file = "global"
|
||||
|
||||
try:
|
||||
config_data = (
|
||||
project_setting
|
||||
[host_from_file]
|
||||
[plugin_kind]
|
||||
[plugin.__name__]
|
||||
)
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
for option, value in config_data.items():
|
||||
if option == "enabled" and value is False:
|
||||
log.info('removing plugin {}'.format(plugin.__name__))
|
||||
plugins.remove(plugin)
|
||||
else:
|
||||
log.info('setting {}:{} on plugin {}'.format(
|
||||
for option, value in plugin_settins.items():
|
||||
log.info("setting {}:{} on plugin {}".format(
|
||||
option, value, plugin.__name__))
|
||||
|
||||
setattr(plugin, option, value)
|
||||
|
||||
# Remove disabled plugins
|
||||
if getattr(plugin, "enabled", True) is False:
|
||||
plugins.remove(plugin)
|
||||
|
||||
|
||||
def find_close_plugin(close_plugin_name, log):
|
||||
if close_plugin_name:
|
||||
|
|
|
|||
|
|
@ -72,24 +72,9 @@ class CollectContextEntities(pyblish.api.ContextPlugin):
|
|||
context.data["frameStart"] = frame_start
|
||||
context.data["frameEnd"] = frame_end
|
||||
|
||||
handles = data.get("handles") or 0
|
||||
handle_start = data.get("handleStart")
|
||||
if handle_start is None:
|
||||
handle_start = handles
|
||||
self.log.info((
|
||||
"Key \"handleStart\" is not set."
|
||||
" Using value from \"handles\" key {}."
|
||||
).format(handle_start))
|
||||
handle_start = data.get("handleStart") or 0
|
||||
handle_end = data.get("handleEnd") or 0
|
||||
|
||||
handle_end = data.get("handleEnd")
|
||||
if handle_end is None:
|
||||
handle_end = handles
|
||||
self.log.info((
|
||||
"Key \"handleEnd\" is not set."
|
||||
" Using value from \"handles\" key {}."
|
||||
).format(handle_end))
|
||||
|
||||
context.data["handles"] = int(handles)
|
||||
context.data["handleStart"] = int(handle_start)
|
||||
context.data["handleEnd"] = int(handle_end)
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,9 @@ class CollectOtioReview(pyblish.api.InstancePlugin):
|
|||
otio_review_clips.append(otio_gap)
|
||||
|
||||
if otio_review_clips:
|
||||
instance.data["label"] += " (review)"
|
||||
# add review track to instance and change label to reflect it
|
||||
label = instance.data.get("label", instance.data["subset"])
|
||||
instance.data["label"] = label + " (review)"
|
||||
instance.data["families"] += ["review", "ftrack"]
|
||||
instance.data["otioReviewClips"] = otio_review_clips
|
||||
self.log.info(
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ class ExtractBurnin(publish.Extractor):
|
|||
"webpublisher",
|
||||
"aftereffects",
|
||||
"photoshop",
|
||||
"flame"
|
||||
"flame",
|
||||
"houdini"
|
||||
# "resolve"
|
||||
]
|
||||
|
||||
|
|
@ -78,9 +79,10 @@ class ExtractBurnin(publish.Extractor):
|
|||
self.log.warning("No profiles present for create burnin")
|
||||
return
|
||||
|
||||
# QUESTION what is this for and should we raise an exception?
|
||||
if "representations" not in instance.data:
|
||||
raise RuntimeError("Burnin needs already created mov to work on.")
|
||||
if not instance.data.get("representations"):
|
||||
self.log.info(
|
||||
"Instance does not have filled representations. Skipping")
|
||||
return
|
||||
|
||||
self.main_process(instance)
|
||||
|
||||
|
|
@ -456,12 +458,6 @@ class ExtractBurnin(publish.Extractor):
|
|||
frame_end = 1
|
||||
frame_end = int(frame_end)
|
||||
|
||||
handles = instance.data.get("handles")
|
||||
if handles is None:
|
||||
handles = context.data.get("handles")
|
||||
if handles is None:
|
||||
handles = 0
|
||||
|
||||
handle_start = instance.data.get("handleStart")
|
||||
if handle_start is None:
|
||||
handle_start = context.data.get("handleStart")
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
"nuke",
|
||||
"maya",
|
||||
"blender",
|
||||
"houdini",
|
||||
"shell",
|
||||
"hiero",
|
||||
"premiere",
|
||||
|
|
|
|||
|
|
@ -912,7 +912,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
|||
|
||||
# Include optional data if present in
|
||||
optionals = [
|
||||
"frameStart", "frameEnd", "step", "handles",
|
||||
"frameStart", "frameEnd", "step",
|
||||
"handleEnd", "handleStart", "sourceHashes"
|
||||
]
|
||||
for key in optionals:
|
||||
|
|
|
|||
|
|
@ -987,7 +987,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
|
||||
# Include optional data if present in
|
||||
optionals = [
|
||||
"frameStart", "frameEnd", "step", "handles",
|
||||
"frameStart", "frameEnd", "step",
|
||||
"handleEnd", "handleStart", "sourceHashes"
|
||||
]
|
||||
for key in optionals:
|
||||
|
|
|
|||
|
|
@ -253,13 +253,14 @@
|
|||
{
|
||||
"families": [],
|
||||
"hosts": [
|
||||
"maya"
|
||||
"maya",
|
||||
"houdini"
|
||||
],
|
||||
"task_types": [],
|
||||
"task_names": [],
|
||||
"subsets": [],
|
||||
"burnins": {
|
||||
"maya_burnin": {
|
||||
"focal_length_burnin": {
|
||||
"TOP_LEFT": "{yy}-{mm}-{dd}",
|
||||
"TOP_CENTERED": "{focalLength:.2f} mm",
|
||||
"TOP_RIGHT": "{anatomy[version]}",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@
|
|||
"floatLut": "linear",
|
||||
"logLut": "Cineon",
|
||||
"viewerLut": "sRGB",
|
||||
"thumbnailLut": "sRGB"
|
||||
"thumbnailLut": "sRGB",
|
||||
"monitorOutLut": "sRGB"
|
||||
},
|
||||
"regexInputs": {
|
||||
"inputs": [
|
||||
|
|
|
|||
|
|
@ -1047,6 +1047,10 @@
|
|||
125,
|
||||
255
|
||||
]
|
||||
},
|
||||
"reference_loader": {
|
||||
"namespace": "{asset_name}_{subset}_##",
|
||||
"group_name": "_GRP"
|
||||
}
|
||||
},
|
||||
"workfile_build": {
|
||||
|
|
@ -1140,6 +1144,10 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"include_handles": {
|
||||
"include_handles_default": false,
|
||||
"per_task_type": []
|
||||
},
|
||||
"templated_workfile_build": {
|
||||
"profiles": []
|
||||
},
|
||||
|
|
|
|||
|
|
@ -363,6 +363,11 @@
|
|||
"optional": true,
|
||||
"active": true
|
||||
},
|
||||
"ValidateBackdrop": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
"active": true
|
||||
},
|
||||
"ValidateScript": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
|
|
|
|||
|
|
@ -42,10 +42,19 @@
|
|||
"nuke-default": "nuke-default"
|
||||
},
|
||||
{
|
||||
"aces_1.0.3": "aces_1.0.3"
|
||||
"aces_1.0.3": "aces_1.0.3 (12)"
|
||||
},
|
||||
{
|
||||
"aces_1.1": "aces_1.1"
|
||||
"aces_1.1": "aces_1.1 (12, 13)"
|
||||
},
|
||||
{
|
||||
"aces_1.2": "aces_1.2 (13, 14)"
|
||||
},
|
||||
{
|
||||
"studio-config-v1.0.0_aces-v1.3_ocio-v2.1": "studio-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)"
|
||||
},
|
||||
{
|
||||
"cg-config-v1.0.0_aces-v1.3_ocio-v2.1": "cg-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)"
|
||||
},
|
||||
{
|
||||
"custom": "custom"
|
||||
|
|
@ -93,6 +102,11 @@
|
|||
"type": "text",
|
||||
"key": "thumbnailLut",
|
||||
"label": "Thumbnails"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "monitorOutLut",
|
||||
"label": "Monitor"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,6 +151,40 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "include_handles",
|
||||
"collapsible": true,
|
||||
"label": "Include/Exclude Handles in default playback & render range",
|
||||
"children": [
|
||||
{
|
||||
"key": "include_handles_default",
|
||||
"label": "Include handles by default",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "per_task_type",
|
||||
"label": "Include/exclude handles by task type",
|
||||
"use_label_wrap": true,
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"type": "task-types-enum",
|
||||
"key": "task_type",
|
||||
"label": "Task types"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "include_handles",
|
||||
"label": "Include handles"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_scriptsmenu"
|
||||
|
|
|
|||
|
|
@ -91,6 +91,28 @@
|
|||
"key": "yetiRig"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "reference_loader",
|
||||
"label": "Reference Loader",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Namespace",
|
||||
"key": "namespace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Group name",
|
||||
"key": "group_name"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Here's a link to the doc where you can find explanations about customing the naming of referenced assets: https://openpype.io/docs/admin_hosts_maya#load-plugins"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,28 +74,34 @@
|
|||
"nuke-default": "nuke-default"
|
||||
},
|
||||
{
|
||||
"spi-vfx": "spi-vfx"
|
||||
"spi-vfx": "spi-vfx (11)"
|
||||
},
|
||||
{
|
||||
"spi-anim": "spi-anim"
|
||||
"spi-anim": "spi-anim (11)"
|
||||
},
|
||||
{
|
||||
"aces_0.1.1": "aces_0.1.1"
|
||||
"aces_0.1.1": "aces_0.1.1 (11)"
|
||||
},
|
||||
{
|
||||
"aces_0.7.1": "aces_0.7.1"
|
||||
"aces_0.7.1": "aces_0.7.1 (11)"
|
||||
},
|
||||
{
|
||||
"aces_1.0.1": "aces_1.0.1"
|
||||
"aces_1.0.1": "aces_1.0.1 (11)"
|
||||
},
|
||||
{
|
||||
"aces_1.0.3": "aces_1.0.3"
|
||||
"aces_1.0.3": "aces_1.0.3 (11, 12)"
|
||||
},
|
||||
{
|
||||
"aces_1.1": "aces_1.1"
|
||||
"aces_1.1": "aces_1.1 (12, 13)"
|
||||
},
|
||||
{
|
||||
"aces_1.2": "aces_1.2"
|
||||
"aces_1.2": "aces_1.2 (13, 14)"
|
||||
},
|
||||
{
|
||||
"studio-config-v1.0.0_aces-v1.3_ocio-v2.1": "studio-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)"
|
||||
},
|
||||
{
|
||||
"cg-config-v1.0.0_aces-v1.3_ocio-v2.1": "cg-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)"
|
||||
},
|
||||
{
|
||||
"custom": "custom"
|
||||
|
|
@ -257,4 +263,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@
|
|||
"template_data": [
|
||||
{
|
||||
"key": "ValidateCorrectAssetName",
|
||||
"label": "Validate Correct Asset name"
|
||||
"label": "Validate Correct Asset Name"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
"template_data": [
|
||||
{
|
||||
"key": "ValidateContainers",
|
||||
"label": "ValidateContainers"
|
||||
"label": "Validate Containers"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"key": "ValidateKnobs",
|
||||
"label": "ValidateKnobs",
|
||||
"label": "Validate Knobs",
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
|
|
@ -104,6 +104,10 @@
|
|||
"key": "ValidateOutputResolution",
|
||||
"label": "Validate Output Resolution"
|
||||
},
|
||||
{
|
||||
"key": "ValidateBackdrop",
|
||||
"label": "Validate Backdrop"
|
||||
},
|
||||
{
|
||||
"key": "ValidateGizmo",
|
||||
"label": "Validate Gizmo (Group)"
|
||||
|
|
|
|||
|
|
@ -361,12 +361,11 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
version_data.get("endFrame", None)
|
||||
)
|
||||
|
||||
handles_label = None
|
||||
handle_start = version_data.get("handleStart", None)
|
||||
handle_end = version_data.get("handleEnd", None)
|
||||
if handle_start is not None and handle_end is not None:
|
||||
handles = "{}-{}".format(str(handle_start), str(handle_end))
|
||||
else:
|
||||
handles = version_data.get("handles", None)
|
||||
handles_label = "{}-{}".format(str(handle_start), str(handle_end))
|
||||
|
||||
if frame_start is not None and frame_end is not None:
|
||||
# Remove superfluous zeros from numbers (3.0 -> 3) to improve
|
||||
|
|
@ -403,7 +402,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
"duration": duration,
|
||||
"handles": handles,
|
||||
"handles": handles_label,
|
||||
"frames": frames,
|
||||
"step": version_data.get("step", None),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import collections
|
|||
import uuid
|
||||
import tempfile
|
||||
import shutil
|
||||
import inspect
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import six
|
||||
|
|
@ -26,8 +27,8 @@ from openpype.pipeline import (
|
|||
PublishValidationError,
|
||||
KnownPublishError,
|
||||
registered_host,
|
||||
legacy_io,
|
||||
get_process_id,
|
||||
OptionalPyblishPluginMixin,
|
||||
)
|
||||
from openpype.pipeline.create import (
|
||||
CreateContext,
|
||||
|
|
@ -2307,6 +2308,37 @@ class PublisherController(BasePublisherController):
|
|||
def _process_main_thread_item(self, item):
|
||||
item()
|
||||
|
||||
def _is_publish_plugin_active(self, plugin):
|
||||
"""Decide if publish plugin is active.
|
||||
|
||||
This is hack because 'active' is mis-used in mixin
|
||||
'OptionalPyblishPluginMixin' where 'active' is used for default value
|
||||
of optional plugins. Because of that is 'active' state of plugin
|
||||
which inherit from 'OptionalPyblishPluginMixin' ignored. That affects
|
||||
headless publishing inside host, potentially remote publishing.
|
||||
|
||||
We have to change that to match pyblish base, but we can do that
|
||||
only when all hosts use Publisher because the change requires
|
||||
change of settings schemas.
|
||||
|
||||
Args:
|
||||
plugin (pyblish.Plugin): Plugin which should be checked if is
|
||||
active.
|
||||
|
||||
Returns:
|
||||
bool: Is plugin active.
|
||||
"""
|
||||
|
||||
if plugin.active:
|
||||
return True
|
||||
|
||||
if not plugin.optional:
|
||||
return False
|
||||
|
||||
if OptionalPyblishPluginMixin in inspect.getmro(plugin):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _publish_iterator(self):
|
||||
"""Main logic center of publishing.
|
||||
|
||||
|
|
@ -2315,11 +2347,9 @@ class PublisherController(BasePublisherController):
|
|||
states of currently processed publish plugin and instance. Also
|
||||
change state of processed orders like validation order has passed etc.
|
||||
|
||||
Also stops publishing if should stop on validation.
|
||||
|
||||
QUESTION:
|
||||
Does validate button still make sense?
|
||||
Also stops publishing, if should stop on validation.
|
||||
"""
|
||||
|
||||
for idx, plugin in enumerate(self._publish_plugins):
|
||||
self._publish_progress = idx
|
||||
|
||||
|
|
@ -2344,6 +2374,11 @@ class PublisherController(BasePublisherController):
|
|||
# Add plugin to publish report
|
||||
self._publish_report.add_plugin_iter(plugin, self._publish_context)
|
||||
|
||||
# WARNING This is hack fix for optional plugins
|
||||
if not self._is_publish_plugin_active(plugin):
|
||||
self._publish_report.set_plugin_skipped()
|
||||
continue
|
||||
|
||||
# Trigger callback that new plugin is going to be processed
|
||||
plugin_label = plugin.__name__
|
||||
if hasattr(plugin, "label") and plugin.label:
|
||||
|
|
@ -2450,7 +2485,11 @@ def collect_families_from_instances(instances, only_active=False):
|
|||
instances(list<pyblish.api.Instance>): List of publish instances from
|
||||
which are families collected.
|
||||
only_active(bool): Return families only for active instances.
|
||||
|
||||
Returns:
|
||||
list[str]: Families available on instances.
|
||||
"""
|
||||
|
||||
all_families = set()
|
||||
for instance in instances:
|
||||
if only_active:
|
||||
|
|
|
|||
|
|
@ -162,7 +162,8 @@ class PluginsModel(QtGui.QStandardItemModel):
|
|||
|
||||
items = []
|
||||
for plugin_item in plugin_items:
|
||||
item = QtGui.QStandardItem(plugin_item.label)
|
||||
label = plugin_item.label or plugin_item.name
|
||||
item = QtGui.QStandardItem(label)
|
||||
item.setData(False, ITEM_IS_GROUP_ROLE)
|
||||
item.setData(plugin_item.label, ITEM_LABEL_ROLE)
|
||||
item.setData(plugin_item.id, ITEM_ID_ROLE)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ from qtpy import QtWidgets, QtCore, QtGui
|
|||
import qtawesome
|
||||
|
||||
from openpype.client import get_projects
|
||||
from openpype.pipeline import AvalonMongoDB
|
||||
from openpype.style import get_objected_colors
|
||||
from openpype.tools.utils.widgets import ImageButton
|
||||
from openpype.tools.utils.lib import paint_image_with_color
|
||||
|
|
@ -97,6 +96,7 @@ class CompleterView(QtWidgets.QListView):
|
|||
|
||||
# Open the widget unactivated
|
||||
self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating)
|
||||
self.setAttribute(QtCore.Qt.WA_NoMouseReplay)
|
||||
delegate = QtWidgets.QStyledItemDelegate()
|
||||
self.setItemDelegate(delegate)
|
||||
|
||||
|
|
@ -241,6 +241,18 @@ class SettingsLineEdit(PlaceholderLineEdit):
|
|||
if self._completer is not None:
|
||||
self._completer.set_text_filter(text)
|
||||
|
||||
def _completer_should_be_visible(self):
|
||||
return (
|
||||
self.isVisible()
|
||||
and (self.hasFocus() or self._completer.hasFocus())
|
||||
)
|
||||
|
||||
def _show_completer(self):
|
||||
if self._completer_should_be_visible():
|
||||
self._focus_timer.start()
|
||||
self._completer.show()
|
||||
self._update_completer()
|
||||
|
||||
def _update_completer(self):
|
||||
if self._completer is None or not self._completer.isVisible():
|
||||
return
|
||||
|
|
@ -249,7 +261,7 @@ class SettingsLineEdit(PlaceholderLineEdit):
|
|||
self._completer.move(new_point)
|
||||
|
||||
def _on_focus_timer(self):
|
||||
if not self.hasFocus() and not self._completer.hasFocus():
|
||||
if not self._completer_should_be_visible():
|
||||
self._completer.hide()
|
||||
self._focus_timer.stop()
|
||||
|
||||
|
|
@ -258,9 +270,7 @@ class SettingsLineEdit(PlaceholderLineEdit):
|
|||
self.focused_in.emit()
|
||||
|
||||
if self._completer is not None:
|
||||
self._focus_timer.start()
|
||||
self._completer.show()
|
||||
self._update_completer()
|
||||
self._show_completer()
|
||||
|
||||
def paintEvent(self, event):
|
||||
super(SettingsLineEdit, self).paintEvent(event)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.15.4-nightly.2"
|
||||
__version__ = "3.15.4-nightly.3"
|
||||
|
|
|
|||
|
|
@ -106,6 +106,37 @@ or Deadlines **Draft Tile Assembler**.
|
|||
This is useful to fix some specific renderer glitches and advanced hacking of Maya Scene files. `Patch name` is label for patch for easier orientation.
|
||||
`Patch regex` is regex used to find line in file, after `Patch line` string is inserted. Note that you need to add line ending.
|
||||
|
||||
## Load Plugins
|
||||
|
||||
### Reference Loader
|
||||
|
||||
#### Namespace and Group Name
|
||||
Here you can create your own custom naming for the reference loader.
|
||||
|
||||
The custom naming is split into two parts: namespace and group name. If you don't set the namespace or the group name, an error will occur.
|
||||
Here's the different variables you can use:
|
||||
|
||||
<div class="row markdown">
|
||||
<div class="col col--5 markdown">
|
||||
|
||||
| Token | Description |
|
||||
|---|---|
|
||||
|`{asset_name}` | Asset name |
|
||||
|`{asset_type}` | Asset type |
|
||||
|`{subset}` | Subset name |
|
||||
|`{family}` | Subset family |
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
The namespace field can contain a single group of '#' number tokens to indicate where the namespace's unique index should go. The amount of tokens defines the zero padding of the number, e.g ### turns into 001.
|
||||
|
||||
Warning: Note that a namespace will always be prefixed with a _ if it starts with a digit.
|
||||
|
||||
Example:
|
||||
|
||||

|
||||
|
||||
### Extract GPU Cache
|
||||
|
||||

|
||||
|
|
@ -169,6 +200,17 @@ Most settings to override in the viewport are self explanatory and can be found
|
|||
These options are set on the camera shape when publishing the review. They correspond to attributes on the Maya camera shape node.
|
||||
|
||||

|
||||
## Include/exclude handles by task type
|
||||
You can include or exclude handles, globally or by task type.
|
||||
|
||||
The "Include handles by default" defines whether by default handles are included. Additionally you can add a per task type override whether you want to include or exclude handles.
|
||||
|
||||
For example, in this image you can see that handles are included by default in all task types, except for the 'Lighting' task, where the toggle is disabled.
|
||||

|
||||
|
||||
And here you can see that the handles are disabled by default, except in 'Animation' task where it's enabled.
|
||||

|
||||
|
||||
|
||||
## Custom Menu
|
||||
You can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**.
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ If there is an option of automatic repair, there will be `Repair` button on the
|
|||
|
||||
There are currently 2 options of `render` item:
|
||||
- Render of farm - allows offload rendering and publishing to Deadline - requires Deadline module being enabled
|
||||
- Validate Scene Settings - enables validation plugin which controls setting in DB (or asset control system like Ftrak) and scene itself
|
||||
- Validate Scene Settings - enables validation plugin which controls setting in DB (or asset control system like Ftrack) and scene itself
|
||||
|
||||

|
||||
|
||||
|
|
@ -100,6 +100,23 @@ There are currently 2 options of `render` item:
|
|||
- `Validate` - if you would like to run only collecting and validating phases (nothing will be published yet)
|
||||
- `Publish` - standard way how to kick off full publishing process
|
||||
|
||||
#### Support help
|
||||
If you would like to ask for help admin or support, you could use any of the three options on the `Note` button on bottom left:
|
||||
- `Go to details` - switches into a more detailed list of published instances and plugins.
|
||||
- `Copy report` - stash full publishing log to a clipboard
|
||||
- `Export report` - save log into a file for sending it via mail or any communication tool
|
||||
|
||||
If you are able to fix the workfile yourself, use the first button on the right to set the UI to initial state before publish. (Click the `Publish` button to start again.)
|
||||
|
||||
#### Legacy instances
|
||||
|
||||
All screenshots from Publish are from updated dialog, before publishing was being done by regular `Pyblish` tool.
|
||||
New publishing process should be backward compatible, eg. if you have a workfile with instances created in the previous publishing approach, they will be translated automatically and
|
||||
could be used right away.
|
||||
|
||||
If you hit on unexpected behaviour with old instances, contact support first, then you could try to delete and recreate instances from scratch.
|
||||
Nuclear option is to purge workfile metadata in `Window > Metadata > Basic > Label`. This is only for most determined daredevils though!
|
||||
|
||||
### Load
|
||||
|
||||
When you want to load existing published work, you can use the `Loader` tool. You can reach it in the extension's panel.
|
||||
|
|
@ -134,20 +151,3 @@ You can switch to a previous version of the image or update to the latest.
|
|||
|
||||

|
||||

|
||||
|
||||
#### Support help
|
||||
If you would like to ask for help admin or support, you could use any of the three options on the `Note` button on bottom left:
|
||||
- `Go to details` - switches into a more detailed list of published instances and plugins.
|
||||
- `Copy report` - stash full publishing log to a clipboard
|
||||
- `Export report` - save log into a file for sending it via mail or any communication tool
|
||||
|
||||
If you are able to fix the workfile yourself, use the first button on the right to set the UI to initial state before publish. (Click the `Publish` button to start again.)
|
||||
|
||||
#### Legacy instances
|
||||
|
||||
All screenshots from Publish are from updated dialog, before publishing was being done by regular `Pyblish` tool.
|
||||
New publishing process should be backward compatible, eg. if you have a workfile with instances created in the previous publishing approach, they will be translated automatically and
|
||||
could be used right away.
|
||||
|
||||
If you hit on unexpected behaviour with old instances, contact support first, then you could try to delete and recreate instances from scratch.
|
||||
Nuclear option is to purge workfile metadata in `Window > Metadata > Basic > Label`. This is only for most determined daredevils though!
|
||||
|
|
|
|||
|
|
@ -14,20 +14,29 @@ sidebar_label: Houdini
|
|||
- [Library Loader](artist_tools_library-loader)
|
||||
|
||||
## Publishing Alembic Cameras
|
||||
You can publish baked camera in Alembic format. Select your camera and go **OpenPype -> Create** and select **Camera (abc)**.
|
||||
You can publish baked camera in Alembic format.
|
||||
|
||||
Select your camera and go **OpenPype -> Create** and select **Camera (abc)**.
|
||||
This will create Alembic ROP in **out** with path and frame range already set. This node will have a name you've
|
||||
assigned in the **Creator** menu. For example if you name the subset `Default`, output Alembic Driver will be named
|
||||
`cameraDefault`. After that, you can **OpenPype -> Publish** and after some validations your camera will be published
|
||||
to `abc` file.
|
||||
|
||||
## Publishing Composites - Image Sequences
|
||||
You can publish image sequence directly from Houdini. You can use any `cop` network you have and publish image
|
||||
sequence generated from it. For example I've created simple **cop** graph to generate some noise:
|
||||
You can publish image sequences directly from Houdini's image COP networks.
|
||||
|
||||
You can use any COP node and publish the image sequence generated from it. For example this simple graph to generate some noise:
|
||||
|
||||

|
||||
|
||||
If I want to publish it, I'll select node I like - in this case `radialblur1` and go **OpenPype -> Create** and
|
||||
select **Composite (Image Sequence)**. This will create `/out/imagesequenceNoise` Composite ROP (I've named my subset
|
||||
*Noise*) with frame range set. When you hit **Publish** it will render image sequence from selected node.
|
||||
To publish the output of the `radialblur1` go to **OpenPype -> Create** and
|
||||
select **Composite (Image Sequence)**. If you name the variant *Noise* this will create the `/out/imagesequenceNoise` Composite ROP with the frame range set.
|
||||
|
||||
When you hit **Publish** it will render image sequence from selected node.
|
||||
|
||||
:::info Use selection
|
||||
With *Use selection* is enabled on create the node you have selected when creating will be the node used for published. (It set the Composite ROP node's COP path to it). If you don't do this you'll have to manually set the path as needed on e.g. `/out/imagesequenceNoise` to ensure it outputs what you want.
|
||||
:::
|
||||
|
||||
## Publishing Point Caches (alembic)
|
||||
Publishing point caches in alembic format is pretty straightforward, but it is by default enforcing better compatibility
|
||||
|
|
@ -46,6 +55,16 @@ you handle `path` attribute is up to you, this is just an example.*
|
|||
Now select the `output0` node and go **OpenPype -> Create** and select **Point Cache**. It will create
|
||||
Alembic ROP `/out/pointcacheStrange`
|
||||
|
||||
## Publishing Reviews (OpenGL)
|
||||
To generate a review output from Houdini you need to create a **review** instance.
|
||||
Go to **OpenPype -> Create** and select **Review**.
|
||||
|
||||

|
||||
|
||||
On create, with the **Use Selection** checkbox enabled it will set up the first
|
||||
camera found in your selection as the camera for the OpenGL ROP node and other
|
||||
non-cameras are set in **Force Objects**. It will then render those even if
|
||||
their display flag is disabled in your scene.
|
||||
|
||||
## Redshift
|
||||
:::note Work in progress
|
||||
|
|
|
|||
BIN
website/docs/assets/houdini_review_create_attrs.png
Normal file
BIN
website/docs/assets/houdini_review_create_attrs.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
website/docs/assets/maya-admin_custom_namespace.png
Normal file
BIN
website/docs/assets/maya-admin_custom_namespace.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
website/docs/assets/maya-admin_exclude_handles.png
Normal file
BIN
website/docs/assets/maya-admin_exclude_handles.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
website/docs/assets/maya-admin_include_handles.png
Normal file
BIN
website/docs/assets/maya-admin_include_handles.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -53,5 +53,5 @@ There are four settings available:
|
|||
|
||||
## Q&A
|
||||
### Is it safe to rename an entity from Kitsu?
|
||||
Absolutely! Entities are linked by their unique IDs between the two databases.
|
||||
Absolutely! Entities are linked by their unique IDs between the two databases.
|
||||
But renaming from the OP's Project Manager won't apply the change to Kitsu, it'll be overridden during the next synchronization.
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ suffix is **"client"** then the final suffix is **"h264_client"**.
|
|||
| resolution_height | Resolution height. |
|
||||
| fps | Fps of an output. |
|
||||
| timecode | Timecode by frame start and fps. |
|
||||
| focalLength | **Only available in Maya**<br /><br />Camera focal length per frame. Use syntax `{focalLength:.2f}` for decimal truncating. Eg. `35.234985` with `{focalLength:.2f}` would produce `35.23`, whereas `{focalLength:.0f}` would produce `35`. |
|
||||
| focalLength | **Only available in Maya and Houdini**<br /><br />Camera focal length per frame. Use syntax `{focalLength:.2f}` for decimal truncating. Eg. `35.234985` with `{focalLength:.2f}` would produce `35.23`, whereas `{focalLength:.0f}` would produce `35`. |
|
||||
|
||||
:::warning
|
||||
`timecode` is a specific key that can be **only at the end of content**. (`"BOTTOM_RIGHT": "TC: {timecode}"`)
|
||||
|
|
|
|||
|
|
@ -304,7 +304,7 @@ If source representation has suffix **"h264"** and burnin suffix is **"client"**
|
|||
| resolution_height | Resolution height. |
|
||||
| fps | Fps of an output. |
|
||||
| timecode | Timecode by frame start and fps. |
|
||||
| focalLength | **Only available in Maya**<br /><br />Camera focal length per frame. Use syntax `{focalLength:.2f}` for decimal truncating. Eg. `35.234985` with `{focalLength:.2f}` would produce `35.23`, whereas `{focalLength:.0f}` would produce `35`. |
|
||||
| focalLength | **Only available in Maya and Houdini**<br /><br />Camera focal length per frame. Use syntax `{focalLength:.2f}` for decimal truncating. Eg. `35.234985` with `{focalLength:.2f}` would produce `35.23`, whereas `{focalLength:.0f}` would produce `35`. |
|
||||
|
||||
:::warning
|
||||
`timecode` is specific key that can be **only at the end of content**. (`"BOTTOM_RIGHT": "TC: {timecode}"`)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue