Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Harmony #538

Closed
wants to merge 37 commits into from
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
71afdfb
Working prototype for Harmony
tokejepsen Mar 10, 2020
c9c7c6d
Only connect to the server once.
tokejepsen Mar 11, 2020
9fd7334
Support sending dictionaries.
tokejepsen Mar 11, 2020
9878d42
Working version of Harmony integration.
tokejepsen Mar 19, 2020
3f024b5
Code clean up
tokejepsen Mar 19, 2020
5a3574b
Dedicated "wait" argument.
tokejepsen Mar 19, 2020
730e9cf
On save threaded callback.
tokejepsen Mar 22, 2020
0d79a9d
Instance creation
tokejepsen Mar 24, 2020
e546578
Fix save callback when saving a new workfile.
tokejepsen Mar 24, 2020
bc63efc
Expose "send" at avalon.harmony level
tokejepsen Mar 24, 2020
5d30b48
Support showing workfiles on launch.
tokejepsen Mar 24, 2020
fd2a52a
Maintained nodes state function
tokejepsen Mar 25, 2020
608365a
Documentation
tokejepsen Mar 30, 2020
1d15c7b
Silence hound.
tokejepsen Mar 30, 2020
c74cd6e
Shhh... except "eval"
tokejepsen Mar 30, 2020
47e4312
Silence hound.
tokejepsen Mar 30, 2020
b5b47ae
Silence hound.
tokejepsen Mar 30, 2020
2d84f17
Containerise, ls and finished documentation.
tokejepsen Mar 31, 2020
109adbd
Ignore harmony on Maya tests.
tokejepsen Apr 2, 2020
4ee395e
Hound tolerate use of eval.
tokejepsen Apr 2, 2020
d649a82
Try whole jshintrc
tokejepsen Apr 2, 2020
46caea6
Simplify jshintrc
tokejepsen Apr 2, 2020
ef7ec1f
Hound experiments
tokejepsen Apr 2, 2020
96c5e66
Hound experiments
tokejepsen Apr 2, 2020
d55decb
Hound experiments
tokejepsen Apr 2, 2020
3d67fe6
Hound experiments
tokejepsen Apr 2, 2020
ba6b7fb
Hound experiments
tokejepsen Apr 2, 2020
5addeb7
Support all node types for creation.
tokejepsen May 26, 2020
4288e5e
Update README
tokejepsen May 26, 2020
9555693
Ensure all requests are processed.
tokejepsen May 26, 2020
11f52ac
Speed up communication with edge case for scene save.
tokejepsen May 27, 2020
5ef1daf
Shh... good doggy.
tokejepsen May 27, 2020
1c19e1b
Nice doggy.
tokejepsen May 27, 2020
6b56de7
Documentation on scene save.
tokejepsen May 27, 2020
0a6c96f
Prevent local overwrite when local scene is newer.
tokejepsen May 27, 2020
730b629
Created instances (nodes) are closer in the node view.
tokejepsen May 27, 2020
2ea2df9
"save_scene" not registered correctly.
tokejepsen May 29, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 249 additions & 0 deletions avalon/harmony/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
# Photoshop Integration

## Setup

The easiest way to setup for using Toon Boom Harmony is to use the built-in launch:

```
python -c "import avalon.harmony;avalon.harmony.launch("path/to/harmony/executable")"
```

The server/Harmony relationship looks like this:
```
+------------+
| |
| Python |
| Process |
| |
| +--------+ |
| | | |
| | Main | |
| | Thread | |
| | | |
| +----^---+ |
| || |
| || |
| +---v----+ | +---------+
| | | | | |
| | Server +-------> Harmony |
| | Thread <-------+ Process |
| | | | | |
| +--------+ | +---------+
+------------+
```

## Usage

The integration creates an `Avalon` menu entry where all Avalon related tools are located.

**NOTE: Menu creation can be temperamental. The best way is to launch Harmony and do nothing else until Harmony is fully launched.**

### Work files

Because Harmony projects are directories, this integration uses `.zip` as work file extension. Internally the project directories are stored under `[User]/.avalon/harmony`. Whenever the user saves the `.xstage` file, the integration zips up the project directory and move it to the Avalon project path. Zipping and moving happens in the background.

### Show Workfiles on launch

You can show the Workfiles app when Harmony launches by setting environment variable `AVALON_HARMONY_WORKFILES_ON_LAUNCH=1`.

