This repository has been archived by the owner on Sep 20, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 129
Layout asset for Blender #226
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
"""Create a layout asset.""" | ||
|
||
import bpy | ||
|
||
from avalon import api | ||
from avalon.blender import Creator, lib | ||
import pype.hosts.blender.plugin | ||
|
||
|
||
class CreateLayout(Creator): | ||
"""Layout output for character rigs""" | ||
|
||
name = "layoutMain" | ||
label = "Layout" | ||
family = "layout" | ||
icon = "cubes" | ||
|
||
def process(self): | ||
|
||
asset = self.data["asset"] | ||
subset = self.data["subset"] | ||
name = pype.hosts.blender.plugin.asset_name(asset, subset) | ||
collection = bpy.data.collections.new(name=name) | ||
bpy.context.scene.collection.children.link(collection) | ||
self.data['task'] = api.Session.get('AVALON_TASK') | ||
lib.imprint(collection, self.data) | ||
|
||
# Add the rig object and all the children meshes to | ||
# a set and link them all at the end to avoid duplicates. | ||
# Blender crashes if trying to link an object that is already linked. | ||
# This links automatically the children meshes if they were not | ||
# selected, and doesn't link them twice if they, insted, | ||
# were manually selected by the user. | ||
objects_to_link = set() | ||
|
||
if (self.options or {}).get("useSelection"): | ||
|
||
for obj in lib.get_selection(): | ||
|
||
objects_to_link.add(obj) | ||
|
||
if obj.type == 'ARMATURE': | ||
|
||
for subobj in obj.children: | ||
|
||
objects_to_link.add(subobj) | ||
|
||
for obj in objects_to_link: | ||
|
||
collection.objects.link(obj) | ||
|
||
return collection |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,259 @@ | ||
"""Load a layout in Blender.""" | ||
|
||
import logging | ||
from pathlib import Path | ||
from pprint import pformat | ||
from typing import Dict, List, Optional | ||
|
||
from avalon import api, blender | ||
import bpy | ||
import pype.hosts.blender.plugin | ||
|
||
|
||
logger = logging.getLogger("pype").getChild( | ||
"blender").getChild("load_layout") | ||
|
||
|
||
class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): | ||
"""Load animations from a .blend file. | ||
|
||
Warning: | ||
Loading the same asset more then once is not properly supported at the | ||
moment. | ||
""" | ||
|
||
families = ["layout"] | ||
representations = ["blend"] | ||
|
||
label = "Link Layout" | ||
icon = "code-fork" | ||
color = "orange" | ||
|
||
@staticmethod | ||
def _remove(self, objects, lib_container): | ||
|
||
for obj in objects: | ||
|
||
if obj.type == 'ARMATURE': | ||
bpy.data.armatures.remove(obj.data) | ||
elif obj.type == 'MESH': | ||
bpy.data.meshes.remove(obj.data) | ||
|
||
bpy.data.collections.remove(bpy.data.collections[lib_container]) | ||
|
||
@staticmethod | ||
def _process(self, libpath, lib_container, container_name, actions): | ||
|
||
relative = bpy.context.preferences.filepaths.use_relative_paths | ||
with bpy.data.libraries.load( | ||
libpath, link=True, relative=relative | ||
) as (_, data_to): | ||
data_to.collections = [lib_container] | ||
|
||
scene = bpy.context.scene | ||
|
||
scene.collection.children.link(bpy.data.collections[lib_container]) | ||
|
||
layout_container = scene.collection.children[lib_container].make_local() | ||
|
||
meshes = [ | ||
obj for obj in layout_container.objects if obj.type == 'MESH'] | ||
armatures = [ | ||
obj for obj in layout_container.objects if obj.type == 'ARMATURE'] | ||
|
||
objects_list = [] | ||
|
||
# Link meshes first, then armatures. | ||
# The armature is unparented for all the non-local meshes, | ||
# when it is made local. | ||
for obj in meshes + armatures: | ||
|
||
obj = obj.make_local() | ||
|
||
obj.data.make_local() | ||
|
||
if not obj.get(blender.pipeline.AVALON_PROPERTY): | ||
|
||
obj[blender.pipeline.AVALON_PROPERTY] = dict() | ||
|
||
avalon_info = obj[blender.pipeline.AVALON_PROPERTY] | ||
avalon_info.update({"container_name": container_name}) | ||
|
||
action = actions.get( obj.name, None ) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. whitespace after '(' |
||
|
||
if obj.type == 'ARMATURE' and action is not None: | ||
|
||
obj.animation_data.action = action | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. blank line contains whitespace |
||
objects_list.append(obj) | ||
|
||
layout_container.pop(blender.pipeline.AVALON_PROPERTY) | ||
|
||
bpy.ops.object.select_all(action='DESELECT') | ||
|
||
return objects_list | ||
|
||
def process_asset( | ||
self, context: dict, name: str, namespace: Optional[str] = None, | ||
options: Optional[Dict] = None | ||
) -> Optional[List]: | ||
""" | ||
Arguments: | ||
name: Use pre-defined name | ||
namespace: Use pre-defined namespace | ||
context: Full parenthood of representation to load | ||
options: Additional settings dictionary | ||
""" | ||
|
||
libpath = self.fname | ||
asset = context["asset"]["name"] | ||
subset = context["subset"]["name"] | ||
lib_container = pype.hosts.blender.plugin.asset_name(asset, subset) | ||
container_name = pype.hosts.blender.plugin.asset_name( | ||
asset, subset, namespace | ||
) | ||
|
||
container = bpy.data.collections.new(lib_container) | ||
container.name = container_name | ||
blender.pipeline.containerise_existing( | ||
container, | ||
name, | ||
namespace, | ||
context, | ||
self.__class__.__name__, | ||
) | ||
|
||
container_metadata = container.get( | ||
blender.pipeline.AVALON_PROPERTY) | ||
|
||
container_metadata["libpath"] = libpath | ||
container_metadata["lib_container"] = lib_container | ||
|
||
objects_list = self._process( | ||
self, libpath, lib_container, container_name, {}) | ||
|
||
# Save the list of objects in the metadata container | ||
container_metadata["objects"] = objects_list | ||
|
||
nodes = list(container.objects) | ||
nodes.append(container) | ||
self[:] = nodes | ||
return nodes | ||
|
||
def update(self, container: Dict, representation: Dict): | ||
"""Update the loaded asset. | ||
|
||
This will remove all objects of the current collection, load the new | ||
ones and add them to the collection. | ||
If the objects of the collection are used in another collection they | ||
will not be removed, only unlinked. Normally this should not be the | ||
case though. | ||
|
||
Warning: | ||
No nested collections are supported at the moment! | ||
""" | ||
|
||
collection = bpy.data.collections.get( | ||
container["objectName"] | ||
) | ||
|
||
libpath = Path(api.get_representation_path(representation)) | ||
extension = libpath.suffix.lower() | ||
|
||
logger.info( | ||
"Container: %s\nRepresentation: %s", | ||
pformat(container, indent=2), | ||
pformat(representation, indent=2), | ||
) | ||
|
||
assert collection, ( | ||
f"The asset is not loaded: {container['objectName']}" | ||
) | ||
assert not (collection.children), ( | ||
"Nested collections are not supported." | ||
) | ||
assert libpath, ( | ||
"No existing library file found for {container['objectName']}" | ||
) | ||
assert libpath.is_file(), ( | ||
f"The file doesn't exist: {libpath}" | ||
) | ||
assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, ( | ||
f"Unsupported file: {libpath}" | ||
) | ||
|
||
collection_metadata = collection.get( | ||
blender.pipeline.AVALON_PROPERTY) | ||
|
||
collection_libpath = collection_metadata["libpath"] | ||
normalized_collection_libpath = ( | ||
str(Path(bpy.path.abspath(collection_libpath)).resolve()) | ||
) | ||
normalized_libpath = ( | ||
str(Path(bpy.path.abspath(str(libpath))).resolve()) | ||
) | ||
logger.debug( | ||
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", | ||
normalized_collection_libpath, | ||
normalized_libpath, | ||
) | ||
if normalized_collection_libpath == normalized_libpath: | ||
logger.info("Library already loaded, not updating...") | ||
return | ||
|
||
objects = collection_metadata["objects"] | ||
lib_container = collection_metadata["lib_container"] | ||
|
||
actions = {} | ||
|
||
for obj in objects: | ||
|
||
if obj.type == 'ARMATURE': | ||
|
||
actions[obj.name] = obj.animation_data.action | ||
|
||
self._remove(self, objects, lib_container) | ||
|
||
objects_list = self._process( | ||
self, str(libpath), lib_container, collection.name, actions) | ||
|
||
# Save the list of objects in the metadata container | ||
collection_metadata["objects"] = objects_list | ||
collection_metadata["libpath"] = str(libpath) | ||
collection_metadata["representation"] = str(representation["_id"]) | ||
|
||
bpy.ops.object.select_all(action='DESELECT') | ||
|
||
def remove(self, container: Dict) -> bool: | ||
"""Remove an existing container from a Blender scene. | ||
|
||
Arguments: | ||
container (avalon-core:container-1.0): Container to remove, | ||
from `host.ls()`. | ||
|
||
Returns: | ||
bool: Whether the container was deleted. | ||
|
||
Warning: | ||
No nested collections are supported at the moment! | ||
""" | ||
|
||
collection = bpy.data.collections.get( | ||
container["objectName"] | ||
) | ||
if not collection: | ||
return False | ||
assert not (collection.children), ( | ||
"Nested collections are not supported." | ||
) | ||
|
||
collection_metadata = collection.get( | ||
blender.pipeline.AVALON_PROPERTY) | ||
objects = collection_metadata["objects"] | ||
lib_container = collection_metadata["lib_container"] | ||
|
||
self._remove(self, objects, lib_container) | ||
|
||
bpy.data.collections.remove(collection) | ||
|
||
return True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
line too long (80 > 79 characters)