## Developing
### Plugin Examples
These plugins were made with the [polly config](https://github.com/mindbender-studio/config).

#### Creator Plugin
```python
from avalon import harmony


class CreateComposite(harmony.Creator):
"""Composite node for publish."""

name = "compositeDefault"
label = "Composite"
family = "mindbender.imagesequence"

def __init__(self, *args, **kwargs):
super(CreateComposite, self).__init__(*args, **kwargs)
```

#### Collector Plugin
```python
import pyblish.api
from avalon import harmony


class CollectInstances(pyblish.api.ContextPlugin):
"""Gather instances by nodes metadata.

This collector takes into account assets that are associated with
a composite node and marked with a unique identifier;

Identifier:
id (str): "pyblish.avalon.instance"
"""

label = "Instances"
order = pyblish.api.CollectorOrder
hosts = ["harmony"]

def process(self, context):
nodes = harmony.send(
{"function": "node.getNodes", "args": [["COMPOSITE"]]}
)["result"]

for node in nodes:
data = harmony.read(node)

# Skip non-tagged nodes.
if not data:
continue

# Skip containers.
if "container" in data["id"]:
continue

instance = context.create_instance(node.split("/")[-1])
instance.append(node)
instance.data.update(data)

# Produce diagnostic message for any graphical
# user interface interested in visualising it.
self.log.info("Found: \"%s\" " % instance.data["name"])
```

#### Extractor Plugin
```python
import os

import pyblish.api
from avalon import harmony

import clique


class ExtractImage(pyblish.api.InstancePlugin):
"""Produce a flattened image file from instance.
This plug-in only takes into account the nodes connected to the composite.
"""
label = "Extract Image Sequence"
order = pyblish.api.ExtractorOrder
hosts = ["harmony"]
families = ["mindbender.imagesequence"]

def process(self, instance):
project_path = harmony.send(
{"function": "scene.currentProjectPath"}
)["result"]

# Store reference for integration
if "files" not in instance.data:
instance.data["files"] = list()

# Store display source node for later.
display_node = "Top/Display"
func = """function func(display_node)
{
var source_node = null;
if (node.isLinked(display_node, 0))
{
source_node = node.srcNode(display_node, 0);
node.unlink(display_node, 0);
}
return source_node
}
func
"""
display_source_node = harmony.send(
{"function": func, "args": [display_node]}
)["result"]

# Perform extraction
path = os.path.join(
os.path.normpath(
project_path
).replace("\\", "/"),
instance.data["name"]
)
if not os.path.exists(path):
os.makedirs(path)

render_func = """function frameReady(frame, celImage)
{{
var path = "{path}/{filename}" + frame + ".png";
celImage.imageFileAs(path, "", "PNG4");
}}
function func(composite_node)
{{
node.link(composite_node, 0, "{display_node}", 0);
render.frameReady.connect(frameReady);
render.setRenderDisplay("{display_node}");
render.renderSceneAll();
render.frameReady.disconnect(frameReady);
}}
func
"""
restore_func = """function func(args)
{
var display_node = args[0];
var display_source_node = args[1];
if (node.isLinked(display_node, 0))
{
node.unlink(display_node, 0);
}
node.link(display_source_node, 0, display_node, 0);
}
func
"""

with harmony.maintained_selection():
self.log.info("Extracting %s" % str(list(instance)))

harmony.send(
{
"function": render_func.format(
path=path.replace("\\", "/"),
filename=os.path.basename(path),
display_node=display_node
),
"args": [instance[0]]
}
)

# Restore display.
if display_source_node:
harmony.send(
{
"function": restore_func,
"args": [display_node, display_source_node]
}
)

files = os.listdir(path)
collections, remainder = clique.assemble(files, minimum_items=1)
assert not remainder, (
"There shouldn't have been a remainder for '%s': "
"%s" % (instance[0], remainder)
)
assert len(collections) == 1, (
"There should only be one image sequence in {}. Found: {}".format(
path, len(collections)
)
)

data = {
"subset": collections[0].head,
"isSeries": True,
"stagingDir": path,
"files": list(collections[0]),
}
instance.data.update(data)

self.log.info("Extracted {instance} to {path}".format(**locals()))
```


## Resources
- https://github.com/diegogarciahuerta/tk-harmony
- https://github.com/cfourney/OpenHarmony
- [Toon Boom Discord](https://discord.gg/syAjy4H)
- [Toon Boom TD](https://discord.gg/yAjyQtZ)
Loading