Merge branch 'develop' into feature/OP-1539_Flame-Loading-published-clips-back

This commit is contained in:
Jakub Jezek 2022-02-15 10:46:58 +01:00
commit 4355ede311
No known key found for this signature in database
GPG key ID: D8548FBF690B100A
255 changed files with 14762 additions and 383293 deletions

View file

@ -1,6 +1,6 @@
# Changelog
## [3.8.3-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD)
## [3.9.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.2...HEAD)
@ -12,19 +12,37 @@
**🚀 Enhancements**
- Maya configurable unit validator [\#2680](https://github.com/pypeclub/OpenPype/pull/2680)
- Ftrack: Sync description to assets [\#2670](https://github.com/pypeclub/OpenPype/pull/2670)
- Houdini: Moved to OpenPype [\#2658](https://github.com/pypeclub/OpenPype/pull/2658)
- Maya: Move implementation to OpenPype [\#2649](https://github.com/pypeclub/OpenPype/pull/2649)
- General: FFmpeg conversion also check attribute string length [\#2635](https://github.com/pypeclub/OpenPype/pull/2635)
- Houdini: Load Arnold .ass procedurals into Houdini [\#2606](https://github.com/pypeclub/OpenPype/pull/2606)
- Houdini: Implement Arnold .ass standin extraction from Houdini \(also support .ass.gz\) [\#2603](https://github.com/pypeclub/OpenPype/pull/2603)
- New Publisher: New features and preparations for new standalone publisher [\#2556](https://github.com/pypeclub/OpenPype/pull/2556)
**🐛 Bug fixes**
- LookAssigner: Fix imports after moving code to OpenPype repository [\#2701](https://github.com/pypeclub/OpenPype/pull/2701)
- Maya Redshift fixes [\#2692](https://github.com/pypeclub/OpenPype/pull/2692)
- Maya: fix fps validation popup [\#2685](https://github.com/pypeclub/OpenPype/pull/2685)
- Houdini Explicitly collect correct frame name even in case of single frame render when `frameStart` is provided [\#2676](https://github.com/pypeclub/OpenPype/pull/2676)
- hiero: fix effect collector name and order [\#2673](https://github.com/pypeclub/OpenPype/pull/2673)
- Maya: Fix menu callbacks [\#2671](https://github.com/pypeclub/OpenPype/pull/2671)
- hiero: removing obsolete unsupported plugin [\#2667](https://github.com/pypeclub/OpenPype/pull/2667)
- Launcher: Fix access to 'data' attribute on actions [\#2659](https://github.com/pypeclub/OpenPype/pull/2659)
**Merged pull requests:**
- Webpublisher: Photoshop create a beauty png [\#2689](https://github.com/pypeclub/OpenPype/pull/2689)
- Ftrack: Hierarchical attributes are queried properly [\#2682](https://github.com/pypeclub/OpenPype/pull/2682)
- Fix python install in docker for centos7 [\#2664](https://github.com/pypeclub/OpenPype/pull/2664)
- Maya: Add Validate Frame Range settings [\#2661](https://github.com/pypeclub/OpenPype/pull/2661)
- Harmony: move to Openpype [\#2657](https://github.com/pypeclub/OpenPype/pull/2657)
- General: Show applications without integration in project [\#2656](https://github.com/pypeclub/OpenPype/pull/2656)
- Maya: cleanup duplicate rendersetup code [\#2642](https://github.com/pypeclub/OpenPype/pull/2642)
- Deadline: Be able to pass Mongo url to job [\#2616](https://github.com/pypeclub/OpenPype/pull/2616)
- Fix - safer pulling of task name for webpublishing from PS [\#2613](https://github.com/pypeclub/OpenPype/pull/2613)
## [3.8.2](https://github.com/pypeclub/OpenPype/tree/3.8.2) (2022-02-07)
@ -37,11 +55,11 @@
**🚀 Enhancements**
- TVPaint: Image loaders also work on review family [\#2638](https://github.com/pypeclub/OpenPype/pull/2638)
- Global: adding studio name/code to anatomy template formatting data [\#2630](https://github.com/pypeclub/OpenPype/pull/2630)
- General: Project backup tools [\#2629](https://github.com/pypeclub/OpenPype/pull/2629)
- nuke: adding clear button to write nodes [\#2627](https://github.com/pypeclub/OpenPype/pull/2627)
- Ftrack: Family to Asset type mapping is in settings [\#2602](https://github.com/pypeclub/OpenPype/pull/2602)
- Nuke: load color space from representation data [\#2576](https://github.com/pypeclub/OpenPype/pull/2576)
- New Publisher: New features and preparations for new standalone publisher [\#2556](https://github.com/pypeclub/OpenPype/pull/2556)
**🐛 Bug fixes**
@ -52,7 +70,6 @@
- Docker: enhance dockerfiles with metadata, fix pyenv initialization [\#2647](https://github.com/pypeclub/OpenPype/pull/2647)
- WebPublisher: fix instance duplicates [\#2641](https://github.com/pypeclub/OpenPype/pull/2641)
- Fix - safer pulling of task name for webpublishing from PS [\#2613](https://github.com/pypeclub/OpenPype/pull/2613)
## [3.8.1](https://github.com/pypeclub/OpenPype/tree/3.8.1) (2022-02-01)
@ -62,7 +79,6 @@
- Webpublisher: Thumbnail extractor [\#2600](https://github.com/pypeclub/OpenPype/pull/2600)
- Loader: Allow to toggle default family filters between "include" or "exclude" filtering [\#2541](https://github.com/pypeclub/OpenPype/pull/2541)
- Launcher: Added context menu to to skip opening last workfile [\#2536](https://github.com/pypeclub/OpenPype/pull/2536)
**🐛 Bug fixes**
@ -79,7 +95,6 @@
- Bump pillow from 8.4.0 to 9.0.0 [\#2595](https://github.com/pypeclub/OpenPype/pull/2595)
- Webpublisher: Skip version collect [\#2591](https://github.com/pypeclub/OpenPype/pull/2591)
- build\(deps\): bump pillow from 8.4.0 to 9.0.0 [\#2523](https://github.com/pypeclub/OpenPype/pull/2523)
## [3.8.0](https://github.com/pypeclub/OpenPype/tree/3.8.0) (2022-01-24)
@ -91,10 +106,10 @@
**🆕 New features**
- General: Store settings by OpenPype version [\#2570](https://github.com/pypeclub/OpenPype/pull/2570)
- Flame: extracting segments with trans-coding [\#2547](https://github.com/pypeclub/OpenPype/pull/2547)
- Maya : V-Ray Proxy - load all ABC files via proxy [\#2544](https://github.com/pypeclub/OpenPype/pull/2544)
- Maya to Unreal: Extended static mesh workflow [\#2537](https://github.com/pypeclub/OpenPype/pull/2537)
- Flame: collecting publishable instances [\#2519](https://github.com/pypeclub/OpenPype/pull/2519)
**🚀 Enhancements**
@ -104,10 +119,6 @@
- Settings: PathInput strip passed string [\#2550](https://github.com/pypeclub/OpenPype/pull/2550)
- Global: Exctract Review anatomy fill data with output name [\#2548](https://github.com/pypeclub/OpenPype/pull/2548)
- Cosmetics: Clean up some cosmetics / typos [\#2542](https://github.com/pypeclub/OpenPype/pull/2542)
- General: Validate if current process OpenPype version is requested version [\#2529](https://github.com/pypeclub/OpenPype/pull/2529)
- General: Be able to use anatomy data in ffmpeg output arguments [\#2525](https://github.com/pypeclub/OpenPype/pull/2525)
- Expose toggle publish plug-in settings for Maya Look Shading Engine Naming [\#2521](https://github.com/pypeclub/OpenPype/pull/2521)
- Photoshop: Move implementation to OpenPype [\#2510](https://github.com/pypeclub/OpenPype/pull/2510)
**🐛 Bug fixes**
@ -119,23 +130,12 @@
- Webpublisher: Fixed progress reporting [\#2553](https://github.com/pypeclub/OpenPype/pull/2553)
- Fix Maya AssProxyLoader version switch [\#2551](https://github.com/pypeclub/OpenPype/pull/2551)
- General: Fix install thread in igniter [\#2549](https://github.com/pypeclub/OpenPype/pull/2549)
- Houdini: vdbcache family preserve frame numbers on publish integration + enable validate version for Houdini [\#2535](https://github.com/pypeclub/OpenPype/pull/2535)
- Maya: Fix Load VDB to V-Ray [\#2533](https://github.com/pypeclub/OpenPype/pull/2533)
- Maya: ReferenceLoader fix not unique group name error for attach to root [\#2532](https://github.com/pypeclub/OpenPype/pull/2532)
- Maya: namespaced context go back to original namespace when started from inside a namespace [\#2531](https://github.com/pypeclub/OpenPype/pull/2531)
- Fix create zip tool - path argument [\#2522](https://github.com/pypeclub/OpenPype/pull/2522)
- Maya: Fix Extract Look with space in names [\#2518](https://github.com/pypeclub/OpenPype/pull/2518)
- Fix published frame content for sequence starting with 0 [\#2513](https://github.com/pypeclub/OpenPype/pull/2513)
- Maya: reset empty string attributes correctly to "" instead of "None" [\#2506](https://github.com/pypeclub/OpenPype/pull/2506)
- Improve FusionPreLaunch hook errors [\#2505](https://github.com/pypeclub/OpenPype/pull/2505)
**Merged pull requests:**
- AfterEffects: Move implementation to OpenPype [\#2543](https://github.com/pypeclub/OpenPype/pull/2543)
- Maya: Remove Maya Look Assigner check on startup [\#2540](https://github.com/pypeclub/OpenPype/pull/2540)
- build\(deps\): bump shelljs from 0.8.4 to 0.8.5 in /website [\#2538](https://github.com/pypeclub/OpenPype/pull/2538)
- build\(deps\): bump follow-redirects from 1.14.4 to 1.14.7 in /website [\#2534](https://github.com/pypeclub/OpenPype/pull/2534)
- Nuke: Merge avalon's implementation into OpenPype [\#2514](https://github.com/pypeclub/OpenPype/pull/2514)
## [3.7.0](https://github.com/pypeclub/OpenPype/tree/3.7.0) (2022-01-04)

View file

@ -23,7 +23,8 @@ from .user_settings import (
OpenPypeSettingsRegistry
)
from .tools import (
get_openpype_path_from_db,
get_openpype_global_settings,
get_openpype_path_from_settings,
get_expected_studio_version_str
)
@ -973,9 +974,19 @@ class BootstrapRepos:
for line in checksums_data.split("\n") if line
]
# get list of files in zip minus `checksums` file itself
# and turn in to set to compare against list of files
# from checksum file. If difference exists, something is
# wrong
files_in_zip = set(zip_file.namelist())
files_in_zip.remove("checksums")
files_in_checksum = {file[1] for file in checksums}
diff = files_in_zip.difference(files_in_checksum)
if diff:
return False, f"Missing files {diff}"
# calculate and compare checksums in the zip file
for file in checksums:
file_name = file[1]
for file_checksum, file_name in checksums:
if platform.system().lower() == "windows":
file_name = file_name.replace("/", "\\")
h = hashlib.sha256()
@ -983,21 +994,9 @@ class BootstrapRepos:
h.update(zip_file.read(file_name))
except FileNotFoundError:
return False, f"Missing file [ {file_name} ]"
if h.hexdigest() != file[0]:
if h.hexdigest() != file_checksum:
return False, f"Invalid checksum on {file_name}"
# get list of files in zip minus `checksums` file itself
# and turn in to set to compare against list of files
# from checksum file. If difference exists, something is
# wrong
files_in_zip = zip_file.namelist()
files_in_zip.remove("checksums")
files_in_zip = set(files_in_zip)
files_in_checksum = {file[1] for file in checksums}
diff = files_in_zip.difference(files_in_checksum)
if diff:
return False, f"Missing files {diff}"
return True, "All ok"
@staticmethod
@ -1011,16 +1010,22 @@ class BootstrapRepos:
tuple(line.split(":"))
for line in checksums_data.split("\n") if line
]
files_in_dir = [
# compare file list against list of files from checksum file.
# If difference exists, something is wrong and we invalidate directly
files_in_dir = set(
file.relative_to(path).as_posix()
for file in path.iterdir() if file.is_file()
]
)
files_in_dir.remove("checksums")
files_in_dir = set(files_in_dir)
files_in_checksum = {file[1] for file in checksums}
for file in checksums:
file_name = file[1]
diff = files_in_dir.difference(files_in_checksum)
if diff:
return False, f"Missing files {diff}"
# calculate and compare checksums
for file_checksum, file_name in checksums:
if platform.system().lower() == "windows":
file_name = file_name.replace("/", "\\")
try:
@ -1028,11 +1033,8 @@ class BootstrapRepos:
except FileNotFoundError:
return False, f"Missing file [ {file_name} ]"
if file[0] != current:
if file_checksum != current:
return False, f"Invalid checksum on {file_name}"
diff = files_in_dir.difference(files_in_checksum)
if diff:
return False, f"Missing files {diff}"
return True, "All ok"
@ -1262,7 +1264,8 @@ class BootstrapRepos:
openpype_path = None
# try to get OpenPype path from mongo.
if location.startswith("mongodb"):
openpype_path = get_openpype_path_from_db(location)
global_settings = get_openpype_global_settings(location)
openpype_path = get_openpype_path_from_settings(global_settings)
if not openpype_path:
self._print("cannot find OPENPYPE_PATH in settings.")
return None

View file

@ -12,7 +12,6 @@ from Qt.QtCore import QTimer # noqa
from .install_thread import InstallThread
from .tools import (
validate_mongo_connection,
get_openpype_path_from_db,
get_openpype_icon_path
)

View file

@ -161,18 +161,17 @@ def get_openpype_global_settings(url: str) -> dict:
return global_settings.get("data") or {}
def get_openpype_path_from_db(url: str) -> Union[str, None]:
def get_openpype_path_from_settings(settings: dict) -> Union[str, None]:
"""Get OpenPype path from global settings.
Args:
url (str): mongodb url.
settings (dict): mongodb url.
Returns:
path to OpenPype or None if not found
"""
global_settings = get_openpype_global_settings(url)
paths = (
global_settings
settings
.get("openpype_path", {})
.get(platform.system().lower())
) or []

View file

@ -1,10 +1,10 @@
import os
import subprocess
from openpype.lib import (
PreLaunchHook,
get_openpype_execute_args
)
from openpype.lib.applications import get_non_python_host_kwargs
from openpype import PACKAGE_DIR as OPENPYPE_DIR
@ -51,3 +51,7 @@ class NonPythonHostHook(PreLaunchHook):
if remainders:
self.launch_context.launch_args.extend(remainders)
self.launch_context.kwargs = \
get_non_python_host_kwargs(self.launch_context.kwargs)

View file

@ -35,7 +35,6 @@ def main(*subprocess_args):
launcher.start()
if os.environ.get("HEADLESS_PUBLISH"):
# reusing ConsoleTrayApp approach as it was already implemented
launcher.execute_in_main_thread(lambda: headless_publish(
log,
"CloseAE",

View file

@ -306,6 +306,17 @@ class LaunchManager(LaunchQtApp):
self._window.refresh()
class LaunchLibrary(LaunchQtApp):
"""Launch Library Loader."""
bl_idname = "wm.library_loader"
bl_label = "Library..."
_tool_name = "libraryloader"
def before_window_show(self):
self._window.refresh()
class LaunchWorkFiles(LaunchQtApp):
"""Launch Avalon Work Files."""
@ -365,6 +376,7 @@ class TOPBAR_MT_avalon(bpy.types.Menu):
icon_value=pyblish_menu_icon_id,
)
layout.operator(LaunchManager.bl_idname, text="Manage...")
layout.operator(LaunchLibrary.bl_idname, text="Library...")
layout.separator()
layout.operator(LaunchWorkFiles.bl_idname, text="Work Files...")
# TODO (jasper): maybe add 'Reload Pipeline', 'Reset Frame Range' and
@ -382,6 +394,7 @@ classes = [
LaunchLoader,
LaunchPublisher,
LaunchManager,
LaunchLibrary,
LaunchWorkFiles,
TOPBAR_MT_avalon,
]

View file

@ -110,19 +110,19 @@ class FlameMenuProjectConnect(_FlameMenuApp):
menu = deepcopy(self.menu)
menu['actions'].append({
"name": "Workfiles ...",
"name": "Workfiles...",
"execute": lambda x: self.tools_helper.show_workfiles()
})
menu['actions'].append({
"name": "Load ...",
"name": "Load...",
"execute": lambda x: self.tools_helper.show_loader()
})
menu['actions'].append({
"name": "Manage ...",
"name": "Manage...",
"execute": lambda x: self.tools_helper.show_scene_inventory()
})
menu['actions'].append({
"name": "Library ...",
"name": "Library...",
"execute": lambda x: self.tools_helper.show_library_loader()
})
return menu
@ -164,24 +164,27 @@ class FlameMenuTimeline(_FlameMenuApp):
menu = deepcopy(self.menu)
menu['actions'].append({
"name": "Create ...",
"name": "Create...",
"execute": lambda x: callback_selection(
x, self.tools_helper.show_creator)
})
menu['actions'].append({
"name": "Publish ...",
"name": "Publish...",
"execute": lambda x: callback_selection(
x, self.tools_helper.show_publish)
})
menu['actions'].append({
"name": "Load ...",
"name": "Load...",
"execute": lambda x: self.tools_helper.show_loader()
})
menu['actions'].append({
"name": "Manage ...",
"name": "Manage...",
"execute": lambda x: self.tools_helper.show_scene_inventory()
})
menu['actions'].append({
"name": "Library...",
"execute": lambda x: self.tools_helper.show_library_loader()
})
return menu
def refresh(self, *args, **kwargs):

View file

@ -29,18 +29,6 @@ log = Logger.get_logger(__name__)
def install():
# Disable all families except for the ones we explicitly want to see
family_states = [
"imagesequence",
"render2d",
"plate",
"render",
"mov",
"clip"
]
avalon.data["familiesStateDefault"] = False
avalon.data["familiesStateToggled"] = family_states
pyblish.register_host("flame")
pyblish.register_plugin_path(PUBLISH_PATH)

View file

@ -54,13 +54,13 @@ class OpenPypeMenu(QtWidgets.QWidget):
)
self.render_mode_widget = None
self.setWindowTitle("OpenPype")
workfiles_btn = QtWidgets.QPushButton("Workfiles ...", self)
create_btn = QtWidgets.QPushButton("Create ...", self)
publish_btn = QtWidgets.QPushButton("Publish ...", self)
load_btn = QtWidgets.QPushButton("Load ...", self)
inventory_btn = QtWidgets.QPushButton("Inventory ...", self)
libload_btn = QtWidgets.QPushButton("Library ...", self)
rendermode_btn = QtWidgets.QPushButton("Set render mode ...", self)
workfiles_btn = QtWidgets.QPushButton("Workfiles...", self)
create_btn = QtWidgets.QPushButton("Create...", self)
publish_btn = QtWidgets.QPushButton("Publish...", self)
load_btn = QtWidgets.QPushButton("Load...", self)
inventory_btn = QtWidgets.QPushButton("Inventory...", self)
libload_btn = QtWidgets.QPushButton("Library...", self)
rendermode_btn = QtWidgets.QPushButton("Set render mode...", self)
duplicate_with_inputs_btn = QtWidgets.QPushButton(
"Duplicate with input connections", self
)

View file

@ -31,13 +31,6 @@ def install():
"""
# Disable all families except for the ones we explicitly want to see
family_states = ["imagesequence",
"camera",
"pointcache"]
avalon.data["familiesStateDefault"] = False
avalon.data["familiesStateToggled"] = family_states
log.info("openpype.hosts.fusion installed")
pyblish.register_host("fusion")

View file

@ -4,7 +4,8 @@ import os
def add_implementation_envs(env, _app):
"""Modify environments to contain all required for implementation."""
openharmony_path = os.path.join(
os.environ["OPENPYPE_REPOS_ROOT"], "pype", "vendor", "OpenHarmony"
os.environ["OPENPYPE_REPOS_ROOT"], "openpype", "hosts",
"harmony", "vendor", "OpenHarmony"
)
# TODO check if is already set? What to do if is already set?
env["LIB_OPENHARMONY_PATH"] = openharmony_path

View file

@ -0,0 +1,655 @@
# Harmony Integration
## Setup
The easiest way to setup for using Toon Boom Harmony is to use the built-in launch:
```
python -c "import openpype.hosts.harmony.api as harmony;harmony.launch("path/to/harmony/executable")"
```
Communication with Harmony happens with a server/client relationship where the server is in the Python process and the client is in the Harmony process. Messages between Python and Harmony are required to be dictionaries, which are serialized to strings:
```
+------------+
| |
| Python |
| Process |
| |
| +--------+ |
| | | |
| | Main | |
| | Thread | |
| | | |
| +----^---+ |
| || |
| || |
| +---v----+ | +---------+
| | | | | |
| | Server +-------> Harmony |
| | Thread <-------+ Process |
| | | | | |
| +--------+ | +---------+
+------------+
```
Server/client now uses stricter protocol to handle communication. This is necessary because of precise control over data passed between server/client. Each message is prepended with 6 bytes:
```
| A | H | 0x00 | 0x00 | 0x00 | 0x00 | ...
```
First two bytes are *magic* bytes stands for **A**valon **H**armony. Next four bytes hold length of the message `...` encoded as 32bit unsigned integer. This way we know how many bytes to read from the socket and if we need more or we need to parse multiple messages.
## Usage
The integration creates an `Openpype` menu entry where all 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 moves 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
### Low level messaging
To send from Python to Harmony you can use the exposed method:
```python
import openpype.hosts.harmony.api as harmony
from uuid import uuid4
func = """function %s_hello(person)
{
return ("Hello " + person + "!");
}
%s_hello
""" % (uuid4(), uuid4())
print(harmony.send({"function": func, "args": ["Python"]})["result"])
```
**NOTE:** Its important to declare the function at the end of the function string. You can have multiple functions within your function string, but the function declared at the end is what gets executed.
To send a function with multiple arguments its best to declare the arguments within the function:
```python
import openpype.hosts.harmony.api as harmony
from uuid import uuid4
signature = str(uuid4()).replace("-", "_")
func = """function %s_hello(args)
{
var greeting = args[0];
var person = args[1];
return (greeting + " " + person + "!");
}
%s_hello
""" % (signature, signature)
print(harmony.send({"function": func, "args": ["Hello", "Python"]})["result"])
```
### Caution
When naming your functions be aware that they are executed in global scope. They can potentially clash with Harmony own function and object names.
For example `func` is already existing Harmony object. When you call your function `func` it will overwrite in global scope the one from Harmony, causing
erratic behavior of Harmony. Openpype is prefixing those function names with [UUID4](https://docs.python.org/3/library/uuid.html) making chance of such clash minimal.
See above examples how that works. This will result in function named `38dfcef0_a6d7_4064_8069_51fe99ab276e_hello()`.
You can find list of Harmony object and function in Harmony documentation.
### Higher level (recommended)
Instead of sending functions directly to Harmony, it is more efficient and safe to just add your code to `js/PypeHarmony.js` or utilize `{"script": "..."}` method.
#### Extending PypeHarmony.js
Add your function to `PypeHarmony.js`. For example:
```javascript
PypeHarmony.myAwesomeFunction = function() {
someCoolStuff();
};
```
Then you can call that javascript code from your Python like:
```Python
import openpype.hosts.harmony.api as harmony
harmony.send({"function": "PypeHarmony.myAwesomeFunction"});
```
#### Using Script method
You can also pass whole scripts into harmony and call their functions later as needed.
For example, you have bunch of javascript files:
```javascript
/* Master.js */
var Master = {
Foo = {};
Boo = {};
};
/* FileA.js */
var Foo = function() {};
Foo.prototype.A = function() {
someAStuff();
}
// This will construct object Foo and add it to Master namespace.
Master.Foo = new Foo();
/* FileB.js */
var Boo = function() {};
Boo.prototype.B = function() {
someBStuff();
}
// This will construct object Boo and add it to Master namespace.
Master.Boo = new Boo();
```
Now in python, just read all those files and send them to Harmony.
```python
from pathlib import Path
import openpype.hosts.harmony.api as harmony
path_to_js = Path('/path/to/my/js')
script_to_send = ""
for file in path_to_js.iterdir():
if file.suffix == ".js":
script_to_send += file.read_text()
harmony.send({"script": script_to_send})
# and use your code in Harmony
harmony.send({"function": "Master.Boo.B"})
```
### Scene Save
Instead of sending a request to Harmony with `scene.saveAll` please use:
```python
import openpype.hosts.harmony.api as harmony
harmony.save_scene()
```
<details>
<summary>Click to expand for details on scene save.</summary>
Because Openpype tools does not deal well with folders for a single entity like a Harmony scene, this integration has implemented to use zip files to encapsulate the Harmony scene folders. Saving scene in Harmony via menu or CTRL+S will not result in producing zip file, only saving it from Workfiles will. This is because
zipping process can take some time in which we cannot block user from saving again. If xstage file is changed during zipping process it will produce corrupted zip
archive.
</details>
### Plugin Examples
These plugins were made with the [polly config](https://github.com/mindbender-studio/config).
#### Creator Plugin
```python
import openpype.hosts.harmony.api as harmony
from uuid import uuid4
class CreateComposite(harmony.Creator):
"""Composite node for publish."""
name = "compositeDefault"
label = "Composite"
family = "mindbender.template"
def __init__(self, *args, **kwargs):
super(CreateComposite, self).__init__(*args, **kwargs)
```
The creator plugin can be configured to use other node types. For example here is a write node creator:
```python
import openpype.hosts.harmony.api as harmony
class CreateRender(harmony.Creator):
"""Composite node for publishing renders."""
name = "writeDefault"
label = "Write"
family = "mindbender.imagesequence"
node_type = "WRITE"
def __init__(self, *args, **kwargs):
super(CreateRender, self).__init__(*args, **kwargs)
def setup_node(self, node):
signature = str(uuid4()).replace("-", "_")
func = """function %s_func(args)
{
node.setTextAttr(args[0], "DRAWING_TYPE", 1, "PNG4");
}
%s_func
""" % (signature, signature)
harmony.send(
{"function": func, "args": [node]}
)
```
#### Collector Plugin
```python
import pyblish.api
import openpype.hosts.harmony.api as 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
import openpype.hosts.harmony.api as 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"
signature = str(uuid4()).replace("-", "_")
func = """function %s_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
}
%s_func
""" % (signature, signature)
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 %s_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);
}}
%s_func
""" % (signature, signature)
restore_func = """function %s_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);
}
%s_func
""" % (signature, signature)
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()))
```
#### Loader Plugin
```python
import os
from avalon import api, io
import openpype.hosts.harmony.api as harmony
signature = str(uuid4()).replace("-", "_")
copy_files = """function copyFile(srcFilename, dstFilename)
{
var srcFile = new PermanentFile(srcFilename);
var dstFile = new PermanentFile(dstFilename);
srcFile.copy(dstFile);
}
"""
import_files = """function %s_import_files()
{
var PNGTransparencyMode = 0; // Premultiplied wih Black
var TGATransparencyMode = 0; // Premultiplied wih Black
var SGITransparencyMode = 0; // Premultiplied wih Black
var LayeredPSDTransparencyMode = 1; // Straight
var FlatPSDTransparencyMode = 2; // Premultiplied wih White
function getUniqueColumnName( column_prefix )
{
var suffix = 0;
// finds if unique name for a column
var column_name = column_prefix;
while(suffix < 2000)
{
if(!column.type(column_name))
break;
suffix = suffix + 1;
column_name = column_prefix + "_" + suffix;
}
return column_name;
}
function import_files(args)
{
var root = args[0];
var files = args[1];
var name = args[2];
var start_frame = args[3];
var vectorFormat = null;
var extension = null;
var filename = files[0];
var pos = filename.lastIndexOf(".");
if( pos < 0 )
return null;
extension = filename.substr(pos+1).toLowerCase();
if(extension == "jpeg")
extension = "jpg";
if(extension == "tvg")
{
vectorFormat = "TVG"
extension ="SCAN"; // element.add() will use this.
}
var elemId = element.add(
name,
"BW",
scene.numberOfUnitsZ(),
extension.toUpperCase(),
vectorFormat
);
if (elemId == -1)
{
// hum, unknown file type most likely -- let's skip it.
return null; // no read to add.
}
var uniqueColumnName = getUniqueColumnName(name);
column.add(uniqueColumnName , "DRAWING");
column.setElementIdOfDrawing(uniqueColumnName, elemId);
var read = node.add(root, name, "READ", 0, 0, 0);
var transparencyAttr = node.getAttr(
read, frame.current(), "READ_TRANSPARENCY"
);
var opacityAttr = node.getAttr(read, frame.current(), "OPACITY");
transparencyAttr.setValue(true);
opacityAttr.setValue(true);
var alignmentAttr = node.getAttr(read, frame.current(), "ALIGNMENT_RULE");
alignmentAttr.setValue("ASIS");
var transparencyModeAttr = node.getAttr(
read, frame.current(), "applyMatteToColor"
);
if (extension == "png")
transparencyModeAttr.setValue(PNGTransparencyMode);
if (extension == "tga")
transparencyModeAttr.setValue(TGATransparencyMode);
if (extension == "sgi")
transparencyModeAttr.setValue(SGITransparencyMode);
if (extension == "psd")
transparencyModeAttr.setValue(FlatPSDTransparencyMode);
node.linkAttr(read, "DRAWING.ELEMENT", uniqueColumnName);
// Create a drawing for each file.
for( var i =0; i <= files.length - 1; ++i)
{
timing = start_frame + i
// Create a drawing drawing, 'true' indicate that the file exists.
Drawing.create(elemId, timing, true);
// Get the actual path, in tmp folder.
var drawingFilePath = Drawing.filename(elemId, timing.toString());
copyFile( files[i], drawingFilePath );
column.setEntry(uniqueColumnName, 1, timing, timing.toString());
}
return read;
}
import_files();
}
%s_import_files
""" % (signature, signature)
replace_files = """function %s_replace_files(args)
{
var files = args[0];
var _node = args[1];
var start_frame = args[2];
var _column = node.linkedColumn(_node, "DRAWING.ELEMENT");
// Delete existing drawings.
var timings = column.getDrawingTimings(_column);
for( var i =0; i <= timings.length - 1; ++i)
{
column.deleteDrawingAt(_column, parseInt(timings[i]));
}
// Create new drawings.
for( var i =0; i <= files.length - 1; ++i)
{
timing = start_frame + i
// Create a drawing drawing, 'true' indicate that the file exists.
Drawing.create(node.getElementId(_node), timing, true);
// Get the actual path, in tmp folder.
var drawingFilePath = Drawing.filename(
node.getElementId(_node), timing.toString()
);
copyFile( files[i], drawingFilePath );
column.setEntry(_column, 1, timing, timing.toString());
}
}
%s_replace_files
""" % (signature, signature)
class ImageSequenceLoader(api.Loader):
"""Load images
Stores the imported asset in a container named after the asset.
"""
families = ["mindbender.imagesequence"]
representations = ["*"]
def load(self, context, name=None, namespace=None, data=None):
files = []
for f in context["version"]["data"]["files"]:
files.append(
os.path.join(
context["version"]["data"]["stagingDir"], f
).replace("\\", "/")
)
read_node = harmony.send(
{
"function": copy_files + import_files,
"args": ["Top", files, context["version"]["data"]["subset"], 1]
}
)["result"]
self[:] = [read_node]
return harmony.containerise(
name,
namespace,
read_node,
context,
self.__class__.__name__
)
def update(self, container, representation):
node = container.pop("node")
version = io.find_one({"_id": representation["parent"]})
files = []
for f in version["data"]["files"]:
files.append(
os.path.join(
version["data"]["stagingDir"], f
).replace("\\", "/")
)
harmony.send(
{
"function": copy_files + replace_files,
"args": [files, node, 1]
}
)
harmony.imprint(
node, {"representation": str(representation["_id"])}
)
def remove(self, container):
node = container.pop("node")
signature = str(uuid4()).replace("-", "_")
func = """function %s_deleteNode(_node)
{
node.deleteNode(_node, true, true);
}
%_deleteNode
""" % (signature, signature)
harmony.send(
{"function": func, "args": [node]}
)
def switch(self, container, representation):
self.update(container, representation)
```
## 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)

View file

@ -0,0 +1,531 @@
/* global QTcpSocket, QByteArray, QDataStream, QTimer, QTextCodec, QIODevice, QApplication, include */
/* global QTcpSocket, QByteArray, QDataStream, QTimer, QTextCodec, QIODevice, QApplication, include */
/*
Avalon Harmony Integration - Client
-----------------------------------
This script implements client communication with Avalon server to bridge
gap between Python and QtScript.
*/
/* jshint proto: true */
var LD_OPENHARMONY_PATH = System.getenv('LIB_OPENHARMONY_PATH');
LD_OPENHARMONY_PATH = LD_OPENHARMONY_PATH + '/openHarmony.js';
LD_OPENHARMONY_PATH = LD_OPENHARMONY_PATH.replace(/\\/g, "/");
include(LD_OPENHARMONY_PATH);
this.__proto__['$'] = $;
function Client() {
var self = this;
/** socket */
self.socket = new QTcpSocket(this);
/** receiving data buffer */
self.received = '';
self.messageId = 1;
self.buffer = new QByteArray();
self.waitingForData = 0;
/**
* pack number into bytes.
* @function
* @param {int} num 32 bit integer
* @return {string}
*/
self.pack = function(num) {
var ascii='';
for (var i = 3; i >= 0; i--) {
ascii += String.fromCharCode((num >> (8 * i)) & 255);
}
return ascii;
};
/**
* unpack number from string.
* @function
* @param {string} numString bytes to unpack
* @return {int} 32bit unsigned integer.
*/
self.unpack = function(numString) {
var result=0;
for (var i = 3; i >= 0; i--) {
result += numString.charCodeAt(3 - i) << (8 * i);
}
return result;
};
/**
* prettify json for easier debugging
* @function
* @param {object} json json to process
* @return {string} prettified json string
*/
self.prettifyJson = function(json) {
var jsonString = JSON.stringify(json);
return JSON.stringify(JSON.parse(jsonString), null, 2);
};
/**
* Log message in debug level.
* @function
* @param {string} data - message
*/
self.logDebug = function(data) {
var message = typeof(data.message) != 'undefined' ? data.message : data;
MessageLog.trace('(DEBUG): ' + message.toString());
};
/**
* Log message in info level.
* @function
* @param {string} data - message
*/
self.logInfo = function(data) {
var message = typeof(data.message) != 'undefined' ? data.message : data;
MessageLog.trace('(DEBUG): ' + message.toString());
};
/**
* Log message in warning level.
* @function
* @param {string} data - message
*/
self.logWarning = function(data) {
var message = typeof(data.message) != 'undefined' ? data.message : data;
MessageLog.trace('(INFO): ' + message.toString());
};
/**
* Log message in error level.
* @function
* @param {string} data - message
*/
self.logError = function(data) {
var message = typeof(data.message) != 'undefined' ? data.message : data;
MessageLog.trace('(ERROR): ' +message.toString());
};
/**
* Show message in Harmony GUI as popup window.
* @function
* @param {string} msg - message
*/
self.showMessage = function(msg) {
MessageBox.information(msg);
};
/**
* Implement missing setTimeout() in Harmony.
* This calls once given function after some interval in milliseconds.
* @function
* @param {function} fc function to call.
* @param {int} interval interval in milliseconds.
* @param {boolean} single behave as setTimeout or setInterval.
*/
self.setTimeout = function(fc, interval, single) {
var timer = new QTimer();
if (!single) {
timer.singleShot = true; // in-case if setTimout and false in-case of setInterval
} else {
timer.singleShot = single;
}
timer.interval = interval; // set the time in milliseconds
timer.singleShot = true; // in-case if setTimout and false in-case of setInterval
timer.timeout.connect(this, function(){
fc.call();
});
timer.start();
};
/**
* Process recieved request. This will eval recieved function and produce
* results.
* @function
* @param {object} request - recieved request JSON
* @return {object} result of evaled function.
*/
self.processRequest = function(request) {
var mid = request.message_id;
if (typeof request.reply !== 'undefined') {
self.logDebug('['+ mid +'] *** received reply.');
return;
}
self.logDebug('['+ mid +'] - Processing: ' + self.prettifyJson(request));
var result = null;
if (typeof request.script !== 'undefined') {
self.logDebug('[' + mid + '] Injecting script.');
try {
eval.call(null, request.script);
} catch (error) {
self.logError(error);
}
} else if (typeof request["function"] !== 'undefined') {
try {
var _func = eval.call(null, request["function"]);
if (request.args == null) {
result = _func();
} else {
result = _func(request.args);
}
} catch (error) {
result = 'Error processing request.\n' +
'Request:\n' +
self.prettifyJson(request) + '\n' +
'Error:\n' + error;
}
} else {
self.logError('Command type not implemented.');
}
return result;
};
/**
* This gets called when socket received new data.
* @function
*/
self.onReadyRead = function() {
var currentSize = self.buffer.size();
self.logDebug('--- Receiving data ( '+ currentSize + ' )...');
var newData = self.socket.readAll();
var newSize = newData.size();
self.buffer.append(newData);
self.logDebug(' - got ' + newSize + ' bytes of new data.');
self.processBuffer();
};
/**
* Process data received in buffer.
* This detects messages by looking for header and message length.
* @function
*/
self.processBuffer = function() {
var length = self.waitingForData;
if (self.waitingForData == 0) {
// read header from the buffer and remove it
var header_data = self.buffer.mid(0, 6);
self.buffer = self.buffer.remove(0, 6);
// convert header to string
var header = '';
for (var i = 0; i < header_data.size(); ++i) {
// data in QByteArray come out as signed bytes.
var unsigned = header_data.at(i) & 0xff;
header = header.concat(String.fromCharCode(unsigned));
}
// skip 'AH' and read only length, unpack it to integer
header = header.substr(2);
length = self.unpack(header);
}
var data = self.buffer.mid(0, length);
self.logDebug('--- Expected: ' + length + ' | Got: ' + data.size());
if (length > data.size()) {
// we didn't received whole message.
self.waitingForData = length;
self.logDebug('... waiting for more data (' + length + ') ...');
return;
}
self.waitingForData = 0;
self.buffer.remove(0, length);
for (var j = 0; j < data.size(); ++j) {
self.received = self.received.concat(String.fromCharCode(data.at(j)));
}
// self.logDebug('--- Received: ' + self.received);
var to_parse = self.received;
var request = JSON.parse(to_parse);
var mid = request.message_id;
// self.logDebug('[' + mid + '] - Request: ' + '\n' + JSON.stringify(request));
self.logDebug('[' + mid + '] Recieved.');
request.result = self.processRequest(request);
self.logDebug('[' + mid + '] Processing done.');
self.received = '';
if (request.reply !== true) {
request.reply = true;
self.logDebug('[' + mid + '] Replying.');
self._send(JSON.stringify(request));
}
if ((length < data.size()) || (length < self.buffer.size())) {
// we've received more data.
self.logDebug('--- Got more data to process ...');
self.processBuffer();
}
};
/**
* Run when Harmony connects to server.
* @function
*/
self.onConnected = function() {
self.logDebug('Connected to server ...');
self.lock = false;
self.socket.readyRead.connect(self.onReadyRead);
var app = QCoreApplication.instance();
app.avalonClient.send(
{
'module': 'avalon.api',
'method': 'emit',
'args': ['application.launched']
}, false);
};
self._send = function(message) {
var data = new QByteArray();
var outstr = new QDataStream(data, QIODevice.WriteOnly);
outstr.writeInt(0);
data.append('UTF-8');
outstr.device().seek(0);
outstr.writeInt(data.size() - 4);
var codec = QTextCodec.codecForUtfText(data);
var msg = codec.fromUnicode(message);
var l = msg.size();
var coded = new QByteArray('AH').append(self.pack(l));
coded = coded.append(msg);
self.socket.write(new QByteArray(coded));
self.logDebug('Sent.');
};
self.waitForLock = function() {
if (self._lock === false) {
self.logDebug('>>> Unlocking ...');
return;
} else {
self.logDebug('Setting timer.');
self.setTimeout(self.waitForLock, 300);
}
};
/**
* Send request to server.
* @param {object} request - json encoded request.
*/
self.send = function(request) {
request.message_id = self.messageId;
if (typeof request.reply == 'undefined') {
self.logDebug("[" + self.messageId + "] sending:\n" + self.prettifyJson(request));
}
self._send(JSON.stringify(request));
};
/**
* Executed on disconnection.
*/
self.onDisconnected = function() {
self.socket.close();
};
/**
* Disconnect from server.
*/
self.disconnect = function() {
self.socket.close();
};
self.socket.connected.connect(self.onConnected);
self.socket.disconnected.connect(self.onDisconnected);
}
/**
* Entry point, creating Avalon Client.
*/
function start() {
var self = this;
/** hostname or ip of server - should be localhost */
var host = '127.0.0.1';
/** port of the server */
var port = parseInt(System.getenv('AVALON_HARMONY_PORT'));
// Attach the client to the QApplication to preserve.
var app = QCoreApplication.instance();
if (app.avalonClient == null) {
app.avalonClient = new Client();
app.avalonClient.socket.connectToHost(host, port);
}
var menuBar = QApplication.activeWindow().menuBar();
var actions = menuBar.actions();
app.avalonMenu = null;
for (var i = 0 ; i < actions.length; i++) {
label = System.getenv('AVALON_LABEL');
if (actions[i].text == label) {
app.avalonMenu = true;
}
}
var menu = null;
if (app.avalonMenu == null) {
menu = menuBar.addMenu(System.getenv('AVALON_LABEL'));
}
// menu = menuBar.addMenu('Avalon');
/**
* Show creator
*/
self.onCreator = function() {
app.avalonClient.send({
'module': 'openpype.hosts.harmony.api.lib',
'method': 'show',
'args': ['creator']
}, false);
};
var action = menu.addAction('Create...');
action.triggered.connect(self.onCreator);
/**
* Show Workfiles
*/
self.onWorkfiles = function() {
app.avalonClient.send({
'module': 'openpype.hosts.harmony.api.lib',
'method': 'show',
'args': ['workfiles']
}, false);
};
if (app.avalonMenu == null) {
action = menu.addAction('Workfiles...');
action.triggered.connect(self.onWorkfiles);
}
/**
* Show Loader
*/
self.onLoad = function() {
app.avalonClient.send({
'module': 'openpype.hosts.harmony.api.lib',
'method': 'show',
'args': ['loader']
}, false);
};
// add Loader item to menu
if (app.avalonMenu == null) {
action = menu.addAction('Load...');
action.triggered.connect(self.onLoad);
}
/**
* Show Publisher
*/
self.onPublish = function() {
app.avalonClient.send({
'module': 'openpype.hosts.harmony.api.lib',
'method': 'show',
'args': ['publish']
}, false);
};
// add Publisher item to menu
if (app.avalonMenu == null) {
action = menu.addAction('Publish...');
action.triggered.connect(self.onPublish);
}
/**
* Show Scene Manager
*/
self.onManage = function() {
app.avalonClient.send({
'module': 'openpype.hosts.harmony.api.lib',
'method': 'show',
'args': ['sceneinventory']
}, false);
};
// add Scene Manager item to menu
if (app.avalonMenu == null) {
action = menu.addAction('Manage...');
action.triggered.connect(self.onManage);
}
/**
* Show Subset Manager
*/
self.onSubsetManage = function() {
app.avalonClient.send({
'module': 'openpype.hosts.harmony.api.lib',
'method': 'show',
'args': ['subsetmanager']
}, false);
};
// add Subset Manager item to menu
if (app.avalonMenu == null) {
action = menu.addAction('Subset Manager...');
action.triggered.connect(self.onSubsetManage);
}
/**
* Show Experimental dialog
*/
self.onExperimentalTools = function() {
app.avalonClient.send({
'module': 'openpype.hosts.harmony.api.lib',
'method': 'show',
'args': ['experimental_tools']
}, false);
};
// add Subset Manager item to menu
if (app.avalonMenu == null) {
action = menu.addAction('Experimental Tools...');
action.triggered.connect(self.onExperimentalTools);
}
// FIXME(antirotor): We need to disable `on_file_changed` now as is wreak
// havoc when "Save" is called multiple times and zipping didn't finished yet
/*
// Watch scene file for changes.
app.onFileChanged = function(path)
{
var app = QCoreApplication.instance();
if (app.avalonOnFileChanged){
app.avalonClient.send(
{
'module': 'avalon.harmony.lib',
'method': 'on_file_changed',
'args': [path]
},
false
);
}
app.watcher.addPath(path);
};
app.watcher = new QFileSystemWatcher();
scene_path = scene.currentProjectPath() +"/" + scene.currentVersionName() + ".xstage";
app.watcher.addPath(scenePath);
app.watcher.fileChanged.connect(app.onFileChanged);
app.avalonOnFileChanged = true;
*/
app.onFileChanged = function(path) {
// empty stub
return path;
};
}
function ensureSceneSettings() {
var app = QCoreApplication.instance();
app.avalonClient.send(
{
"module": "openpype.hosts.harmony.api",
"method": "ensure_scene_settings",
"args": []
},
false
);
}
function TB_sceneOpened()
{
start();
}

View file

@ -1,209 +1,90 @@
# -*- coding: utf-8 -*-
"""Pype Harmony Host implementation."""
import os
from pathlib import Path
import logging
"""Public API
from openpype import lib
import openpype.hosts.harmony
Anything that isn't defined here is INTERNAL and unreliable for external use.
import pyblish.api
"""
from .pipeline import (
ls,
install,
list_instances,
remove_instance,
select_instance,
containerise,
set_scene_settings,
get_asset_settings,
ensure_scene_settings,
check_inventory,
application_launch,
export_template,
on_pyblish_instance_toggled,
inject_avalon_js,
)
from avalon import io, harmony
import avalon.api
from .lib import (
launch,
maintained_selection,
imprint,
read,
send,
maintained_nodes_state,
save_scene,
save_scene_as,
remove,
delete_node,
find_node_by_name,
signature,
select_nodes,
get_scene_data
)
from .workio import (
open_file,
save_file,
current_file,
has_unsaved_changes,
file_extensions,
work_root
)
log = logging.getLogger("openpype.hosts.harmony")
__all__ = [
# pipeline
"ls",
"install",
"list_instances",
"remove_instance",
"select_instance",
"containerise",
"set_scene_settings",
"get_asset_settings",
"ensure_scene_settings",
"check_inventory",
"application_launch",
"export_template",
"on_pyblish_instance_toggled",
"inject_avalon_js",
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.harmony.__file__))
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
# lib
"launch",
"maintained_selection",
"imprint",
"read",
"send",
"maintained_nodes_state",
"save_scene",
"save_scene_as",
"remove",
"delete_node",
"find_node_by_name",
"signature",
"select_nodes",
"get_scene_data",
# Workfiles API
"open_file",
"save_file",
"current_file",
"has_unsaved_changes",
"file_extensions",
"work_root",
]
def set_scene_settings(settings):
"""Set correct scene settings in Harmony.
Args:
settings (dict): Scene settings.
Returns:
dict: Dictionary of settings to set.
"""
harmony.send(
{"function": "PypeHarmony.setSceneSettings", "args": settings})
def get_asset_settings():
"""Get settings on current asset from database.
Returns:
dict: Scene data.
"""
asset_data = lib.get_asset()["data"]
fps = asset_data.get("fps")
frame_start = asset_data.get("frameStart")
frame_end = asset_data.get("frameEnd")
handle_start = asset_data.get("handleStart")
handle_end = asset_data.get("handleEnd")
resolution_width = asset_data.get("resolutionWidth")
resolution_height = asset_data.get("resolutionHeight")
entity_type = asset_data.get("entityType")
scene_data = {
"fps": fps,
"frameStart": frame_start,
"frameEnd": frame_end,
"handleStart": handle_start,
"handleEnd": handle_end,
"resolutionWidth": resolution_width,
"resolutionHeight": resolution_height,
"entityType": entity_type
}
return scene_data
def ensure_scene_settings():
"""Validate if Harmony scene has valid settings."""
settings = get_asset_settings()
invalid_settings = []
valid_settings = {}
for key, value in settings.items():
if value is None:
invalid_settings.append(key)
else:
valid_settings[key] = value
# Warn about missing attributes.
if invalid_settings:
msg = "Missing attributes:"
for item in invalid_settings:
msg += f"\n{item}"
harmony.send(
{"function": "PypeHarmony.message", "args": msg})
set_scene_settings(valid_settings)
def check_inventory():
"""Check is scene contains outdated containers.
If it does it will colorize outdated nodes and display warning message
in Harmony.
"""
if not lib.any_outdated():
return
host = avalon.api.registered_host()
outdated_containers = []
for container in host.ls():
representation = container['representation']
representation_doc = io.find_one(
{
"_id": io.ObjectId(representation),
"type": "representation"
},
projection={"parent": True}
)
if representation_doc and not lib.is_latest(representation_doc):
outdated_containers.append(container)
# Colour nodes.
outdated_nodes = []
for container in outdated_containers:
if container["loader"] == "ImageSequenceLoader":
outdated_nodes.append(
harmony.find_node_by_name(container["name"], "READ")
)
harmony.send({"function": "PypeHarmony.setColor", "args": outdated_nodes})
# Warn about outdated containers.
msg = "There are outdated containers in the scene."
harmony.send({"function": "PypeHarmony.message", "args": msg})
def application_launch():
"""Event that is executed after Harmony is launched."""
# FIXME: This is breaking server <-> client communication.
# It is now moved so it it manually called.
# ensure_scene_settings()
# check_inventory()
# fills OPENPYPE_HARMONY_JS
pype_harmony_path = Path(__file__).parent.parent / "js" / "PypeHarmony.js"
pype_harmony_js = pype_harmony_path.read_text()
# go through js/creators, loaders and publish folders and load all scripts
script = ""
for item in ["creators", "loaders", "publish"]:
dir_to_scan = Path(__file__).parent.parent / "js" / item
for child in dir_to_scan.iterdir():
script += child.read_text()
# send scripts to Harmony
harmony.send({"script": pype_harmony_js})
harmony.send({"script": script})
def export_template(backdrops, nodes, filepath):
"""Export Template to file.
Args:
backdrops (list): List of backdrops to export.
nodes (list): List of nodes to export.
filepath (str): Path where to save Template.
"""
harmony.send({
"function": "PypeHarmony.exportTemplate",
"args": [
backdrops,
nodes,
os.path.basename(filepath),
os.path.dirname(filepath)
]
})
def install():
"""Install Pype as host config."""
print("Installing Pype config ...")
pyblish.api.register_plugin_path(PUBLISH_PATH)
avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH)
avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH)
log.info(PUBLISH_PATH)
# Register callbacks.
pyblish.api.register_callback(
"instanceToggled", on_pyblish_instance_toggled
)
avalon.api.on("application.launched", application_launch)
def uninstall():
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH)
avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH)
def on_pyblish_instance_toggled(instance, old_value, new_value):
"""Toggle node enabling on instance toggles."""
node = None
if instance.data.get("setMembers"):
node = instance.data["setMembers"][0]
if node:
harmony.send(
{
"function": "PypeHarmony.toggleInstance",
"args": [node, new_value]
}
)

View file

@ -0,0 +1,117 @@
{
"env": {
"browser": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 3
},
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
},
"globals": {
"Action": "readonly",
"Backdrop": "readonly",
"Button": "readonly",
"Cel": "readonly",
"Cel3d": "readonly",
"CheckBox": "readonly",
"ColorRGBA": "readonly",
"ComboBox": "readonly",
"DateEdit": "readonly",
"DateEditEnum": "readonly",
"Dialog": "readonly",
"Dir": "readonly",
"DirSpec": "readonly",
"Drawing": "readonly",
"DrawingToolParams": "readonly",
"DrawingTools": "readonly",
"EnvelopeCreator": "readonly",
"ExportVideoDlg": "readonly",
"File": "readonly",
"FileAccess": "readonly",
"FileDialog": "readonly",
"GroupBox": "readonly",
"ImportDrawingDlg": "readonly",
"Input": "readonly",
"KeyModifiers": "readonly",
"Label": "readonly",
"LayoutExports": "readonly",
"LayoutExportsParams": "readonly",
"LineEdit": "readonly",
"Matrix4x4": "readonly",
"MessageBox": "readonly",
"MessageLog": "readonly",
"Model3d": "readonly",
"MovieImport": "readonly",
"NumberEdit": "readonly",
"PaletteManager": "readonly",
"PaletteObjectManager": "readonly",
"PermanentFile": "readonly",
"Point2d": "readonly",
"Point3d": "readonly",
"Process": "readonly",
"Process2": "readonly",
"Quaternion": "readonly",
"QuicktimeExporter": "readonly",
"RadioButton": "readonly",
"RemoteCmd": "readonly",
"Scene": "readonly",
"Settings": "readonly",
"Slider": "readonly",
"SpinBox": "readonly",
"SubnodeData": "readonly",
"System": "readonly",
"TemporaryFile": "readonly",
"TextEdit": "readonly",
"TimeEdit": "readonly",
"Timeline": "readonly",
"ToolProperties": "readonly",
"UiLoader": "readonly",
"Vector2d": "readonly",
"Vector3d": "readonly",
"WebCCExporter": "readonly",
"Workspaces": "readonly",
"__scriptManager__": "readonly",
"__temporaryFileContext__": "readonly",
"about": "readonly",
"column": "readonly",
"compositionOrder": "readonly",
"copyPaste": "readonly",
"deformation": "readonly",
"drawingExport": "readonly",
"element": "readonly",
"exporter": "readonly",
"fileMapper": "readonly",
"frame": "readonly",
"func": "readonly",
"library": "readonly",
"node": "readonly",
"preferences": "readonly",
"render": "readonly",
"scene": "readonly",
"selection": "readonly",
"sound": "readonly",
"specialFolders": "readonly",
"translator": "readonly",
"view": "readonly",
"waypoint": "readonly",
"xsheet": "readonly",
"QCoreApplication": "readonly"
}
}

View file

@ -0,0 +1,226 @@
// ***************************************************************************
// * Avalon Harmony Host *
// ***************************************************************************
/**
* @namespace
* @classdesc AvalonHarmony encapsulate all Avalon related functions.
*/
var AvalonHarmony = {};
/**
* Get scene metadata from Harmony.
* @function
* @return {object} Scene metadata.
*/
AvalonHarmony.getSceneData = function() {
var metadata = scene.metadata('avalon');
if (metadata){
return JSON.parse(metadata.value);
}else {
return {};
}
};
/**
* Set scene metadata to Harmony.
* @function
* @param {object} metadata Object containing metadata.
*/
AvalonHarmony.setSceneData = function(metadata) {
scene.setMetadata({
'name' : 'avalon',
'type' : 'string',
'creator' : 'Avalon',
'version' : '1.0',
'value' : JSON.stringify(metadata)
});
};
/**
* Get selected nodes in Harmony.
* @function
* @return {array} Selected nodes paths.
*/
AvalonHarmony.getSelectedNodes = function () {
var selectionLength = selection.numberOfNodesSelected();
var selectedNodes = [];
for (var i = 0 ; i < selectionLength; i++) {
selectedNodes.push(selection.selectedNode(i));
}
return selectedNodes;
};
/**
* Set selection of nodes.
* @function
* @param {array} nodes Arrya containing node paths to add to selection.
*/
AvalonHarmony.selectNodes = function(nodes) {
selection.clearSelection();
for (var i = 0 ; i < nodes.length; i++) {
selection.addNodeToSelection(nodes[i]);
}
};
/**
* Is node enabled?
* @function
* @param {string} node Node path.
* @return {boolean} state
*/
AvalonHarmony.isEnabled = function(node) {
return node.getEnable(node);
};
/**
* Are nodes enabled?
* @function
* @param {array} nodes Array of node paths.
* @return {array} array of boolean states.
*/
AvalonHarmony.areEnabled = function(nodes) {
var states = [];
for (var i = 0 ; i < nodes.length; i++) {
states.push(node.getEnable(nodes[i]));
}
return states;
};
/**
* Set state on nodes.
* @function
* @param {array} args Array of nodes array and states array.
*/
AvalonHarmony.setState = function(args) {
var nodes = args[0];
var states = args[1];
// length of both arrays must be equal.
if (nodes.length !== states.length) {
return false;
}
for (var i = 0 ; i < nodes.length; i++) {
node.setEnable(nodes[i], states[i]);
}
return true;
};
/**
* Disable specified nodes.
* @function
* @param {array} nodes Array of nodes.
*/
AvalonHarmony.disableNodes = function(nodes) {
for (var i = 0 ; i < nodes.length; i++)
{
node.setEnable(nodes[i], false);
}
};
/**
* Save scene in Harmony.
* @function
* @return {string} Scene path.
*/
AvalonHarmony.saveScene = function() {
var app = QCoreApplication.instance();
app.avalon_on_file_changed = false;
scene.saveAll();
return (
scene.currentProjectPath() + '/' +
scene.currentVersionName() + '.xstage'
);
};
/**
* Enable Harmony file-watcher.
* @function
*/
AvalonHarmony.enableFileWather = function() {
var app = QCoreApplication.instance();
app.avalon_on_file_changed = true;
};
/**
* Add path to file-watcher.
* @function
* @param {string} path Path to watch.
*/
AvalonHarmony.addPathToWatcher = function(path) {
var app = QCoreApplication.instance();
app.watcher.addPath(path);
};
/**
* Setup node for Creator.
* @function
* @param {string} node Node path.
*/
AvalonHarmony.setupNodeForCreator = function(node) {
node.setTextAttr(node, 'COMPOSITE_MODE', 1, 'Pass Through');
};
/**
* Get node names for specified node type.
* @function
* @param {string} nodeType Node type.
* @return {array} Node names.
*/
AvalonHarmony.getNodesNamesByType = function(nodeType) {
var nodes = node.getNodes(nodeType);
var nodeNames = [];
for (var i = 0; i < nodes.length; ++i) {
nodeNames.push(node.getName(nodes[i]));
}
return nodeNames;
};
/**
* Create container node in Harmony.
* @function
* @param {array} args Arguments, see example.
* @return {string} Resulting node.
*
* @example
* // arguments are in following order:
* var args = [
* nodeName,
* nodeType,
* selection
* ];
*/
AvalonHarmony.createContainer = function(args) {
var resultNode = node.add('Top', args[0], args[1], 0, 0, 0);
if (args.length > 2) {
node.link(args[2], 0, resultNode, 0, false, true);
node.setCoord(resultNode,
node.coordX(args[2]),
node.coordY(args[2]) + 70);
}
return resultNode;
};
/**
* Delete node.
* @function
* @param {string} node Node path.
*/
AvalonHarmony.deleteNode = function(_node) {
node.deleteNode(_node, true, true);
};

View file

@ -0,0 +1,18 @@
{
"name": "avalon-harmony",
"version": "1.0.0",
"description": "Avalon Harmony Host integration",
"keywords": [
"Avalon",
"Harmony",
"pipeline"
],
"license": "MIT",
"main": "AvalonHarmony.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"devDependencies": {
"eslint": "^7.11.0"
}
}

View file

@ -0,0 +1,622 @@
# -*- coding: utf-8 -*-
"""Utility functions used for Avalon - Harmony integration."""
import subprocess
import threading
import os
import random
import zipfile
import sys
import filecmp
import shutil
import logging
import contextlib
import json
import signal
import time
from uuid import uuid4
from Qt import QtWidgets, QtCore, QtGui
import collections
from .server import Server
from openpype.tools.stdout_broker.app import StdOutBroker
from openpype.tools.utils import host_tools
from openpype import style
from openpype.lib.applications import get_non_python_host_kwargs
# Setup logging.
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
class ProcessContext:
server = None
pid = None
process = None
application_path = None
callback_queue = collections.deque()
workfile_path = None
port = None
stdout_broker = None
workfile_tool = None
@classmethod
def execute_in_main_thread(cls, func_to_call_from_main_thread):
cls.callback_queue.append(func_to_call_from_main_thread)
@classmethod
def main_thread_listen(cls):
if cls.callback_queue:
callback = cls.callback_queue.popleft()
callback()
if cls.process is not None and cls.process.poll() is not None:
log.info("Server is not running, closing")
ProcessContext.stdout_broker.stop()
QtWidgets.QApplication.quit()
def signature(postfix="func") -> str:
"""Return random ECMA6 compatible function name.
Args:
postfix (str): name to append to random string.
Returns:
str: random function name.
"""
return "f{}_{}".format(str(uuid4()).replace("-", "_"), postfix)
class _ZipFile(zipfile.ZipFile):
"""Extended check for windows invalid characters."""
# this is extending default zipfile table for few invalid characters
# that can come from Mac
_windows_illegal_characters = ":<>|\"?*\r\n\x00"
_windows_illegal_name_trans_table = str.maketrans(
_windows_illegal_characters,
"_" * len(_windows_illegal_characters)
)
def main(*subprocess_args):
# coloring in StdOutBroker
os.environ["OPENPYPE_LOG_NO_COLORS"] = "False"
app = QtWidgets.QApplication([])
app.setQuitOnLastWindowClosed(False)
icon = QtGui.QIcon(style.get_app_icon_path())
app.setWindowIcon(icon)
ProcessContext.stdout_broker = StdOutBroker('harmony')
ProcessContext.stdout_broker.start()
launch(*subprocess_args)
loop_timer = QtCore.QTimer()
loop_timer.setInterval(20)
loop_timer.timeout.connect(ProcessContext.main_thread_listen)
loop_timer.start()
sys.exit(app.exec_())
def setup_startup_scripts():
"""Manages installation of avalon's TB_sceneOpened.js for Harmony launch.
If a studio already has defined "TOONBOOM_GLOBAL_SCRIPT_LOCATION", copies
the TB_sceneOpened.js to that location if the file is different.
Otherwise, will set the env var to point to the avalon/harmony folder.
Admins should be aware that this will overwrite TB_sceneOpened in the
"TOONBOOM_GLOBAL_SCRIPT_LOCATION", and that if they want to have additional
logic, they will need to one of the following:
* Create a Harmony package to manage startup logic
* Use TB_sceneOpenedUI.js instead to manage startup logic
* Add their startup logic to avalon/harmony/TB_sceneOpened.js
"""
avalon_dcc_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)),
"api")
startup_js = "TB_sceneOpened.js"
if os.getenv("TOONBOOM_GLOBAL_SCRIPT_LOCATION"):
avalon_harmony_startup = os.path.join(avalon_dcc_dir, startup_js)
env_harmony_startup = os.path.join(
os.getenv("TOONBOOM_GLOBAL_SCRIPT_LOCATION"), startup_js)
if not filecmp.cmp(avalon_harmony_startup, env_harmony_startup):
try:
shutil.copy(avalon_harmony_startup, env_harmony_startup)
except Exception as e:
log.error(e)
log.warning(
"Failed to copy {0} to {1}! "
"Defaulting to Avalon TOONBOOM_GLOBAL_SCRIPT_LOCATION."
.format(avalon_harmony_startup, env_harmony_startup))
os.environ["TOONBOOM_GLOBAL_SCRIPT_LOCATION"] = avalon_dcc_dir
else:
os.environ["TOONBOOM_GLOBAL_SCRIPT_LOCATION"] = avalon_dcc_dir
def check_libs():
"""Check if `OpenHarmony`_ is available.
Avalon expects either path in `LIB_OPENHARMONY_PATH` or `openHarmony.js`
present in `TOONBOOM_GLOBAL_SCRIPT_LOCATION`.
Throws:
RuntimeError: If openHarmony is not found.
.. _OpenHarmony:
https://github.com/cfourney/OpenHarmony
"""
if not os.getenv("LIB_OPENHARMONY_PATH"):
if os.getenv("TOONBOOM_GLOBAL_SCRIPT_LOCATION"):
if os.path.exists(
os.path.join(
os.getenv("TOONBOOM_GLOBAL_SCRIPT_LOCATION"),
"openHarmony.js")):
os.environ["LIB_OPENHARMONY_PATH"] = \
os.getenv("TOONBOOM_GLOBAL_SCRIPT_LOCATION")
return
else:
log.error(("Cannot find OpenHarmony library. "
"Please set path to it in LIB_OPENHARMONY_PATH "
"environment variable."))
raise RuntimeError("Missing OpenHarmony library.")
def launch(application_path, *args):
"""Set Harmony for launch.
Launches Harmony and the server, then starts listening on the main thread
for callbacks from the server. This is to have Qt applications run in the
main thread.
Args:
application_path (str): Path to Harmony.
"""
from avalon import api
from openpype.hosts.harmony import api as harmony
api.install(harmony)
ProcessContext.port = random.randrange(49152, 65535)
os.environ["AVALON_HARMONY_PORT"] = str(ProcessContext.port)
ProcessContext.application_path = application_path
# Launch Harmony.
setup_startup_scripts()
check_libs()
if not os.environ.get("AVALON_HARMONY_WORKFILES_ON_LAUNCH", False):
open_empty_workfile()
return
ProcessContext.workfile_tool = host_tools.get_tool_by_name("workfiles")
host_tools.show_workfiles(save=False)
ProcessContext.execute_in_main_thread(check_workfiles_tool)
def check_workfiles_tool():
if ProcessContext.workfile_tool.isVisible():
ProcessContext.execute_in_main_thread(check_workfiles_tool)
elif not ProcessContext.workfile_path:
open_empty_workfile()
def open_empty_workfile():
zip_file = os.path.join(os.path.dirname(__file__), "temp.zip")
temp_path = get_local_harmony_path(zip_file)
if os.path.exists(temp_path):
log.info(f"removing existing {temp_path}")
try:
shutil.rmtree(temp_path)
except Exception as e:
log.critical(f"cannot clear {temp_path}")
raise Exception(f"cannot clear {temp_path}") from e
launch_zip_file(zip_file)
def get_local_harmony_path(filepath):
"""From the provided path get the equivalent local Harmony path."""
basename = os.path.splitext(os.path.basename(filepath))[0]
harmony_path = os.path.join(os.path.expanduser("~"), ".avalon", "harmony")
return os.path.join(harmony_path, basename)
def launch_zip_file(filepath):
"""Launch a Harmony application instance with the provided zip file.
Args:
filepath (str): Path to file.
"""
print(f"Localizing {filepath}")
temp_path = get_local_harmony_path(filepath)
scene_path = os.path.join(
temp_path, os.path.basename(temp_path) + ".xstage"
)
unzip = False
if os.path.exists(scene_path):
# Check remote scene is newer than local.
if os.path.getmtime(scene_path) < os.path.getmtime(filepath):
try:
shutil.rmtree(temp_path)
except Exception as e:
log.error(e)
raise Exception("Cannot delete working folder") from e
unzip = True
else:
unzip = True
if unzip:
with _ZipFile(filepath, "r") as zip_ref:
zip_ref.extractall(temp_path)
# Close existing scene.
if ProcessContext.pid:
os.kill(ProcessContext.pid, signal.SIGTERM)
# Stop server.
if ProcessContext.server:
ProcessContext.server.stop()
# Launch Avalon server.
ProcessContext.server = Server(ProcessContext.port)
ProcessContext.server.start()
# thread = threading.Thread(target=self.server.start)
# thread.daemon = True
# thread.start()
# Save workfile path for later.
ProcessContext.workfile_path = filepath
# find any xstage files is directory, prefer the one with the same name
# as directory (plus extension)
xstage_files = []
for _, _, files in os.walk(temp_path):
for file in files:
if os.path.splitext(file)[1] == ".xstage":
xstage_files.append(file)
if not os.path.basename("temp.zip"):
if not xstage_files:
ProcessContext.server.stop()
print("no xstage file was found")
return
# try to use first available
scene_path = os.path.join(
temp_path, xstage_files[0]
)
# prefer the one named as zip file
zip_based_name = "{}.xstage".format(
os.path.splitext(os.path.basename(filepath))[0])
if zip_based_name in xstage_files:
scene_path = os.path.join(
temp_path, zip_based_name
)
if not os.path.exists(scene_path):
print("error: cannot determine scene file")
ProcessContext.server.stop()
return
print("Launching {}".format(scene_path))
kwargs = get_non_python_host_kwargs({}, False)
process = subprocess.Popen(
[ProcessContext.application_path, scene_path],
**kwargs
)
ProcessContext.pid = process.pid
ProcessContext.process = process
ProcessContext.stdout_broker.host_connected()
def on_file_changed(path, threaded=True):
"""Threaded zipping and move of the project directory.
This method is called when the `.xstage` file is changed.
"""
log.debug("File changed: " + path)
if ProcessContext.workfile_path is None:
return
if threaded:
thread = threading.Thread(
target=zip_and_move,
args=(os.path.dirname(path), ProcessContext.workfile_path)
)
thread.start()
else:
zip_and_move(os.path.dirname(path), ProcessContext.workfile_path)
def zip_and_move(source, destination):
"""Zip a directory and move to `destination`.
Args:
source (str): Directory to zip and move to destination.
destination (str): Destination file path to zip file.
"""
os.chdir(os.path.dirname(source))
shutil.make_archive(os.path.basename(source), "zip", source)
with _ZipFile(os.path.basename(source) + ".zip") as zr:
if zr.testzip() is not None:
raise Exception("File archive is corrupted.")
shutil.move(os.path.basename(source) + ".zip", destination)
log.debug(f"Saved '{source}' to '{destination}'")
def show(module_name):
"""Call show on "module_name".
This allows to make a QApplication ahead of time and always "exec_" to
prevent crashing.
Args:
module_name (str): Name of module to call "show" on.
"""
# Requests often get doubled up when showing tools, so we wait a second for
# requests to be received properly.
time.sleep(1)
# Get tool name from module name
# TODO this is for backwards compatibility not sure if `TB_sceneOpened.js`
# is automatically updated.
# Previous javascript sent 'module_name' which contained whole tool import
# string e.g. "avalon.tools.workfiles" now it should be only "workfiles"
tool_name = module_name.split(".")[-1]
kwargs = {}
if tool_name == "loader":
kwargs["use_context"] = True
ProcessContext.execute_in_main_thread(
lambda: host_tools.show_tool_by_name(tool_name, **kwargs)
)
# Required return statement.
return "nothing"
def get_scene_data():
try:
return send(
{
"function": "AvalonHarmony.getSceneData"
})["result"]
except json.decoder.JSONDecodeError:
# Means no sceen metadata has been made before.
return {}
except KeyError:
# Means no existing scene metadata has been made.
return {}
def set_scene_data(data):
"""Write scene data to metadata.
Args:
data (dict): Data to write.
"""
# Write scene data.
send(
{
"function": "AvalonHarmony.setSceneData",
"args": data
})
def read(node_id):
"""Read object metadata in to a dictionary.
Args:
node_id (str): Path to node or id of object.
Returns:
dict
"""
scene_data = get_scene_data()
if node_id in scene_data:
return scene_data[node_id]
return {}
def remove(node_id):
"""
Remove node data from scene metadata.
Args:
node_id (str): full name (eg. 'Top/renderAnimation')
"""
data = get_scene_data()
del data[node_id]
set_scene_data(data)
def delete_node(node):
""" Physically delete node from scene. """
send(
{
"function": "AvalonHarmony.deleteNode",
"args": node
}
)
def imprint(node_id, data, remove=False):
"""Write `data` to the `node` as json.
Arguments:
node_id (str): Path to node or id of object.
data (dict): Dictionary of key/value pairs.
remove (bool): Removes the data from the scene.
Example:
>>> from avalon.harmony import lib
>>> node = "Top/Display"
>>> data = {"str": "someting", "int": 1, "float": 0.32, "bool": True}
>>> lib.imprint(layer, data)
"""
scene_data = get_scene_data()
if remove and (node_id in scene_data):
scene_data.pop(node_id, None)
else:
if node_id in scene_data:
scene_data[node_id].update(data)
else:
scene_data[node_id] = data
set_scene_data(scene_data)
@contextlib.contextmanager
def maintained_selection():
"""Maintain selection during context."""
selected_nodes = send(
{
"function": "AvalonHarmony.getSelectedNodes"
})["result"]
try:
yield selected_nodes
finally:
selected_nodes = send(
{
"function": "AvalonHarmony.selectNodes",
"args": selected_nodes
}
)
def send(request):
"""Public method for sending requests to Harmony."""
return ProcessContext.server.send(request)
def select_nodes(nodes):
""" Selects nodes in Node View """
_ = send(
{
"function": "AvalonHarmony.selectNodes",
"args": nodes
}
)
@contextlib.contextmanager
def maintained_nodes_state(nodes):
"""Maintain nodes states during context."""
# Collect current state.
states = send(
{
"function": "AvalonHarmony.areEnabled", "args": nodes
})["result"]
# Disable all nodes.
send(
{
"function": "AvalonHarmony.disableNodes", "args": nodes
})
try:
yield
finally:
send(
{
"function": "AvalonHarmony.setState",
"args": [nodes, states]
})
def save_scene():
"""Save the Harmony scene safely.
The built-in (to Avalon) background zip and moving of the Harmony scene
folder, interfers with server/client communication by sending two requests
at the same time. This only happens when sending "scene.saveAll()". This
method prevents this double request and safely saves the scene.
"""
# Need to turn off the backgound watcher else the communication with
# the server gets spammed with two requests at the same time.
scene_path = send(
{"function": "AvalonHarmony.saveScene"})["result"]
# Manually update the remote file.
on_file_changed(scene_path, threaded=False)
# Re-enable the background watcher.
send({"function": "AvalonHarmony.enableFileWather"})
def save_scene_as(filepath):
"""Save Harmony scene as `filepath`."""
scene_dir = os.path.dirname(filepath)
destination = os.path.join(
os.path.dirname(ProcessContext.workfile_path),
os.path.splitext(os.path.basename(filepath))[0] + ".zip"
)
if os.path.exists(scene_dir):
try:
shutil.rmtree(scene_dir)
except Exception as e:
log.error(f"Cannot remove {scene_dir}")
raise Exception(f"Cannot remove {scene_dir}") from e
send(
{"function": "scene.saveAs", "args": [scene_dir]}
)["result"]
zip_and_move(scene_dir, destination)
ProcessContext.workfile_path = destination
send(
{"function": "AvalonHarmony.addPathToWatcher", "args": filepath}
)
def find_node_by_name(name, node_type):
"""Find node by its name.
Args:
name (str): Name of the Node. (without part before '/')
node_type (str): Type of the Node.
'READ' - for loaded data with Loaders (background)
'GROUP' - for loaded data with Loaders (templates)
'WRITE' - render nodes
Returns:
str: FQ Node name.
"""
nodes = send(
{"function": "node.getNodes", "args": [[node_type]]}
)["result"]
for node in nodes:
node_name = node.split("/")[-1]
if name == node_name:
return node
return None

View file

@ -0,0 +1,351 @@
import os
from pathlib import Path
import logging
import pyblish.api
from avalon import io
import avalon.api
from avalon.pipeline import AVALON_CONTAINER_ID
from openpype import lib
import openpype.hosts.harmony
import openpype.hosts.harmony.api as harmony
log = logging.getLogger("openpype.hosts.harmony")
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.harmony.__file__))
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
def set_scene_settings(settings):
"""Set correct scene settings in Harmony.
Args:
settings (dict): Scene settings.
Returns:
dict: Dictionary of settings to set.
"""
harmony.send(
{"function": "PypeHarmony.setSceneSettings", "args": settings})
def get_asset_settings():
"""Get settings on current asset from database.
Returns:
dict: Scene data.
"""
asset_data = lib.get_asset()["data"]
fps = asset_data.get("fps")
frame_start = asset_data.get("frameStart")
frame_end = asset_data.get("frameEnd")
handle_start = asset_data.get("handleStart")
handle_end = asset_data.get("handleEnd")
resolution_width = asset_data.get("resolutionWidth")
resolution_height = asset_data.get("resolutionHeight")
entity_type = asset_data.get("entityType")
scene_data = {
"fps": fps,
"frameStart": frame_start,
"frameEnd": frame_end,
"handleStart": handle_start,
"handleEnd": handle_end,
"resolutionWidth": resolution_width,
"resolutionHeight": resolution_height,
"entityType": entity_type
}
return scene_data
def ensure_scene_settings():
"""Validate if Harmony scene has valid settings."""
settings = get_asset_settings()
invalid_settings = []
valid_settings = {}
for key, value in settings.items():
if value is None:
invalid_settings.append(key)
else:
valid_settings[key] = value
# Warn about missing attributes.
if invalid_settings:
msg = "Missing attributes:"
for item in invalid_settings:
msg += f"\n{item}"
harmony.send(
{"function": "PypeHarmony.message", "args": msg})
set_scene_settings(valid_settings)
def check_inventory():
"""Check is scene contains outdated containers.
If it does it will colorize outdated nodes and display warning message
in Harmony.
"""
if not lib.any_outdated():
return
host = avalon.api.registered_host()
outdated_containers = []
for container in host.ls():
representation = container['representation']
representation_doc = io.find_one(
{
"_id": io.ObjectId(representation),
"type": "representation"
},
projection={"parent": True}
)
if representation_doc and not lib.is_latest(representation_doc):
outdated_containers.append(container)
# Colour nodes.
outdated_nodes = []
for container in outdated_containers:
if container["loader"] == "ImageSequenceLoader":
outdated_nodes.append(
harmony.find_node_by_name(container["name"], "READ")
)
harmony.send({"function": "PypeHarmony.setColor", "args": outdated_nodes})
# Warn about outdated containers.
msg = "There are outdated containers in the scene."
harmony.send({"function": "PypeHarmony.message", "args": msg})
def application_launch():
"""Event that is executed after Harmony is launched."""
# FIXME: This is breaking server <-> client communication.
# It is now moved so it it manually called.
# ensure_scene_settings()
# check_inventory()
# fills OPENPYPE_HARMONY_JS
pype_harmony_path = Path(__file__).parent.parent / "js" / "PypeHarmony.js"
pype_harmony_js = pype_harmony_path.read_text()
# go through js/creators, loaders and publish folders and load all scripts
script = ""
for item in ["creators", "loaders", "publish"]:
dir_to_scan = Path(__file__).parent.parent / "js" / item
for child in dir_to_scan.iterdir():
script += child.read_text()
# send scripts to Harmony
harmony.send({"script": pype_harmony_js})
harmony.send({"script": script})
inject_avalon_js()
def export_template(backdrops, nodes, filepath):
"""Export Template to file.
Args:
backdrops (list): List of backdrops to export.
nodes (list): List of nodes to export.
filepath (str): Path where to save Template.
"""
harmony.send({
"function": "PypeHarmony.exportTemplate",
"args": [
backdrops,
nodes,
os.path.basename(filepath),
os.path.dirname(filepath)
]
})
def install():
"""Install Pype as host config."""
print("Installing Pype config ...")
pyblish.api.register_host("harmony")
pyblish.api.register_plugin_path(PUBLISH_PATH)
avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH)
avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH)
log.info(PUBLISH_PATH)
# Register callbacks.
pyblish.api.register_callback(
"instanceToggled", on_pyblish_instance_toggled
)
avalon.api.on("application.launched", application_launch)
def uninstall():
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH)
avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH)
def on_pyblish_instance_toggled(instance, old_value, new_value):
"""Toggle node enabling on instance toggles."""
node = None
if instance.data.get("setMembers"):
node = instance.data["setMembers"][0]
if node:
harmony.send(
{
"function": "PypeHarmony.toggleInstance",
"args": [node, new_value]
}
)
def inject_avalon_js():
"""Inject AvalonHarmony.js into Harmony."""
avalon_harmony_js = Path(__file__).parent.joinpath("js/AvalonHarmony.js")
script = avalon_harmony_js.read_text()
# send AvalonHarmony.js to Harmony
harmony.send({"script": script})
def ls():
"""Yields containers from Harmony scene.
This is the host-equivalent of api.ls(), but instead of listing
assets on disk, it lists assets already loaded in Harmony; once loaded
they are called 'containers'.
Yields:
dict: container
"""
objects = harmony.get_scene_data() or {}
for _, data in objects.items():
# Skip non-tagged objects.
if not data:
continue
# Filter to only containers.
if "container" not in data.get("id"):
continue
if not data.get("objectName"): # backward compatibility
data["objectName"] = data["name"]
yield data
def list_instances(remove_orphaned=True):
"""
List all created instances from current workfile which
will be published.
Pulls from File > File Info
For SubsetManager, by default it check if instance has matching node
in the scene, if not, instance gets deleted from metadata.
Returns:
(list) of dictionaries matching instances format
"""
objects = harmony.get_scene_data() or {}
instances = []
for key, data in objects.items():
# Skip non-tagged objects.
if not data:
continue
# Filter out containers.
if "container" in data.get("id"):
continue
data['uuid'] = key
if remove_orphaned:
node_name = key.split("/")[-1]
located_node = harmony.find_node_by_name(node_name, 'WRITE')
if not located_node:
print("Removing orphaned instance {}".format(key))
harmony.remove(key)
continue
instances.append(data)
return instances
def remove_instance(instance):
"""
Remove instance from current workfile metadata and from scene!
Updates metadata of current file in File > File Info and removes
icon highlight on group layer.
For SubsetManager
Args:
instance (dict): instance representation from subsetmanager model
"""
node = instance.get("uuid")
harmony.remove(node)
harmony.delete_node(node)
def select_instance(instance):
"""
Select instance in Node View
Args:
instance (dict): instance representation from subsetmanager model
"""
harmony.select_nodes([instance.get("uuid")])
def containerise(name,
namespace,
node,
context,
loader=None,
suffix=None,
nodes=None):
"""Imprint node with metadata.
Containerisation enables a tracking of version, author and origin
for loaded assets.
Arguments:
name (str): Name of resulting assembly.
namespace (str): Namespace under which to host container.
node (str): Node to containerise.
context (dict): Asset information.
loader (str, optional): Name of loader used to produce this container.
suffix (str, optional): Suffix of container, defaults to `_CON`.
Returns:
container (str): Path of container assembly.
"""
if not nodes:
nodes = []
data = {
"schema": "openpype:container-2.0",
"id": AVALON_CONTAINER_ID,
"name": name,
"namespace": namespace,
"loader": str(loader),
"representation": str(context["representation"]["_id"]),
"nodes": nodes
}
harmony.imprint(node, data)
return node

View file

@ -1,6 +1,70 @@
from avalon import harmony
import avalon.api
from openpype.api import PypeCreatorMixin
import openpype.hosts.harmony.api as harmony
class Creator(PypeCreatorMixin, harmony.Creator):
pass
class Creator(PypeCreatorMixin, avalon.api.Creator):
"""Creator plugin to create instances in Harmony.
By default a Composite node is created to support any number of nodes in
an instance, but any node type is supported.
If the selection is used, the selected nodes will be connected to the
created node.
"""
node_type = "COMPOSITE"
def setup_node(self, node):
"""Prepare node as container.
Args:
node (str): Path to node.
"""
harmony.send(
{
"function": "AvalonHarmony.setupNodeForCreator",
"args": node
}
)
def process(self):
"""Plugin entry point."""
existing_node_names = harmony.send(
{
"function": "AvalonHarmony.getNodesNamesByType",
"args": self.node_type
})["result"]
# Dont allow instances with the same name.
msg = "Instance with name \"{}\" already exists.".format(self.name)
for name in existing_node_names:
if self.name.lower() == name.lower():
harmony.send(
{
"function": "AvalonHarmony.message", "args": msg
}
)
return False
with harmony.maintained_selection() as selection:
node = None
if (self.options or {}).get("useSelection") and selection:
node = harmony.send(
{
"function": "AvalonHarmony.createContainer",
"args": [self.name, self.node_type, selection[-1]]
}
)["result"]
else:
node = harmony.send(
{
"function": "AvalonHarmony.createContainer",
"args": [self.name, self.node_type]
}
)["result"]
harmony.imprint(node, self.data)
self.setup_node(node)
return node

View file

@ -0,0 +1,267 @@
# -*- coding: utf-8 -*-
"""Server-side implementation of Toon Boon Harmony communication."""
import socket
import logging
import json
import traceback
import importlib
import functools
import time
import struct
from datetime import datetime
import threading
from . import lib
class Server(threading.Thread):
"""Class for communication with Toon Boon Harmony.
Attributes:
connection (Socket): connection holding object.
received (str): received data buffer.any(iterable)
port (int): port number.
message_id (int): index of last message going out.
queue (dict): dictionary holding queue of incoming messages.
"""
def __init__(self, port):
"""Constructor."""
super(Server, self).__init__()
self.daemon = True
self.connection = None
self.received = ""
self.port = port
self.message_id = 1
# Setup logging.
self.log = logging.getLogger(__name__)
self.log.setLevel(logging.DEBUG)
# Create a TCP/IP socket
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Bind the socket to the port
server_address = ("127.0.0.1", port)
self.log.debug(
f"[{self.timestamp()}] Starting up on "
f"{server_address[0]}:{server_address[1]}")
self.socket.bind(server_address)
# Listen for incoming connections
self.socket.listen(1)
self.queue = {}
def process_request(self, request):
"""Process incoming request.
Args:
request (dict): {
"module": (str), # Module of method.
"method" (str), # Name of method in module.
"args" (list), # Arguments to pass to method.
"kwargs" (dict), # Keywork arguments to pass to method.
"reply" (bool), # Optional wait for method completion.
}
"""
pretty = self._pretty(request)
self.log.debug(
f"[{self.timestamp()}] Processing request:\n{pretty}")
try:
module = importlib.import_module(request["module"])
method = getattr(module, request["method"])
args = request.get("args", [])
kwargs = request.get("kwargs", {})
partial_method = functools.partial(method, *args, **kwargs)
lib.ProcessContext.execute_in_main_thread(partial_method)
except Exception:
self.log.error(traceback.format_exc())
def receive(self):
"""Receives data from `self.connection`.
When the data is a json serializable string, a reply is sent then
processing of the request.
"""
current_time = time.time()
while True:
# Receive the data in small chunks and retransmit it
request = None
header = self.connection.recv(6)
if len(header) == 0:
# null data received, socket is closing.
self.log.info(f"[{self.timestamp()}] Connection closing.")
break
if header[0:2] != b"AH":
self.log.error("INVALID HEADER")
length = struct.unpack(">I", header[2:])[0]
data = self.connection.recv(length)
while (len(data) < length):
# we didn't received everything in first try, lets wait for
# all data.
time.sleep(0.1)
if self.connection is None:
self.log.error(f"[{self.timestamp()}] "
"Connection is broken")
break
if time.time() > current_time + 30:
self.log.error(f"[{self.timestamp()}] Connection timeout.")
break
data += self.connection.recv(length - len(data))
self.received += data.decode("utf-8")
pretty = self._pretty(self.received)
self.log.debug(
f"[{self.timestamp()}] Received:\n{pretty}")
try:
request = json.loads(self.received)
except json.decoder.JSONDecodeError as e:
self.log.error(f"[{self.timestamp()}] "
f"Invalid message received.\n{e}",
exc_info=True)
self.received = ""
if request is None:
continue
if "message_id" in request.keys():
message_id = request["message_id"]
self.message_id = message_id + 1
self.log.debug(f"--- storing request as {message_id}")
self.queue[message_id] = request
if "reply" not in request.keys():
request["reply"] = True
self.send(request)
self.process_request(request)
if "message_id" in request.keys():
try:
self.log.debug(f"[{self.timestamp()}] "
f"Removing from the queue {message_id}")
del self.queue[message_id]
except IndexError:
self.log.debug(f"[{self.timestamp()}] "
f"{message_id} is no longer in queue")
else:
self.log.debug(f"[{self.timestamp()}] "
"received data was just a reply.")
def run(self):
"""Entry method for server.
Waits for a connection on `self.port` before going into listen mode.
"""
# Wait for a connection
timestamp = datetime.now().strftime("%H:%M:%S.%f")
self.log.debug(f"[{timestamp}] Waiting for a connection.")
self.connection, client_address = self.socket.accept()
timestamp = datetime.now().strftime("%H:%M:%S.%f")
self.log.debug(f"[{timestamp}] Connection from: {client_address}")
self.receive()
def stop(self):
"""Shutdown socket server gracefully."""
timestamp = datetime.now().strftime("%H:%M:%S.%f")
self.log.debug(f"[{timestamp}] Shutting down server.")
if self.connection is None:
self.log.debug("Connect to shutdown.")
socket.socket(
socket.AF_INET, socket.SOCK_STREAM
).connect(("localhost", self.port))
self.connection.close()
self.connection = None
self.socket.close()
def _send(self, message):
"""Send a message to Harmony.
Args:
message (str): Data to send to Harmony.
"""
# Wait for a connection.
while not self.connection:
pass
timestamp = datetime.now().strftime("%H:%M:%S.%f")
encoded = message.encode("utf-8")
coded_message = b"AH" + struct.pack('>I', len(encoded)) + encoded
pretty = self._pretty(coded_message)
self.log.debug(
f"[{timestamp}] Sending [{self.message_id}]:\n{pretty}")
self.log.debug(f"--- Message length: {len(encoded)}")
self.connection.sendall(coded_message)
self.message_id += 1
def send(self, request):
"""Send a request in dictionary to Harmony.
Waits for a reply from Harmony.
Args:
request (dict): Data to send to Harmony.
"""
request["message_id"] = self.message_id
self._send(json.dumps(request))
if request.get("reply"):
timestamp = datetime.now().strftime("%H:%M:%S.%f")
self.log.debug(
f"[{timestamp}] sent reply, not waiting for anything.")
return None
result = None
current_time = time.time()
try_index = 1
while True:
time.sleep(0.1)
if time.time() > current_time + 30:
timestamp = datetime.now().strftime("%H:%M:%S.%f")
self.log.error((f"[{timestamp}][{self.message_id}] "
"No reply from Harmony in 30s. "
f"Retrying {try_index}"))
try_index += 1
current_time = time.time()
if try_index > 30:
break
try:
result = self.queue[request["message_id"]]
timestamp = datetime.now().strftime("%H:%M:%S.%f")
self.log.debug((f"[{timestamp}] Got request "
f"id {self.message_id}, "
"removing from queue"))
del self.queue[request["message_id"]]
break
except KeyError:
# response not in received queue yey
pass
try:
result = json.loads(self.received)
break
except json.decoder.JSONDecodeError:
pass
self.received = ""
return result
def _pretty(self, message) -> str:
# result = pformat(message, indent=2)
# return result.replace("\\n", "\n")
return "{}{}".format(4 * " ", message)
def timestamp(self):
"""Return current timestamp as a string.
Returns:
str: current timestamp.
"""
return datetime.now().strftime("%H:%M:%S.%f")

Binary file not shown.

View file

@ -0,0 +1,78 @@
"""Host API required Work Files tool"""
import os
import shutil
from .lib import (
ProcessContext,
get_local_harmony_path,
zip_and_move,
launch_zip_file
)
from avalon import api
# used to lock saving until previous save is done.
save_disabled = False
def file_extensions():
return api.HOST_WORKFILE_EXTENSIONS["harmony"]
def has_unsaved_changes():
if ProcessContext.server:
return ProcessContext.server.send(
{"function": "scene.isDirty"})["result"]
return False
def save_file(filepath):
global save_disabled
if save_disabled:
return ProcessContext.server.send(
{
"function": "show_message",
"args": "Saving in progress, please wait until it finishes."
})["result"]
save_disabled = True
temp_path = get_local_harmony_path(filepath)
if ProcessContext.server:
if os.path.exists(temp_path):
try:
shutil.rmtree(temp_path)
except Exception as e:
raise Exception(f"cannot delete {temp_path}") from e
ProcessContext.server.send(
{"function": "scene.saveAs", "args": [temp_path]}
)["result"]
zip_and_move(temp_path, filepath)
ProcessContext.workfile_path = filepath
scene_path = os.path.join(
temp_path, os.path.basename(temp_path) + ".xstage"
)
ProcessContext.server.send(
{"function": "AvalonHarmony.addPathToWatcher", "args": scene_path}
)
else:
os.environ["HARMONY_NEW_WORKFILE_PATH"] = filepath.replace("\\", "/")
save_disabled = False
def open_file(filepath):
launch_zip_file(filepath)
def current_file():
"""Returning None to make Workfiles app look at first file extension."""
return None
def work_root(session):
return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/")

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""Create Composite node for render on farm."""
from avalon import harmony
import openpype.hosts.harmony.api as harmony
from openpype.hosts.harmony.api import plugin

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""Create render node."""
from avalon import harmony
import openpype.hosts.harmony.api as harmony
from openpype.hosts.harmony.api import plugin

View file

@ -1,4 +1,5 @@
from avalon import api, harmony
from avalon import api
import openpype.hosts.harmony.api as harmony
sig = harmony.signature()
func = """

View file

@ -1,7 +1,8 @@
import os
import json
from avalon import api, harmony
from avalon import api
import openpype.hosts.harmony.api as harmony
import openpype.lib

View file

@ -6,7 +6,8 @@ from pathlib import Path
import clique
from avalon import api, harmony
from avalon import api
import openpype.hosts.harmony.api as harmony
import openpype.lib

View file

@ -1,7 +1,8 @@
import os
import shutil
from avalon import api, harmony
from avalon import api
import openpype.hosts.harmony.api as harmony
class ImportPaletteLoader(api.Loader):
@ -41,7 +42,9 @@ class ImportPaletteLoader(api.Loader):
harmony.save_scene()
msg = "Updated {}.".format(subset_name)
msg += " You need to reload the scene to see the changes."
msg += " You need to reload the scene to see the changes.\n"
msg += "Please save workfile when ready and use Workfiles "
msg += "to reopen it."
harmony.send(
{

View file

@ -6,7 +6,8 @@ import os
import shutil
import uuid
from avalon import api, harmony
from avalon import api
import openpype.hosts.harmony.api as harmony
import openpype.lib

View file

@ -3,7 +3,8 @@ import zipfile
import os
import shutil
from avalon import api, harmony
from avalon import api
import openpype.hosts.harmony.api as harmony
class ImportTemplateLoader(api.Loader):

View file

@ -3,7 +3,7 @@
import os
import pyblish.api
from avalon import harmony
import openpype.hosts.harmony.api as harmony
class CollectCurrentFile(pyblish.api.ContextPlugin):

View file

@ -3,9 +3,10 @@
from pathlib import Path
import attr
from avalon import harmony, api
from avalon import api
import openpype.lib.abstract_collect_render
import openpype.hosts.harmony.api as harmony
from openpype.lib.abstract_collect_render import RenderInstance
import openpype.lib

View file

@ -3,7 +3,7 @@
import json
import pyblish.api
from avalon import harmony
import openpype.hosts.harmony.api as harmony
class CollectInstances(pyblish.api.ContextPlugin):

View file

@ -5,7 +5,7 @@ import json
import re
import pyblish.api
from avalon import harmony
import openpype.hosts.harmony.api as harmony
class CollectPalettes(pyblish.api.ContextPlugin):

View file

@ -3,7 +3,7 @@
import os
import pyblish.api
from avalon import harmony
import openpype.hosts.harmony.api as harmony
class CollectScene(pyblish.api.ContextPlugin):

View file

@ -5,7 +5,7 @@ import csv
from PIL import Image, ImageDraw, ImageFont
from avalon import harmony
import openpype.hosts.harmony.api as harmony
import openpype.api

View file

@ -3,7 +3,7 @@ import tempfile
import subprocess
import pyblish.api
from avalon import harmony
import openpype.hosts.harmony.api as harmony
import openpype.lib
import clique

View file

@ -1,5 +1,5 @@
import pyblish.api
from avalon import harmony
import openpype.hosts.harmony.api as harmony
class ExtractSaveScene(pyblish.api.ContextPlugin):

View file

@ -4,7 +4,7 @@ import os
import shutil
import openpype.api
from avalon import harmony
import openpype.hosts.harmony.api as harmony
import openpype.hosts.harmony

View file

@ -3,7 +3,7 @@ import os
import pyblish.api
from openpype.action import get_errored_plugins_from_data
from openpype.lib import version_up
from avalon import harmony
import openpype.hosts.harmony.api as harmony
class IncrementWorkfile(pyblish.api.InstancePlugin):

View file

@ -2,7 +2,7 @@ import os
import pyblish.api
from avalon import harmony
import openpype.hosts.harmony.api as harmony
class ValidateAudio(pyblish.api.InstancePlugin):

View file

@ -2,7 +2,7 @@ import os
import pyblish.api
import openpype.api
from avalon import harmony
import openpype.hosts.harmony.api as harmony
class ValidateInstanceRepair(pyblish.api.Action):

View file

@ -6,7 +6,7 @@ import re
import pyblish.api
from avalon import harmony
import openpype.hosts.harmony.api as harmony
import openpype.hosts.harmony

View file

@ -0,0 +1,7 @@
{
"env": {
"browser": true
},
"extends": "eslint:recommended",
"ignorePatterns": ["**/*.js"]
}

View file

@ -0,0 +1,2 @@
node_modules/*

View file

@ -23,7 +23,7 @@ This library's aim is to create a more direct way to interact with Toonboom thro
column.setEntry (myColumnName, 0, 1, "1");
```
to simply writing :
to simply writing :
```javascript
// with openHarmony
@ -36,13 +36,17 @@ Less time spent coding, more time spent having ideas!
-----
## Do I need any knowledge of toonboom scripting to use openHarmony?
OpenHarmony aims to be self contained and to reimplement all the basic functions of the Harmony API. So, while it might help to have prior experience to understand what goes on under the hood, knowledge of the official API is not required.
OpenHarmony aims to be self contained and to reimplement all the basic functions of the Harmony API. So, while it might help to have prior experience to understand what goes on under the hood, knowledge of the official API is not required.
However, should you reach the limits of what openHarmony can offer at this time, you can always access the official API at any moment. Maybe you can submit a request and the missing parts will be added eventually, or you can even delve into the code and add the necessary functions yourself if you feel like it!
You can access a list of all the functions, how to use them, as well as examples, from the online documentation:
https://cfourney.github.io/OpenHarmony/$.html
[https://cfourney.github.io/OpenHarmony/$.html](https://cfourney.github.io/OpenHarmony/$.html)
To help you get started, here is a full example using the library to make and animate a small car, covering most of the basic features.
[https://github.com/cfourney/OpenHarmony/blob/master/examples/openHarmonyExample.js](https://github.com/cfourney/OpenHarmony/blob/master/examples/openHarmonyExample.js)
-----
## The OpenHarmony Document Object Model or DOM
@ -55,7 +59,7 @@ The openHarmony library doesn't merely provide *access* to the elements of a Too
<img src="https://raw.githubusercontent.com/cfourney/OpenHarmony/master/oH_DOM.jpg" alt="The Document ObjectModel" width="1600">
The *Document Object Model* is a way to organise the elements of the Toonboom scene by highlighting the way they interact with each other. The Scene object has a root group, which contains Nodes, which have Attributes which can be linked to Columns which contain Frames, etc. This way it's always easy to find and access the content you are looking for. The attribute system has also been streamlined and you can now set values of node properties with a simple attribution synthax.
The *Document Object Model* is a way to organise the elements of the Toonboom scene by highlighting the way they interact with each other. The Scene object has a root group, which contains Nodes, which have Attributes which can be linked to Columns which contain Frames, etc. This way it's always easy to find and access the content you are looking for. The attribute system has also been streamlined and you can now set values of node properties with a simple attribution synthax.
We implemented a global access to all elements and functions through the standard **dot notation** for the hierarchy, for ease of use, and clarity of code.
@ -68,7 +72,7 @@ On the other hand, the "o" naming scheme allows us to retain full access to the
This library is made available under the [Mozilla Public license 2.0](https://www.mozilla.org/en-US/MPL/2.0/).
OpenHarmony can be downloaded from [this repository](https://github.com/cfourney/OpenHarmony/releases/) directly. In order to make use of its functions, it needs to be unzipped next to the scripts you will be writing.
OpenHarmony can be downloaded from [this repository](https://github.com/cfourney/OpenHarmony/releases/) directly. In order to make use of its functions, it needs to be unzipped next to the scripts you will be writing.
All you have to do is call :
```javascript
@ -78,7 +82,7 @@ at the beggining of your script.
You can ask your users to download their copy of the library and store it alongside, or bundle it as you wish as long as you include the license file provided on this repository.
The entire library is documented at the address :
The entire library is documented at the address :
https://cfourney.github.io/OpenHarmony/$.html
@ -89,16 +93,58 @@ As time goes by, more functions will be added and the documentation will also ge
-----
## Installation
To install:
#### simple install:
- download the zip from [the releases page](https://github.com/cfourney/OpenHarmony/releases/),
- unzip the contents to [your scripts folder](https://docs.toonboom.com/help/harmony-17/advanced/scripting/import-script.html).
#### advanced install (for developers):
- clone the repository to the location of your choice
-- or --
- download the zip from [the releases page](https://github.com/cfourney/OpenHarmony/releases/)
- unzip the contents where you want to store the library,
-- then --
- run `install.bat`.
This last step will tell Harmony where to look to load the library, by setting the environment variable `LIB_OPENHARMONY_PATH` to the current folder.
This last step will tell Harmony where to look to load the library, by setting the environment variable `LIB_OPENHARMONY_PATH` to the current folder.
It will then create a `openHarmony.js` file into the user scripts folder which calls the files from the folder from the `LIB_OPENHARMONY_PATH` variable, so that scripts can make direct use of it without having to worry about where openHarmony is stored.
If you don't need a remote location for the library, you can also unzip the entire download into your user script folder.
##### Troubleshooting:
- to test if the library is correctly installed, open the `Script Editor` window and type:
```javascript
include ("openHarmony.js");
$.alert("hello world");
```
Run the script, and if there is an error (for ex `MAX_REENTRENCY `), check that the file `openHarmony.js` exists in the script folder, and contains only the line:
```javascript
include(System.getenv('LIB_OPENHARMONY_PATH')+'openHarmony.js');
```
Check that the environment variable `LIB_OPENHARMONY_PATH` is set correctly to the remote folder.
-----
## How to add openHarmony to vscode intellisense for autocompletion
Although not fully supported, you can get most of the autocompletion features to work by adding the following lines to a `jsconfig.json` file placed at the root of your working folder.
The paths need to be relative which means the openHarmony source code must be placed directly in your developping environnement.
For example, if your working folder contains the openHarmony source in a folder called `OpenHarmony` and your working scripts in a folder called `myScripts`, place the `jsconfig.json` file at the root of the folder and add these lines to the file:
```javascript
{
include : [
"OpenHarmony/*",
"OpenHarmony/openHarmony/*",
"myScripts/*",
"*"
]
}
```
[More information on vs code and jsconfig.json.](https://code.visualstudio.com/docs/nodejs/working-with-javascript)
-----
## Let's get technical. I can code, and want to contribute, where do I start?
@ -106,33 +152,33 @@ If you don't need a remote location for the library, you can also unzip the enti
Reading and understanding the existing code, or at least the structure of the lib, is a great start, but not a requirement. You can simply start adding your classes to the $ object that is the root of the harmony lib, and start implementing. However, try to follow these guidelines as they are the underlying principles that make the library consistent:
* There is a $ global object, which contains all the class declarations, and can be passed from one context to another to access the functions.
* Each class is an abstract representation of a core concept of Harmony, so naming and consistency (within the lib) is essential. But we are not bound by the structure or naming of Harmony if we find a better way, for example to make nomenclatures more consistent between the scripting interface and the UI.
* Each class defines a bunch of class properties with getter/setters for the values that are directly related to an entity of the scene. If you're thinking of making a getter function that doesn't require arguments, use a getter setter instead!
* Each class also defines methods which can be called on the class instances to affect its contents, or its children's contents. For example, you'd go to the scene class to add the things that live in the scene, such as elements, columns and palettes. You wouldn't go to the column class or palette class to add one, because then what are you adding it *to*?
* We use encapsulation over having to pass a function arguments every time we can. Instead of adding a node to the scene, and having to pass a group as argument, adding a node is done directly by calling a method of the parent group. This way the parent/child relationship is always clear and the arguments list kept to a minimum.
* The goal is to make the most useful set of functions we can. Instead of making a large function that does a lot, consider extracting the small useful subroutines you need in your function into the existing classes directly.
* Each method argument besides the core one (for example, for adding nodes, we have to specify the type of the new node we create) must have a default fallback to make the argument optional.
* Don't use globals ever, but maybe use a class property if you need an enum for example.
* Each method argument besides the core one (for example, for adding nodes, we have to specify the type of the new node we create) must have a default fallback to make the argument optional.
* Don't use globals ever, but maybe use a class property if you need an enum for example.
* Don't use the official API namespace, any function that exists in the official API must remain accessible otherwise things will break. Prefix your class names with "o" to avoid this and to signify this function is part of openHarmony.
* We use the official API as little as we can in the code, so that if the implementation changes, we can easily fix it in a minimal amount of places. Wrap it, then use the wrapper. (ex: oScene.name)
* Users of the lib should almost never have to use "new" to create instances of their classes. Create accessors/factories that will do that for them. For example, $.scn creates and return a oScene instance, and $.scn.nodes returns new oNodes instances, but users don't have to create them themselves, so it's like they were always there, contained within. It also lets you create different subclasses for one factory. For example, $.scn.$node("Top/myNode") will either return a oNode, oDrawingNode, oPegNode or oGroupNode object depending on the node type of the node represented by the object.
* Users of the lib should almost never have to use "new" to create instances of their classes. Create accessors/factories that will do that for them. For example, $.scn creates and return a oScene instance, and $.scn.nodes returns new oNodes instances, but users don't have to create them themselves, so it's like they were always there, contained within. It also lets you create different subclasses for one factory. For example, $.scn.$node("Top/myNode") will either return a oNode, oDrawingNode, oPegNode or oGroupNode object depending on the node type of the node represented by the object.
Exceptions are small useful value containing objects that don't belong to the Harmony hierarchy like oPoint, oBox, oColorValue, etc.
* It's a JS Library, so use camelCase naming and try to follow the google style guide for JS :
https://google.github.io/styleguide/jsguide.html
* Document your new functions using the JSDocs synthax : https://devdocs.io/jsdoc/howto-es2015-classes
* Make a branch, create a merge request when you're done, and we'll add the new stuff you added to the lib :)
@ -141,4 +187,16 @@ Reading and understanding the existing code, or at least the structure of the li
This library was created by Mathieu Chaptel and Chris Fourney.
If you're using openHarmony, and are noticing things that you would like to see in the library, please feel free to contribute to the code directly, or send us feedback through Github. This project will only be as good as people working together can make it, and we need every piece of code and feedback we can get, and would love to hear from you!
If you're using openHarmony, and are noticing things that you would like to see in the library, please feel free to contribute to the code directly, or send us feedback through Github. This project will only be as good as people working together can make it, and we need every piece of code and feedback we can get, and would love to hear from you!
-----
## Community
Join the discord community for help with the library and to contribute:
https://discord.gg/kgT38MG
-----
## Acknowledgements
* [Yu Ueda](https://github.com/yueda1984) for his help to understand Harmony coordinate systems
* [Dash](https://github.com/35743) for his help to debug, test and develop the Pie Menus widgets
* [All the contributors](https://github.com/cfourney/OpenHarmony/graphs/contributors) for their precious help.

View file

Before

Width:  |  Height:  |  Size: 303 KiB

After

Width:  |  Height:  |  Size: 303 KiB

Before After
Before After

View file

@ -100,8 +100,6 @@ $ = {
},
file : __file__,
directory : false,
batchMode : false,
pi : 3.14159265359
};
@ -119,6 +117,25 @@ Object.defineProperty( $, "directory", {
});
/**
* Wether Harmony is run with the interface or simply from command line
*/
Object.defineProperty( $, "batchMode", {
get: function(){
// use a cache to avoid pulling the widgets every time
if (!this.hasOwnProperty("_batchMode")){
this._batchMode = true;
// batchmode is false if there are any widgets visible in the application
var _widgets = QApplication.topLevelWidgets();
for (var i in _widgets){
if (_widgets[i].visible) this._batchMode = false;
}
}
return this._batchMode
}
})
/**
* Function to load openHarmony files from the %installdir%/openHarmony/ folder.
* @name $#loadOpenHarmonyFiles
@ -148,6 +165,7 @@ $.debug = function( obj, level ){
if( level > this.debug_level ) return;
try{
if (typeof obj !== 'object') throw new Error();
this.log(JSON.stringify(obj));
}catch(err){
this.log(obj);
@ -240,6 +258,19 @@ $.alertBox = function(){ return $.dialog.alertBox.apply( $.dialog, arguments ) }
/**
* Prompts with an toast alert. This is a small message that can't be clicked and only stays on the screen for the duration specified.
* @function
* @name $#toast
* @param {string} labelText The label/internal text of the dialog.
* @param {$.oPoint} [position] The position on the screen where the toast will appear (by default, slightly under the middle of the screen).
* @param {float} [duration=2000] The duration of the display (in milliseconds).
* @param {$.oColorValue} [color="#000000"] The color of the background (a 50% alpha value will be applied).
*/
$.toast = function(){ return $.dialog.toast.apply( $.dialog, arguments ) };
/**
* Prompts for a user input.
* @function
@ -254,12 +285,12 @@ $.prompt = function(){ return $.dialog.prompt.apply( $.dialog, arguments ) };
/**
* Prompts with a file selector window
* @function
* @name $#browseForFile
* @param {string} [text="Select a file:"] The title of the confirmation dialog.
* @param {string} [filter="*"] The filter for the file type and/or file name that can be selected. Accepts wildcard character "*".
* @param {string} [getExisting=true] Whether to select an existing file or a save location
* @param {string} [acceptMultiple=false] Whether or not selecting more than one file is ok. Is ignored if getExisting is false.
* @param {string} [startDirectory] The directory showed at the opening of the dialog.
* @name $#browseForFile
* @param {string} [text="Select a file:"] The title of the file select dialog.
* @param {string} [filter="*"] The filter for the file type and/or file name that can be selected. Accepts wildcard character "*".
* @param {string} [getExisting=true] Whether to select an existing file or a save location
* @param {string} [acceptMultiple=false] Whether or not selecting more than one file is ok. Is ignored if getExisting is false.
* @param {string} [startDirectory] The directory showed at the opening of the dialog.
*
* @return {string[]} The list of selected Files, 'undefined' if the dialog is cancelled
*/
@ -269,11 +300,11 @@ $.browseForFile = function(){ return $.dialog.browseForFile.apply( $.dialog, arg
/**
* Prompts with a folder selector window.
* @function
* @name $#browseForFolder
* @param {string} [text] The title of the confirmation dialog.
* @param {string} [startDirectory] The directory showed at the opening of the dialog.
* @name $#browseForFolder
* @param {string} [text] The title of the confirmation dialog.
* @param {string} [startDirectory] The directory showed at the opening of the dialog.
*
* @return {string[]} The path of the selected folder, 'undefined' if the dialog is cancelled
* @return {string} The path of the selected folder, 'undefined' if the dialog is cancelled
*/
$.browseForFolder = function(){ return $.dialog.browseForFolder.apply( $.dialog, arguments ) };
@ -299,16 +330,19 @@ $.cache_oNode = {};
/**
* Starts the tracking of the undo accumulation, all subsequent actions are done in a single undo operation.<br>Close the undo accum with $.endUndo().
* @param {string} undoName The name of the operation that is being done in the undo accum.
* If this function is called multiple time, only the first time will count.
* (this prevents small functions wrapped in their own undo block to interfere with global script undo)
* @param {string} undoName The name of the operation that is being done in the undo accum.
* @name $#beginUndo
* @function
* @see $.endUndo
*/
$.beginUndo = function( undoName ){
//Using epoch as the temp name.
if ($.batchMode) return
if (typeof undoName === 'undefined') var undoName = ''+((new Date()).getTime());
scene.beginUndoRedoAccum( undoName );
if (!$.hasOwnProperty("undoStackSize")) $.undoStackSize = 0;
if ($.undoStackSize == 0) scene.beginUndoRedoAccum( undoName );
$.undoStackSize++;
}
/**
@ -322,19 +356,25 @@ $.cancelUndo = function( ){
/**
* Stops the tracking of the undo accumulation, everything between this and the start of the accumulation behaves as a single undo operation.
* If beginUndo function is called multiple time, each call must be matched with this function.
* (this prevents small functions wrapped in their own undo block to interfere with global script undo)
* @name $#endUndo
* @function
* @see $.beginUndo
*/
$.endUndo = function( ){
scene.endUndoRedoAccum( );
if ($.batchMode) return
if (!$.hasOwnProperty("undoStackSize")) $.undoStackSize = 1;
$.undoStackSize--;
if ($.undoStackSize == 0) scene.endUndoRedoAccum();
}
/**
* Undoes the last n operations. If n is not specified, it will be 1
* @name $#undo
* @function
* @param {int} n The amount of operations to undo.
* @param {int} dist The amount of operations to undo.
*/
$.undo = function( dist ){
if (typeof dist === 'undefined'){ var dist = 1; }
@ -345,7 +385,7 @@ $.undo = function( dist ){
* Redoes the last n operations. If n is not specified, it will be 1
* @name $#redo
* @function
* @param {int} n The amount of operations to undo.
* @param {int} dist The amount of operations to undo.
*/
$.redo = function( dist ){
if (typeof dist === 'undefined'){ var dist = 1; }
@ -363,12 +403,72 @@ $.getPreferences = function( ){
}
//---- Attach Helpers ------
$.network = new $.oNetwork( );
$.utils = new $.oUtils( );
$.dialog = new $.oDialog( );
$.network = new $.oNetwork();
$.utils = $.oUtils;
$.dialog = new $.oDialog();
$.global = this;
//---- Self caching -----
/**
* change this value to allow self caching across openHarmony when initialising objects.
* @name $#useCache
* @type {bool}
*/
$.useCache = false;
/**
* function to call in constructors of classes so that instances of this class
* are cached and unique based on constructor arguments.
* @returns a cached class instance or null if no cached instance exists.
*/
$.getInstanceFromCache = function(){
if (!this.__proto__.hasOwnProperty("__cache__")) {
this.__proto__.__cache__ = {};
}
var _cache = this.__proto__.__cache__;
if (!this.$.useCache) return;
var key = [];
for (var i=0; i<arguments.length; i++){
try{
key.push(arguments[i]+"")
}catch(err){} // ignore arguments that can't be converted to string
}
if (_cache.hasOwnProperty(key)) {
this.$.log("instance returned from cache for "+key, this.$.DEBUG_LEVEL.ERROR)
return _cache[key];
}
this.$.log("creating new instance for "+key, this.$.DEBUG_LEVEL.ERROR)
_cache[key] = this;
this.constructor.invalidateCache = function(){
delete this.prototype.__cache__;
}
return
}
/**
* invalidate all cache for classes that are self caching.
* Will be run at each include('openHarmony.js') statement.
*/
$.clearOpenHarmonyCache = function(){
// clear cache at openHarmony loading.
for (var classItem in this.$){
var ohClass = this.$[classItem]
if (typeof ohClass === "function" && ohClass.prototype.hasOwnProperty('__cache__')){
ohClass.invalidateCache();
}
}
}
$.clearOpenHarmonyCache();
//---- Instantiate Class $ DOM Access ------
function addDOMAccess( target, item ){
Object.defineProperty( target, '$', {
@ -384,10 +484,14 @@ for( var classItem in $ ){
try{
addDOMAccess( $[classItem].prototype, $ );
}catch(err){
$.debug( "Error extending DOM access to : " + classItem, $.DEBUG_LEVEL.ERROR );
$.debug( "Error extending DOM access to : " + classItem + ": "+err, $.DEBUG_LEVEL.ERROR );
}
//Also extend it to the global object.
this[classItem] = $[classItem];
}
}
}
// Add global access to $ object
this.__proto__.$ = $

View file

@ -155,21 +155,21 @@ Object.defineProperty($.oApp.prototype, 'globalMousePosition', {
* @example
* // Access the list of currently existing tools by using the $.app object
* var tools = $.app.tools;
*
*
* // output the list of tools names and ids
* for (var i in tools){
* log(i+" "+tools[i].name)
* }
*
*
* // To get a tool by name, use the $.app.getToolByName() function
* var brushTool = $.app.getToolByName("Brush");
* log (brushTool.name+" "+brushTool.id) // Output: Brush 9
*
*
* // it's also possible to activate a tool in several ways:
* $.app.currentTool = 9; // using the tool "id"
* $.app.currentTool = brushTool // by passing a oTool object
* $.app.currentTool = "Brush" // using the tool name
*
*
* brushTool.activate() // by using the activate function of the oTool class
*/
Object.defineProperty($.oApp.prototype, 'tools', {
@ -208,14 +208,18 @@ Object.defineProperty($.oApp.prototype, 'currentTool', {
return
}
if (typeof tool == "string"){
this.getToolByName(tool).activate();
return
try{
this.getToolByName(tool).activate();
return
}catch(err){
this.$.debug("'"+ tool + "' is not a valid tool name. Valid: "+this.tools.map(function(x){return x.name}).join(", "))
}
}
if (typeof tool == "number"){
this.tools[tool].activate();
return
}
}
}
});
@ -243,24 +247,24 @@ $.oApp.prototype.getWidgetByName = function(name, parentName){
* @example
* // To access the preferences of Harmony, grab the preference object in the $.oApp class:
* var prefs = $.app.preferences;
*
*
* // It's then possible to access all available preferences of the software:
* for (var i in prefs){
* log (i+" "+prefs[i]);
* }
*
*
* // accessing the preference value can be done directly by using the dot notation:
* prefs.USE_OVERLAY_UNDERLAY_ART = true;
* log (prefs.USE_OVERLAY_UNDERLAY_ART);
*
*
* //the details objects of the preferences object allows access to more information about each preference
* var details = prefs.details
* log(details.USE_OVERLAY_UNDERLAY_ART.category+" "+details.USE_OVERLAY_UNDERLAY_ART.id+" "+details.USE_OVERLAY_UNDERLAY_ART.type);
*
*
* for (var i in details){
* log(i+" "+JSON.stringify(details[i])) // each object inside detail is a complete oPreference instance
* }
*
*
* // the preference object also holds a categories array with the list of all categories
* log (prefs.categories)
*/
@ -275,16 +279,22 @@ Object.defineProperty($.oApp.prototype, 'preferences', {
enumerable:false,
value:_categories
})
Object.defineProperty(_prefsObject, "details", {
enumerable:false,
value:_details
})
var prefFile = (new oFile(specialFolders.resource+"/prefs.xml")).parseAsXml().children[0].children;
var userPrefFile = {objectName: "category", id: "user", children:(new oFile(specialFolders.userConfig + "/Harmony Premium-pref.xml")).parseAsXml().children[0].children};
prefFile.push(userPrefFile);
var userPrefFile = new oFile(specialFolders.userConfig + "/Harmony Premium-pref.xml")
// Harmony Pref file is called differently on the database userConfig
if (!userPrefFile.exists) userPrefFile = new oFile(specialFolders.userConfig + "/Harmony-pref.xml")
if (userPrefFile.exists){
var userPref = {objectName: "category", id: "user", children:userPrefFile.parseAsXml().children[0].children};
prefFile.push(userPref);
}
for (var i in prefFile){
if (prefFile[i].objectName != "category" || prefFile[i].id == "Storyboard") continue;
@ -336,7 +346,7 @@ Object.defineProperty($.oApp.prototype, 'preferences', {
* @example
* // Access the stencils list through the $.app object.
* var stencils = $.app.stencils
*
*
* // list all the properties of stencils
* for (var i in stencils){
* log(" ---- "+stencils[i].type+" "+stencils[i].name+" ---- ")
@ -354,7 +364,7 @@ Object.defineProperty($.oApp.prototype, 'stencils', {
var stencils = [];
var stencilXml;
while(stencilXml = penRegex.exec(stencilsFile)){
var stencilObject = new this.$.oStencil(stencilXml[1]);
var stencilObject = this.$.oStencil.getFromXml(stencilXml[1]);
stencils.push(stencilObject);
}
this._stencilsObject = stencils;
@ -365,6 +375,24 @@ Object.defineProperty($.oApp.prototype, 'stencils', {
/**
* The currently selected stencil. Always returns the pencil tool current stencil.
* @name $.oApp#currentStencil
* @type {$.oStencil}
*/
Object.defineProperty($.oApp.prototype, 'currentStencil', {
get: function(){
return this.stencils[PaletteManager.getCurrentPenstyleIndex()];
},
set: function(stencil){
if (stencil instanceof this.$.oStencil) var stencil = stencil.name
this.$.debug("Setting current pen: "+ stencil)
PenstyleManager.setCurrentPenstyleByName(stencil);
}
})
// $.oApp Class Methods
/**
@ -374,7 +402,7 @@ Object.defineProperty($.oApp.prototype, 'stencils', {
$.oApp.prototype.getToolByName = function(toolName){
var _tools = this.tools;
for (var i in _tools){
if (_tools[i].name == toolName) return _tools[i];
if (_tools[i].name.toLowerCase() == toolName.toLowerCase()) return _tools[i];
}
return null;
}

View file

@ -53,7 +53,7 @@
* The constructor for the $.oAttribute class.
* @classdesc
* The $.oAttribute class holds the smart version of the parameter you can find in layer property.<br>
* It is used internally to get and set values and link a oColumn to a parameter in order to animate it.<br>
* It is used internally to get and set values and link a oColumn to a parameter in order to animate it. (Users should never have to instantiate this class) <br>
* For a list of attributes existing in each node type and their type, as well as examples of the values they can hold, refer to :<br>
* {@link NodeType}.
* @constructor
@ -61,12 +61,12 @@
* @param {attr} attributeObject The internal harmony Attribute Object.
* @param {$.oAttribute} parentAttribute The parent attribute of the subattribute.
*
* @property {$.oNode} node The name of the drawing.
* @property {attr} attributeObject The element object associated to the element.
* @property {string} keyword The name of the drawing.
* @property {string} shortKeyword The element object associated to the element.
* @property {$.oAttribute} parentAttribute The element object associated to the element.
* @property {$.oAttribute[]} subAttributes The subattributes, if any exist, of this attribute.
* @property {$.oNode} node The oNode this attribute belongs to.
* @property {attr} attributeObject The internal harmony Attribute Object.
* @property {string} keyword The keyword describing this attribute. (always in lower case)
* @property {string} shortKeyword The full keyword describing this attribute, including parent attributes separated with a "." (always in lower case)
* @property {$.oAttribute} parentAttribute The parent oAttribute object
* @property {$.oAttribute[]} subAttributes The subattributes of this attribute.
* @example
* // oAttribute objects can be grabbed from the node .attributes object with dot notation, by calling the attribute keyword in lowercase.
*
@ -77,6 +77,9 @@
*
* Xattribute.setValue(5, 5); // sets the value to 5 at frame 5
*
* // attribute values can also be set directly on the node when not animated:
* myNode.position.x = 5;
*
*/
$.oAttribute = function( oNodeObject, attributeObject, parentAttribute ){
this._type = "attribute";
@ -173,6 +176,18 @@ $.oAttribute.prototype.getSubAttributes_oldVersion = function (){
return sub_attrs;
}
/**
* The display name of the attribute
* @name $.oAttribute#name
* @type {string}
*/
Object.defineProperty($.oAttribute.prototype, 'name', {
get: function(){
return this.attributeObject.name();
}
})
/**
* The full keyword of the attribute.
* @name $.oAttribute#keyword
@ -220,27 +235,37 @@ Object.defineProperty($.oAttribute.prototype, 'type', {
* The column attached to the attribute.
* @name $.oAttribute#column
* @type {$.oColumn}
* @example
// link a new column to an attribute by setting this value:
var myColumn = $.scn.addColumn("BEZIER");
myNode.attributes.position.x.column = myColumn; // values contained in "myColumn" now define the animation of our peg's x position
// to automatically create a column and link it to the attribute, use:
myNode.attributes.position.x.addColumn(); // if the column exist already, it will just be returned.
// to unlink a column, just set it to null/undefined:
myNode.attributes.position.x.column = null; // values are no longer animated.
*/
Object.defineProperty($.oAttribute.prototype, 'column', {
get : function(){
var _column = node.linkedColumn ( this.node.path, this._keyword );
if( _column && _column.length ){
return this.node.scene.$column( _column, this );
}else{
return null;
}
},
set : function(columnObject){
// unlink if provided with null value or empty string
if (columnObject == "" || columnObject == null){
node.unlinkAttr(this.node.path, this._keyword);
}else{
node.linkAttr(this.node.path, this._keyword, columnObject.uniqueName);
columnObject.attributeObject = this;
// TODO: transfer current value of attribute to a first key on the column if column is empty
}
get : function(){
var _column = node.linkedColumn ( this.node.path, this._keyword );
if( _column && _column.length ){
return this.node.scene.$column( _column, this );
}else{
return null;
}
},
set : function(columnObject){
// unlink if provided with null value or empty string
if (!columnObject){
node.unlinkAttr(this.node.path, this._keyword);
}else{
node.linkAttr(this.node.path, this._keyword, columnObject.uniqueName);
columnObject.attributeObject = this;
// TODO: transfer current value of attribute to a first key on the column if column is empty
}
}
});
@ -415,7 +440,7 @@ $.oAttribute.prototype.getLinkedColumns = function(){
if (_ownColumn != null) _columns.push(_ownColumn);
for (var i=0; i<_subAttributes.length; i++) {
_columns = _column.concat(_subAttributes[i].getLinkedColumns());
_columns = _columns.concat(_subAttributes[i].getLinkedColumns());
}
return _columns;
@ -523,8 +548,10 @@ $.oAttribute.prototype.getValue = function (frame) {
case 'ELEMENT':
// an element always has a column, so we'll fetch it from there
_value = column.getEntry(_column.uniqueName, 1, frame);
// Convert to an instance of oDrawing
_value = _column.element.getDrawingByName(_value);
// Convert to an instance of oDrawing, with a safety in case of psd import
_drawing = _column.element.getDrawingByName(_value);
if (_drawing) _value = _drawing;
break;
// TODO: How does QUATERNION_PATH work? subcolumns I imagine
@ -553,14 +580,25 @@ $.oAttribute.prototype.getValue = function (frame) {
/**
* Sets the value of the attribute at the given frame.
* @param {string} value The value to set on the attribute.
* @param {int} [frame] The frame at which to set the value, if not set, assumes 1
* @param {string} value The value to set on the attribute.
* @param {int} [frame=1] The frame at which to set the value, if not set, assumes 1
*/
$.oAttribute.prototype.setValue = function (value, frame) {
var frame_set = true;
if (typeof frame === 'undefined'){
frame = 1;
frame_set = false;
var _attr = this.attributeObject;
var _column = this.column;
var _type = this.type;
var _animate = false;
if (!frame){
// we don't animate
var frame = 1;
}else if (!_column){
// generate a new column to be able to animate
_column = this.addColumn();
}
if( _column ){
_animate = true;
}
try{
@ -569,22 +607,6 @@ $.oAttribute.prototype.setValue = function (value, frame) {
this.$.debug("setting attr "+this._keyword+" at frame "+frame, this.$.DEBUG_LEVEL.LOG)
};
var _attr = this.attributeObject;
var _column = this.column;
var _type = this.type;
var _animate = false;
if ( frame_set && _column == null ){
// generate a new column to be able to animate
var _doc = new this.$.oScene();
_column = _doc.addColumn(); //this might fail if the type is wrong (by default addColumn creates BEZIER columns)
this.column = _column;
}
if( _column ){
_animate = true;
}
switch(_type){
// TODO: sanitize input
case "COLOR" :
@ -602,7 +624,6 @@ $.oAttribute.prototype.setValue = function (value, frame) {
// check if frame is tied to a column or an attribute
var _frame = _column?(new this.$.oFrame(frame, this.column)):(new this.$.oFrame(frame, _attr));
if (_column){
//this.$.log(_column.name+" "+_frame.frameNumber+" "+_frame.isKeyframe)
if (!_frame.isKeyframe) _frame.isKeyframe = true;
var _point = new this.$.oPathPoint (this.column, _frame);
_point.set(value);
@ -624,36 +645,94 @@ $.oAttribute.prototype.setValue = function (value, frame) {
case "ELEMENT" :
_column = this.column;
value = (value instanceof this.$.oDrawing) ? value.name : value;
column.setEntry(_column.uniqueName, 1, frame, value+"");
break;
case "QUATERNIONPATH" :
break;
//case "STRING" :
// node.setTextAttr( this.node.path, this._keyword, frame, value);
// set quaternion paths as textattr until a better way is found
default :
try{
_animate ? _attr.setValueAt( value, frame ) : _attr.setValue( value );
}catch(err){
this.$.debug("setting text attr "+this._keyword+" value "+value+" as textAttr ", this.$.DEBUG_LEVEL.ERROR)
this.$.debug("error setting attr "+this._keyword+" value "+value+": "+err, this.$.DEBUG_LEVEL.DEBUG);
this.$.debug("setting text attr "+this._keyword+" value "+value+" as textAttr ", this.$.DEBUG_LEVEL.ERROR);
node.setTextAttr( this.node.path, this._keyword, frame, value );
// throw new Error("Couldn't set attribute "+this.keyword+" to value "+value+". Incompatible type.")
}
}
}
// MCNote: I think it would be good practice if functions had verbs in their names and properties were noun. it makes it easier to remember if you need to pass parameters or even include empty brackets
// for example: doc.selection vs doc.getSelection();
/**
* Adds a column with a default name, based on the attribute type.
* If a column already exists, it returns it.
* @returns {$.oColumn} the created column
*/
$.oAttribute.prototype.addColumn = function(){
var _column = this.column;
if (_column) return _column;
if (this.hasSubAttributes){
throw new Error("Can't create columns for attribute "+this.keyword+", column must be created for its subattributes.");
}
var _type = this.type;
var _columnType = "";
var _columnName = this.node.name+": "+this.name.replace(/\s/g, "_");
switch(_type){
case 'INT':
case 'DOUBLE':
case 'DOUBLEVB':
_columnType = "BEZIER";
break;
case "QUATERNIONPATH" :
_columnName = "QUARTERNION";
break;
case "PATH_3D" :
_columnName = "3DPATH";
break;
case "ELEMENT" :
_columnType = "DRAWING";
_columnName = this.node.name;
break;
default :
throw new Error("Can't create columns for attribute "+this.keyword+", not supported by attribute type '"+_type+"'");
}
var _column = this.$.scn.addColumn(_columnType, _columnName);
this.column = _column;
if (!this.column) {
_column.remove();
throw new Error("Can't create columns for attribute "+this.keyword+", animation not supported.");
}
return this.column;
}
/**
* Gets the value of the attribute at the given frame.
* @param {int} frame The frame at which to set the value, if not set, assumes 1
*
* @deprecated use oAttribute.getValue(frame) instead (see: function names as verbs)
* @return {object} The value of the attribute in the native format of that attribute (contextual to the attribute).
*/
$.oAttribute.prototype.value = function(frame){
return this.getValue( frame );
}
/**
* Represents an oAttribute object in string form
* @private
* @returns {string}
*/
$.oAttribute.prototype.toString = function(){
return "[object $.oAttribute '"+this.keyword+(this.subAttributes.length?"' subAttributes: "+this.subAttributes.map(function(x){return x.shortKeyword}):"")+"]";
}

View file

@ -95,8 +95,8 @@ $.oBackdrop = function( groupPath, backdropObject ){
*/
Object.defineProperty($.oBackdrop.prototype, 'index', {
get : function(){
var _groupBackdrops = Backdrop.backdrops(this.group).map(function(x){return x.title.text})
return _groupBackdrops.indexOf(this.title)
var _groupBackdrops = Backdrop.backdrops(this.group).map(function(x){return x.title.text})
return _groupBackdrops.indexOf(this.title)
}
})
@ -114,12 +114,12 @@ Object.defineProperty($.oBackdrop.prototype, 'title', {
set : function(newTitle){
var _backdrops = Backdrop.backdrops(this.group);
// incrementing to prevent two backdrops to have the same title
var names = _backdrops.map(function(x){return x.title.text})
var count = 0;
var title = newTitle
while (names.indexOf(title) != -1){
count++;
title = newTitle+"_"+count;
@ -127,7 +127,7 @@ Object.defineProperty($.oBackdrop.prototype, 'title', {
newTitle = title;
var _index = this.index;
_backdrops[_index].title.text = newTitle;
this.backdropObject = _backdrops[_index];
@ -146,10 +146,10 @@ Object.defineProperty($.oBackdrop.prototype, 'body', {
var _title = this.backdropObject.description.text;
return _title;
},
set : function(newBody){
var _backdrops = Backdrop.backdrops(this.group);
var _index = this.index;
_backdrops[_index].description.text = newBody;
@ -171,11 +171,11 @@ Object.defineProperty($.oBackdrop.prototype, 'titleFont', {
color : ( new oColorValue() ).parseColorFromInt(this.backdropObject.title.color)}
return _font;
},
set : function(newFont){
var _backdrops = Backdrop.backdrops(this.group);
var _index = this.index;
_backdrops[_index].title.font = newFont.family;
_backdrops[_index].title.size = newFont.size;
_backdrops[_index].title.color = newFont.color.toInt();
@ -198,7 +198,7 @@ Object.defineProperty($.oBackdrop.prototype, 'bodyFont', {
color : ( new oColorValue() ).parseColorFromInt(this.backdropObject.description.color)}
return _font;
},
set : function(newFont){
var _backdrops = Backdrop.backdrops(this.group);
var _index = this.index;
@ -206,13 +206,47 @@ Object.defineProperty($.oBackdrop.prototype, 'bodyFont', {
_backdrops[_index].title.font = newFont.family;
_backdrops[_index].title.size = newFont.size;
_backdrops[_index].title.color = newFont.color.toInt();
this.backdropObject = _backdrops[_index];
Backdrop.setBackdrops(this.group, _backdrops);
}
})
/**
* The nodes contained within this backdrop
* @name $.oBackdrop#parent
* @type {$.oNode[]}
* @readonly
*/
Object.defineProperty($.oBackdrop.prototype, 'parent', {
get : function(){
if (!this.hasOwnProperty("_parent")){
this._parent = this.$.scn.getNodeByPath(this.group);
}
return this._parent
}
})
/**
* The nodes contained within this backdrop
* @name $.oBackdrop#nodes
* @type {$.oNode[]}
* @readonly
*/
Object.defineProperty($.oBackdrop.prototype, 'nodes', {
get : function(){
var _nodes = this.parent.nodes;
var _bounds = this.bounds;
_nodes = _nodes.filter(function(x){
return _bounds.contains(x.bounds);
})
return _nodes;
}
})
/**
* The position of the backdrop on the horizontal axis.
* @name $.oBackdrop#x
@ -223,7 +257,7 @@ Object.defineProperty($.oBackdrop.prototype, 'x', {
var _x = this.backdropObject.position.x;
return _x;
},
set : function(newX){
var _backdrops = Backdrop.backdrops(this.group);
var _index = this.index;
@ -242,20 +276,20 @@ Object.defineProperty($.oBackdrop.prototype, 'x', {
* @type {float}
*/
Object.defineProperty($.oBackdrop.prototype, 'y', {
get : function(){
var _y = this.backdropObject.position.y;
return _y;
},
set : function(newY){
var _backdrops = Backdrop.backdrops(this.group);
var _index = this.index;
get : function(){
var _y = this.backdropObject.position.y;
return _y;
},
_backdrops[_index].position.y = newY;
set : function(newY){
var _backdrops = Backdrop.backdrops(this.group);
var _index = this.index;
this.backdropObject = _backdrops[_index];
Backdrop.setBackdrops(this.group, _backdrops);
}
_backdrops[_index].position.y = newY;
this.backdropObject = _backdrops[_index];
Backdrop.setBackdrops(this.group, _backdrops);
}
})
@ -265,20 +299,20 @@ Object.defineProperty($.oBackdrop.prototype, 'y', {
* @type {float}
*/
Object.defineProperty($.oBackdrop.prototype, 'width', {
get : function(){
var _width = this.backdropObject.position.w;
return _width;
},
set : function(newWidth){
var _backdrops = Backdrop.backdrops(this.group);
var _index = this.index;
get : function(){
var _width = this.backdropObject.position.w;
return _width;
},
_backdrops[_index].position.w = newWidth;
set : function(newWidth){
var _backdrops = Backdrop.backdrops(this.group);
var _index = this.index;
this.backdropObject = _backdrops[_index];
Backdrop.setBackdrops(this.group, _backdrops);
}
_backdrops[_index].position.w = newWidth;
this.backdropObject = _backdrops[_index];
Backdrop.setBackdrops(this.group, _backdrops);
}
})
@ -289,20 +323,20 @@ Object.defineProperty($.oBackdrop.prototype, 'width', {
* @type {float}
*/
Object.defineProperty($.oBackdrop.prototype, 'height', {
get : function(){
var _height = this.backdropObject.position.h;
return _height;
},
set : function(newHeight){
var _backdrops = Backdrop.backdrops(this.group);
var _index = this.index;
get : function(){
var _height = this.backdropObject.position.h;
return _height;
},
_backdrops[_index].position.h = newHeight;
set : function(newHeight){
var _backdrops = Backdrop.backdrops(this.group);
var _index = this.index;
this.backdropObject = _backdrops[_index];
Backdrop.setBackdrops(this.group, _backdrops);
}
_backdrops[_index].position.h = newHeight;
this.backdropObject = _backdrops[_index];
Backdrop.setBackdrops(this.group, _backdrops);
}
})
@ -312,21 +346,21 @@ Object.defineProperty($.oBackdrop.prototype, 'height', {
* @type {oPoint}
*/
Object.defineProperty($.oBackdrop.prototype, 'position', {
get : function(){
var _position = new oPoint(this.x, this.y, this.index)
return _position;
},
set : function(newPos){
var _backdrops = Backdrop.backdrops(this.group);
var _index = this.index;
get : function(){
var _position = new oPoint(this.x, this.y, this.index)
return _position;
},
_backdrops[_index].position.x = newPos.x;
_backdrops[_index].position.y = newPos.y;
this.backdropObject = _backdrops[_index];
Backdrop.setBackdrops(this.group, _backdrops);
}
set : function(newPos){
var _backdrops = Backdrop.backdrops(this.group);
var _index = this.index;
_backdrops[_index].position.x = newPos.x;
_backdrops[_index].position.y = newPos.y;
this.backdropObject = _backdrops[_index];
Backdrop.setBackdrops(this.group, _backdrops);
}
})
@ -336,23 +370,23 @@ Object.defineProperty($.oBackdrop.prototype, 'position', {
* @type {oBox}
*/
Object.defineProperty($.oBackdrop.prototype, 'bounds', {
get : function(){
var _box = new oBox(this.x, this.y, this.width+this.x, this.heigth+this.y)
return _box;
},
set : function(newBounds){
var _backdrops = Backdrop.backdrops(this.group);
var _index = this.index;
get : function(){
var _box = new oBox(this.x, this.y, this.width+this.x, this.height+this.y)
return _box;
},
_backdrops[_index].position.x = newBounds.top;
_backdrops[_index].position.y = newBounds.left;
_backdrops[_index].position.w = newBounds.width;
_backdrops[_index].position.h = newBounds.height;
set : function(newBounds){
var _backdrops = Backdrop.backdrops(this.group);
var _index = this.index;
this.backdropObject = _backdrops[_index];
Backdrop.setBackdrops(this.group, _backdrops);
}
_backdrops[_index].position.x = newBounds.top;
_backdrops[_index].position.y = newBounds.left;
_backdrops[_index].position.w = newBounds.width;
_backdrops[_index].position.h = newBounds.height;
this.backdropObject = _backdrops[_index];
Backdrop.setBackdrops(this.group, _backdrops);
}
})
@ -362,21 +396,20 @@ Object.defineProperty($.oBackdrop.prototype, 'bounds', {
* @type {oColorValue}
*/
Object.defineProperty($.oBackdrop.prototype, 'color', {
get : function(){
var _color = this.backdropObject.color;
// TODO: get the rgba values from the int
return _color;
},
set : function(newOColorValue){
var _color = new oColorValue(newOColorValue);
var _index = this.index;
get : function(){
var _color = this.backdropObject.color;
// TODO: get the rgba values from the int
return _color;
},
set : function(newOColorValue){
var _color = new oColorValue(newOColorValue);
var _index = this.index;
var _backdrops = Backdrop.backdrops(this.group);
_backdrops[_index].color = _color.toInt();
var _backdrops = Backdrop.backdrops(this.group);
_backdrops[_index].color = _color.toInt();
this.backdropObject = _backdrops[_index];
Backdrop.setBackdrops(this.group, _backdrops);
}
this.backdropObject = _backdrops[_index];
Backdrop.setBackdrops(this.group, _backdrops);
}
})

View file

@ -48,7 +48,7 @@
// //
//////////////////////////////////////
//////////////////////////////////////
/**
* This class holds a color value. It can be used to set color attributes to a specific value and to convert colors between different formats such as hex strings, RGBA decompositions, as well as HSL values.
* @constructor
@ -61,31 +61,34 @@
* @property {int} a The int value of the alpha component.
* @example
* // initialise the class to start setting up attributes and making conversions by creating a new instance
*
*
* var myColor = new $.oColorValue("#336600ff");
* $.log(myColor.r+" "+mycolor.g+" "+myColor.b+" "+myColor+a) // you can then access each component of the color
*
*
* var myBackdrop = $.scn.root.addBackdrop("Backdrop")
* var myBackdrop.color = myColor // can be used to set the color of a backdrop
*
*
*/
$.oColorValue = function( colorValue ){
if (typeof colorValue === 'undefined') var colorValue = "#000000ff";
this.r = 0; this.g = 0; this.b = 0; this.a = 255;
this.r = 0;
this.g = 0;
this.b = 0;
this.a = 255;
//Special case in which RGBA values are defined directly.
switch( arguments.length ){
case 4:
this.a = ( (typeof arguments[3]) == "number" ) ? arguments[3] : 0;
this.a = ( (typeof arguments[3]) == "number" ) ? arguments[3] : 0;
case 3:
this.r = ( (typeof arguments[0]) == "number" ) ? arguments[0] : 0;
this.g = ( (typeof arguments[1]) == "number" ) ? arguments[1] : 0;
this.b = ( (typeof arguments[2]) == "number" ) ? arguments[2] : 0;
this.r = ( (typeof arguments[0]) == "number" ) ? arguments[0] : 0;
this.g = ( (typeof arguments[1]) == "number" ) ? arguments[1] : 0;
this.b = ( (typeof arguments[2]) == "number" ) ? arguments[2] : 0;
return;
default:
}
if (typeof colorValue === 'string'){
this.fromColorString(colorValue);
}else{
@ -93,7 +96,7 @@ $.oColorValue = function( colorValue ){
if (typeof colorValue.g === 'undefined') colorValue.g = 0;
if (typeof colorValue.b === 'undefined') colorValue.b = 0;
if (typeof colorValue.a === 'undefined') colorValue.a = 255;
this.r = colorValue.r;
this.g = colorValue.g;
this.b = colorValue.b;
@ -117,14 +120,14 @@ $.oColorValue.prototype.toInt = function (){
*/
$.oColorValue.prototype.toString = function (){
var _hex = "#";
var r = ("00"+this.r.toString(16)).slice(-2);
var g = ("00"+this.g.toString(16)).slice(-2);
var b = ("00"+this.b.toString(16)).slice(-2);
var a = ("00"+this.a.toString(16)).slice(-2);
_hex += r + g + b + a;
return _hex;
}
@ -135,18 +138,18 @@ $.oColorValue.prototype.toString = function (){
$.oColorValue.prototype.toHex = function (){
return this.toString();
}
/**
* Ingest a hex string in form #RRGGBBAA to define the colour.
* @param {string} hexString The colour in form #RRGGBBAA
*/
$.oColorValue.prototype.fromColorString = function (hexString){
hexString = hexString.replace("#","");
if (hexString.length == 6) hexString+"ff";
if (hexString.length == 6) hexString += "ff";
if (hexString.length != 8) throw new Error("incorrect color string format");
System.println( "HEX : " + hexString );
this.$.debug( "HEX : " + hexString, this.$.DEBUG_LEVEL.LOG);
this.r = parseInt(hexString.slice(0,2), 16);
this.g = parseInt(hexString.slice(2,4), 16);
this.b = parseInt(hexString.slice(4,6), 16);
@ -155,8 +158,8 @@ $.oColorValue.prototype.fromColorString = function (hexString){
/**
* Uses a color integer (used in backdrops) and parses the INT; applies the RGBA components of the INT to thos oColorValue
* @param { int } colorInt 24 bit-shifted integer containing RGBA values
* Uses a color integer (used in backdrops) and parses the INT; applies the RGBA components of the INT to thos oColorValue
* @param { int } colorInt 24 bit-shifted integer containing RGBA values
*/
$.oColorValue.prototype.parseColorFromInt = function(colorInt){
this.r = colorInt >> 16 & 0xFF;
@ -176,14 +179,14 @@ Object.defineProperty($.oColorValue.prototype, 'h', {
var r = this.r;
var g = this.g;
var b = this.b;
var cmin = Math.min(r,g,b);
var cmax = Math.max(r,g,b);
var delta = cmax - cmin;
var h = 0;
var s = 0;
var l = 0;
if (delta == 0){
h = 0.0;
// Red is max
@ -196,38 +199,38 @@ Object.defineProperty($.oColorValue.prototype, 'h', {
}else{
h = (r - g) / delta + 4.0;
}
h = Math.round(h * 60.0);
//WRAP IN 360.
//WRAP IN 360.
if (h < 0){
h += 360.0;
}
// // Calculate lightness
// l = (cmax + cmin) / 2.0;
// // Calculate saturation
// s = delta == 0 ? 0 : delta / (1.0 - Math.abs(2.0 * l - 1.0));
// s = Math.min( Math.abs(s)*100.0, 100.0 );
// l = (Math.abs(l)/255.0)*100.0;
return h;
},
return h;
},
set : function( new_h ){
var h = Math.min( new_h, 360.0 );
var s = Math.min( this.s, 100.0 )/100.0;
var l = Math.min( this.l, 100.0 )/100.0;
var c = (1.0 - Math.abs(2.0 * l - 1.0)) * s;
var x = c * (1 - Math.abs((h / 60.0) % 2.0 - 1.0));
var m = l - c/2.0;
var r = 0.0;
var g = 0.0;
var b = 0.0;
if (0.0 <= h && h < 60.0) {
r = c; g = x; b = 0;
} else if (60.0 <= h && h < 120.0) {
@ -258,35 +261,35 @@ Object.defineProperty($.oColorValue.prototype, 's', {
var r = this.r;
var g = this.g;
var b = this.b;
var cmin = Math.min(r,g,b);
var cmax = Math.max(r,g,b);
var delta = cmax - cmin;
var s = 0;
var l = 0;
// Calculate lightness
l = (cmax + cmin) / 2.0;
s = delta == 0 ? 0 : delta / (1.0 - Math.abs(2.0 * l - 1.0));
// Calculate saturation
s = Math.min( Math.abs(s)*100.0, 100.0 );
return s;
},
},
set : function( new_s ){
var h = Math.min( this.h, 360.0 );
var s = Math.min( new_s, 100.0 )/100.0;
var l = Math.min( this.l, 100.0 )/100.0;
var c = (1.0 - Math.abs(2.0 * l - 1.0)) * s;
var x = c * (1 - Math.abs((h / 60.0) % 2.0 - 1.0));
var m = l - c/2.0;
var r = 0.0;
var g = 0.0;
var b = 0.0;
if (0.0 <= h && h < 60.0) {
r = c; g = x; b = 0;
} else if (60.0 <= h && h < 120.0) {
@ -317,32 +320,32 @@ Object.defineProperty($.oColorValue.prototype, 'l', {
var r = this.r;
var g = this.g;
var b = this.b;
var cmin = Math.min(r,g,b);
var cmax = Math.max(r,g,b);
var delta = cmax - cmin;
var s = 0;
var l = 0;
// Calculate lightness
l = (cmax + cmin) / 2.0;
l = (Math.abs(l)/255.0)*100.0;
return l;
},
},
set : function( new_l ){
var h = Math.min( this.h, 360.0 );
var s = Math.min( this.s, 100.0 )/100.0;
var l = Math.min( new_l, 100.0 )/100.0;
var c = (1.0 - Math.abs(2.0 * l - 1.0)) * s;
var x = c * (1 - Math.abs((h / 60.0) % 2.0 - 1.0));
var m = l - c/2.0;
var r = 0.0;
var g = 0.0;
var b = 0.0;
if (0.0 <= h && h < 60.0) {
r = c; g = x; b = 0;
} else if (60.0 <= h && h < 120.0) {
@ -375,8 +378,8 @@ Object.defineProperty($.oColorValue.prototype, 'l', {
// //
//////////////////////////////////////
//////////////////////////////////////
// oPalette constructor
/**
@ -395,7 +398,7 @@ $.oColor = function( oPaletteObject, index ){
this.palette = oPaletteObject;
this._index = index;
}
// $.oColor Object Properties
/**
@ -421,7 +424,7 @@ Object.defineProperty($.oColor.prototype, 'name', {
var _color = this.colorObject;
return _color.name;
},
set : function(newName){
var _color = this.colorObject;
_color.setName(newName);
@ -439,7 +442,7 @@ Object.defineProperty($.oColor.prototype, 'id', {
var _color = this.colorObject;
return _color.id
},
set : function(newId){
// TODO: figure out a way to change id? Create a new color with specific id in the palette?
throw new Error("setting oColor.id Not yet implemented");
@ -456,7 +459,7 @@ Object.defineProperty($.oColor.prototype, 'index', {
get : function(){
return this._index;
},
set : function(newIndex){
var _color = this.palette.paletteObject.moveColor(this._index, newIndex);
this._index = newIndex;
@ -473,13 +476,13 @@ Object.defineProperty($.oColor.prototype, 'type', {
set : function(){
throw new Error("setting oColor.type Not yet implemented.");
},
get : function(){
var _color = this.colorObject;
if (_color.isTexture()) return "texture";
if (_color.isTexture) return "texture";
switch (_color.colorType) {
case PaletteObjectManager.Constants.ColorType.SOLID_COLOR:
switch (_color.colorType) {
case PaletteObjectManager.Constants.ColorType.SOLID_COLOR:
return "solid";
case PaletteObjectManager.Constants.ColorType.LINEAR_GRADIENT :
return "gradient";
@ -503,7 +506,7 @@ Object.defineProperty($.oColor.prototype, 'selected', {
var _ids = _colors.map(function(x){return x.id})
return this._index == _ids.indexOf(_currentId);
},
set : function(isSelected){
// TODO: find a way to work with index as more than one color can have the same id, also, can there be no selected color when removing selection?
if (isSelected){
@ -515,57 +518,58 @@ Object.defineProperty($.oColor.prototype, 'selected', {
/**
* Takes a string or array of strings for gradients and filename for textures. Instead of passing rgba objects, it accepts "#rrggbbaa" hex strings for convenience.<br>set gradients, provide an array of {string color, double position} objects that define a gradient scale.
* Takes a string or array of strings for gradients and filename for textures. Instead of passing rgba objects, it accepts "#rrggbbaa" hex strings for convenience.<br>set gradients, provide an object with keys from 0 to 1 for the position of each color.<br>(ex: {0: new $.oColorValue("000000ff"), 1:new $.oColorValue("ffffffff")}).
* @name $.oColor#value
* @type {object}
* @type {$.oColorValue}
*/
Object.defineProperty($.oColor.prototype, 'value', {
get : function(){
var _color = this.colorObject;
switch(this.type){
case "solid":
return new this.$.oColorValue(_color.colorData)
case "texture":
// TODO: no way to return the texture file name?
case "gradient":
case "radial gradient":
var _gradientArray = _color.colorData;
var _value = [];
for (var i = 0; i<_gradientArray.length; i++){
var _tack = {}
_tack.color = new this.$.oColorValue(_gradientArray[i]).toString()
_tack.position = _gradientArray[i].t
_value.push(_tack)
}
return _value;
default:
get : function(){
var _color = this.colorObject;
switch(this.type){
case "solid":
return new this.$.oColorValue(_color.colorData);
case "texture":
return this.palette.path.parent.path + this.palette.name+"_textures/" + this.id + ".tga";
case "gradient":
case "radial gradient":
var _gradientArray = _color.colorData;
var _value = {};
for (var i in _gradientArray){
var _data = _gradientArray[i];
_value[_gradientArray[i].t] = new this.$.oColorValue(_data.r, _data.g, _data.b, _data.a);
}
},
set : function(newValue){
var _color = this.colorObject;
switch(this.type){
case "solid":
_color.setColorData(newValue);
break;
case "texture":
// TODO: need to copy the file into the folder first?
_color.setTextureFile(newValue);
break;
case "gradient":
case "radial gradient":
var _gradientArray = newValue;
var _value = [];
for (var i = 0; i<_gradientArray.length; i++){
var _tack = new this.$.oColorValue(_gradientArray[i].color)
_tack.t = _gradientArray[i]. position
_value.push()
}
_color.setColorData(_value);
break;
default:
};
return _value;
default:
}
},
set : function(newValue){
var _color = this.colorObject;
switch(this.type){
case "solid":
_value = new $.oColorValue(newValue);
_color.setColorData(_value);
break;
case "texture":
// TODO: need to copy the file into the folder first?
_color.setTextureFile(newValue);
break;
case "gradient":
case "radial gradient":
var _value = [];
var _gradient = newValue;
for (var i in _gradient){
var _color = _gradient[i];
var _tack = {r:_color.r, g:_color.g, b:_color.b, a:_color.a, t:parseFloat(i, 10)}
_value.push(_tack);
}
_color.setColorData(_value);
break;
default:
};
}
});
@ -575,14 +579,14 @@ Object.defineProperty($.oColor.prototype, 'value', {
* Moves the palette to another Palette Object (CFNote: perhaps have it push to paletteObject, instead of being done at the color level)
* @param {$.oPalette} oPaletteObject The paletteObject to move this color into.
* @param {int} index Need clarification from mchap
*
*
* @return: {$.oColor} The new resulting $.oColor object.
*/
$.oColor.prototype.moveToPalette = function (oPaletteObject, index){
if (typeof index === 'undefined') var index = oPaletteObject.paletteObject.nColors;
var _duplicate = this.copyToPalette(oPaletteObject, index)
this.remove()
return _duplicate;
}
@ -591,18 +595,18 @@ $.oColor.prototype.moveToPalette = function (oPaletteObject, index){
* Copies the palette to another Palette Object (CFNote: perhaps have it push to paletteObject, instead of being done at the color level)
* @param {$.oPalette} oPaletteObject The paletteObject to move this color into.
* @param {int} index Need clarification from mchap
*
*
* @return: {$.oColor} The new resulting $.oColor object.
*/
$.oColor.prototype.copyToPalette = function (oPaletteObject, index){
var _color = this.colorObject;
oPaletteObject.paletteObject.cloneColor(_color);
var _colors = oPaletteObject.colors;
var _duplicate = _colors.pop();
if (typeof index !== 'undefined') _duplicate.index = index;
return _duplicate;
}
@ -619,7 +623,7 @@ $.oColor.prototype.remove = function (){
/**
* Static helper function to convert from {r:int, g:int, b:int, a:int} to a hex string in format #FFFFFFFF <br>
* Consider moving this to a helper function.
* @param { obj } rgbaObject RGB object
* @param { obj } rgbaObject RGB object
* @static
* @return: { string } Hex color string in format #FFFFFFFF.
*/
@ -637,14 +641,14 @@ $.oColor.prototype.rgbaToHex = function (rgbaObject){
/**
* Static helper function to convert from hex string in format #FFFFFFFF to {r:int, g:int, b:int, a:int} <br>
* Consider moving this to a helper function.
* @param { string } hexString RGB object
* @param { string } hexString RGB object
* @static
* @return: { obj } The hex object returned { r:int, g:int, b:int, a:int }
*/
$.oColor.prototype.hexToRgba = function (hexString){
var _rgba = {};
//Needs a better fail state.
_rgba.r = parseInt(hexString.slice(1,3), 16)
_rgba.g = parseInt(hexString.slice(3,5), 16)
_rgba.b = parseInt(hexString.slice(5,7), 16)

View file

@ -84,6 +84,9 @@
* doc.nodes[0].attributes.position.y.column = myColumn; // now position.x and position.y will share the same animation on the node.
*/
$.oColumn = function( uniqueName, oAttributeObject ){
var instance = this.$.getInstanceFromCache.call(this, uniqueName);
if (instance) return instance;
this._type = "column";
this.uniqueName = uniqueName;
@ -150,14 +153,10 @@ Object.defineProperty($.oColumn.prototype, 'selected', {
}
}
//Also look through the timeline.
System.println( "TODO: Also look through the timeline" );
return false;
},
set : function(){
throw "Setting oColumn.selected is not yet implemented."
selection.addColumnToSelection(this.uniqueName);
}
});
@ -259,6 +258,15 @@ Object.defineProperty($.oColumn.prototype, 'stepSection', {
// $.oColumn Class methods
/**
* Deletes the column from the scene. The column must be unlinked from any attribute first.
*/
$.oColumn.prototype.remove = function(){
column.removeUnlinkedFunctionColumn(this.name);
if (this.type) throw new Error("Couldn't remove column "+this.name+", unlink it from any attribute first.")
}
/**
* Extends the exposure of the drawing's keyframes given the provided arguments.
* @deprecated Use oDrawingColumn.extendExposures instead.
@ -449,10 +457,26 @@ $.oColumn.prototype.setValue = function(newValue, frame){
/**
* Retrieves the nodes index in the timeline provided.
* @param {oTimeline} [timeline] Optional: the timeline object to search the column Layer. (by default, grabs the current timeline)
*
* @return {int} The index within that timeline.
*/
$.oColumn.prototype.getTimelineLayer = function(timeline){
if (typeof timeline === 'undefined') var timeline = this.$.scene.getTimeline();
var _columnNames = timeline.allLayers.map(function(x){return x.column?x.column.uniqueName:null});
return timeline.allLayers[_columnNames.indexOf(this.uniqueName)];
}
//------------------------------------------------------
//TODO FULL IMPLEMENTATION OF THIS.
/**
* @private
*/
$.oColumn.prototype.toString = function(){
return "[object $.oColumn '"+this.name+"']"
}
//////////////////////////////////////
@ -478,15 +502,17 @@ $.oColumn.prototype.setValue = function(newValue, frame){
* @property {$.oAttribute} attributeObject The attribute object that the column is attached to.
*/
$.oDrawingColumn = function( uniqueName, oAttributeObject ) {
// $.oDrawingColumn can only represent a column of type 'DRAWING'
// $.oDrawingColumn can only represent a column of type 'DRAWING'
if (column.type(uniqueName) != 'DRAWING') throw new Error("'uniqueName' parameter must point to a 'DRAWING' type node");
//MessageBox.information("getting an instance of $.oDrawingColumn for column : "+uniqueName)
$.oColumn.call(this, uniqueName, oAttributeObject);
var instance = $.oColumn.call(this, uniqueName, oAttributeObject);
if (instance) return instance;
}
// extends $.oColumn and can use its methods
$.oDrawingColumn.prototype = Object.create($.oColumn.prototype);
$.oDrawingColumn.prototype.constructor = $.oColumn;
/**
@ -507,15 +533,14 @@ Object.defineProperty($.oDrawingColumn.prototype, 'element', {
/**
* Extends the exposure of the drawing's keyframes given the provided arguments.
* @param {$.oFrame[]} exposures The exposures to extend. If UNDEFINED, extends all keyframes.
* @param {int} amount The amount to extend.
* @param {bool} replace Setting this to false will insert frames as opposed to overwrite existing ones.(currently unsupported))
* Extends the exposure of the drawing's keyframes by the specified amount.
* @param {$.oFrame[]} [exposures] The exposures to extend. If not specified, extends all keyframes.
* @param {int} [amount] The number of frames to add to each exposure. If not specified, will extend frame up to the next one.
* @param {bool} [replace=false] Setting this to false will insert frames as opposed to overwrite existing ones.(currently unsupported))
*/
$.oDrawingColumn.prototype.extendExposures = function( exposures, amount, replace){
// if amount is undefined, extend function below will automatically fill empty frames
if (typeof exposures === 'undefined') var exposures = this.getKeyframes();
if (typeof amount === 'undefined') var amount = 1;
this.$.debug("extendingExposures "+exposures.map(function(x){return x.frameNumber})+" by "+amount, this.$.DEBUG_LEVEL.DEBUG)
@ -559,7 +584,6 @@ $.oDrawingColumn.prototype.duplicate = function(newAttribute, duplicateElement)
}
/**
* Renames the column's exposed drawings according to the frame they are first displayed at.
* @param {string} [prefix] a prefix to add to all names.
@ -570,13 +594,14 @@ $.oDrawingColumn.prototype.renameAllByFrame = function(prefix, suffix){
if (typeof suffix === 'undefined') var suffix = "";
// get exposed drawings
var _displayedDrawings = this.getKeyframes();
var _displayedDrawings = this.getExposedDrawings();
this.$.debug("Column "+this.name+" has drawings : "+_displayedDrawings.map(function(x){return x.value}), this.$.DEBUG_LEVEL.LOG);
// remove duplicates
var _seen = [];
for (var i=0; i<_displayedDrawings.length; i++){
var _drawing = _displayedDrawings[i].value
var _drawing = _displayedDrawings[i].value;
if (_seen.indexOf(_drawing.name) == -1){
_seen.push(_drawing.name);
}else{
@ -600,7 +625,8 @@ $.oDrawingColumn.prototype.renameAllByFrame = function(prefix, suffix){
* @param {$.oFrame[]} exposures The exposures to extend. If UNDEFINED, extends all keyframes.
*/
$.oDrawingColumn.prototype.removeUnexposedDrawings = function(){
var _displayedDrawings = this.getKeyframes().map(function(x){return x.value.name});
var _element = this.element;
var _displayedDrawings = this.getExposedDrawings().map(function(x){return x.value.name;});
var _element = this.element;
var _drawings = _element.drawings;
@ -608,4 +634,16 @@ $.oDrawingColumn.prototype.removeUnexposedDrawings = function(){
this.$.debug("removing drawing "+_drawings[i].name+" of column "+this.name+"? "+(_displayedDrawings.indexOf(_drawings[i].name) == -1), this.$.DEBUG_LEVEL.LOG);
if (_displayedDrawings.indexOf(_drawings[i].name) == -1) _drawings[i].remove();
}
}
}
$.oDrawingColumn.prototype.getExposedDrawings = function (){
return this.keyframes.filter(function(x){return x.value != null});
}
/**
* @private
*/
$.oDrawingColumn.prototype.toString = function(){
return "<$.oDrawingColumn '"+this.name+"'>";
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -149,26 +149,46 @@ Object.defineProperty($.oElement.prototype, 'palettes', {
/**
* Adds a drawing to the element. Provide a filename to import an external file as a drawing.
* @param {int} [atFrame] The frame at which to add the drawing on the $.oDrawingColumn. Values < 1 create no exposure.
* @param {int} [atFrame=1] The frame at which to add the drawing on the $.oDrawingColumn. Values < 1 create no exposure.
* @param {name} [name] The name of the drawing to add.
* @param {string} [filename] The filename for the drawing to add.
* @param {string} [filename] Optionally, a path for a drawing file to use for this drawing. Can pass an oFile object as well.
* @param {bool} [convertToTvg=false] If the filename isn't a tvg file, specify if you want it converted (this doesn't vectorize the drawing).
*
* @return {$.oDrawing} The added drawing
*/
$.oElement.prototype.addDrawing = function( atFrame, name, filename ){
$.oElement.prototype.addDrawing = function( atFrame, name, filename, convertToTvg ){
if (typeof atFrame === 'undefined') var atFrame = 1;
if (typeof filename === 'undefined') var filename = null;
if (typeof name === 'undefined') var name = atFrame+'';
var nameByFrame = this.$.app.preferences.XSHEET_NAME_BY_FRAME;
if (typeof name === 'undefined') var name = nameByFrame?atFrame:1;
var name = name +""; // convert name to string
// ensure a new drawing is always created by incrementing depending on preference
var _drawingNames = this.drawings.map(function(x){return x.name}); // index of existing names
var _nameFormat = /(.*?)_(\d+)$/
while (_drawingNames.indexOf(name) != -1){
if (nameByFrame || isNaN(name)){
var nameGroups = name.match(_nameFormat);
if (nameGroups){
// increment the part after the underscore
name = nameGroups[1] + "_" + (parseInt(nameGroups[2])+1);
}else{
name += "_1";
}
}else{
name = parseInt(name, 10);
if (isNaN(name)) name = 0;
name = name + 1 + ""; // increment and convert back to string
}
}
if (!(filename instanceof this.$.oFile)) filename = new this.$.oFile(filename);
var _fileExists = filename.exists;
// TODO deal with fileExists and storeInProjectFolder
Drawing.create (this.id, name, _fileExists, true);
var _drawing = new this.$.oDrawing( name, this );
if (_fileExists) _drawing.importBitmap(filename);
if (_fileExists) _drawing.importBitmap(filename, convertToTvg);
// place drawing on the column at the provided frame
if (this.column != null || this.column != undefined && atFrame >= 1){
@ -181,12 +201,16 @@ $.oElement.prototype.addDrawing = function( atFrame, name, filename ){
/**
* Gets a drawing object by the name.
* @param {string} name The name of the drawing to get.
* @param {string} name The name of the drawing to get.
*
* @return { $.oDrawing } The drawing found by the search
* @return {$.oDrawing} The drawing found by the search
*/
$.oElement.prototype.getDrawingByName = function ( name ){
return new this.$.oDrawing( name, this );
var _drawings = this.drawings;
for (var i in _drawings){
if (_drawings[i].name == name) return _drawings[i];
}
return null;
}
@ -210,14 +234,14 @@ $.oElement.prototype.linkPalette = function ( oPaletteObject , listIndex){
/**
* If the palette passed as a parameter is linked to this element, it will be unlinked, and moved to the scene palette list.
* @param {$.oPalette} oPaletteObject
* @return {bool} the success of the unlinking process.
*/
$.oElement.prototype.unlinkPalette = function ( oPaletteObject) {
log(oPaletteObject.id)
$.oElement.prototype.unlinkPalette = function (oPaletteObject) {
var _palettes = this.palettes;
var _ids = _palettes.map(function(x){return x.id});
var _paletteId = oPaletteObject.id;
var _paletteIndex = _ids.indexOf(_paletteId);
log(_paletteIndex)
if (_paletteIndex == -1) return; // palette already isn't linked
var _palette = _palettes[_paletteIndex];

View file

@ -60,8 +60,14 @@
$.oFolder = function(path){
this._type = "folder";
this._path = fileMapper.toNativePath(path).split("\\").join("/");
// if (this.path.slice(-1) == "/") this.path += this.path.slice(0, -1);
// fix lowercase drive letter
var path_components = this._path.split("/");
if (path_components[0] && about.isWindowsArch()){
// local path that starts with a drive letter
path_components[0] = path_components[0].toUpperCase()
this._path = path_components.join("/");
}
}
@ -205,7 +211,7 @@ Object.defineProperty($.oFolder.prototype, 'content', {
* Lists the file names contained inside the folder.
* @param {string} [filter] Filter wildcards for the content of the folder.
*
* @return: { string[] } The file content of folder.
* @returns {string[]} The names of the files contained in the folder that match the filter.
*/
$.oFolder.prototype.listFiles = function(filter){
if (typeof filter === 'undefined') var filter = "*";
@ -223,9 +229,9 @@ $.oFolder.prototype.listFiles = function(filter){
/**
* get the files from the folder
* @param {string} [filter] Filter wildcards for the content of the folder.
* @param {string} [filter] Filter wildcards for the content of the folder.
*
* @return: { $.oFile[] } The file content of the folder.
* @returns {$.oFile[]} A list of files contained in the folder as oFile objects.
*/
$.oFolder.prototype.getFiles = function( filter ){
if (typeof filter === 'undefined') var filter = "*";
@ -245,9 +251,9 @@ $.oFolder.prototype.getFiles = function( filter ){
/**
* lists the folder names contained inside the folder.
* @param {string} [filter] Filter wildcards for the content of the folder.
* @param {string} [filter="*.*"] Filter wildcards for the content of the folder.
*
* @return: { string[] } The file content of folder.
* @returns {string[]} The names of the files contained in the folder that match the filter.
*/
$.oFolder.prototype.listFolders = function(filter){
@ -275,7 +281,7 @@ $.oFolder.prototype.listFolders = function(filter){
* gets the folders inside the oFolder
* @param {string} [filter] Filter wildcards for the content of the folder.
*
* @return: { $.oFolder[] } The folder contents of the folder.
* @returns {$.oFolder[]} A list of folders contained in the folder, as oFolder objects.
*/
$.oFolder.prototype.getFolders = function( filter ){
if (typeof filter === 'undefined') var filter = "*";
@ -295,44 +301,58 @@ $.oFolder.prototype.getFolders = function( filter ){
/**
* Creates the folder, if it doesn't already exist.
*
* @return: { bool } The existence of the newly created folder.
* @returns { bool } The existence of the newly created folder.
*/
$.oFolder.prototype.create = function(){
if( this.exists ){
this.$.debug("folder "+this.path+" already exists and will not be created", this.$.DEBUG_LEVEL.WARNING)
return true;
}
if( this.exists ){
this.$.debug("folder "+this.path+" already exists and will not be created", this.$.DEBUG_LEVEL.WARNING)
return true;
}
var dir = new QDir(this.path);
//dir.path = this.path;
try{
dir.mkpath(this.path);
return this.exists;
}catch(err){
this.$.debug(err+" ", this.$.DEBUG_LEVEL.ERROR)
return false;
}
var dir = new QDir(this.path);
dir.mkpath(this.path);
if (!this.exists) throw new Error ("folder " + this.path + " could not be created.")
}
/**
* WIP Copy the folder and its contents to another path. WIP
* @param {string} [folderPath] The path to the folder location to copy to (CFNote: Should this not be a $.oFolder?)
* @param {string} [copyName] The name of the folder to copy (CFNote: Should this be avoided and the folderPath be the full path?)
* @param {bool} [overwrite] Whether to overwrite the target.
* Copy the folder and its contents to another path.
* @param {string} folderPath The path to an existing folder in which to copy this folder. (Can provide an oFolder)
* @param {string} [copyName] Optionally, a name for the folder copy, if different from the original
* @param {bool} [overwrite=false] Whether to overwrite the files that are already present at the copy location.
* @returns {$.oFolder} the oFolder describing the newly created copy.
*/
$.oFolder.prototype.copy = function( folderPath, copyName, overwrite ){
if (typeof overwrite === 'undefined') var overwrite = false;
if (typeof copyName === 'undefined') var copyName = this.name;
if (typeof folderPath === 'undefined') var folderPath = this.folder.path;
// TODO: it should propagate errors from the recursive copy and throw them before ending?
if (typeof overwrite === 'undefined') var overwrite = false;
if (typeof copyName === 'undefined' || !copyName) var copyName = this.name;
if (!(folderPath instanceof this.$.oFolder)) folderPath = new $.oFolder(folderPath);
if (this.name == copyName && folderPath == this.folder.path) copyName += "_copy";
if (this.name == copyName && folderPath == this.folder.path) copyName += "_copy";
if (!folderPath.exists) throw new Error("Target folder " + folderPath +" doesn't exist. Can't copy folder "+this.path)
var copyPath = folderPath+copyName;
var nextFolder = new $.oFolder(folderPath.path + "/" + copyName);
nextFolder.create();
var files = this.getFiles();
for (var i in files){
var _file = files[i];
var targetFile = new $.oFile(nextFolder.path + "/" + _file.fullName);
// TODO: deep recursive copy file by file of the contents
// deal with overwriting
if (targetFile.exists && !overwrite){
this.$.debug("File " + targetFile + " already exists, skipping copy of "+ _file, this.$.DEBUG_LEVEL.ERROR);
continue;
}
_file.copy(nextFolder, undefined, overwrite);
}
var folders = this.getFolders();
for (var i in folders){
folders[i].copy(nextFolder, undefined, overwrite);
}
return nextFolder;
}
@ -365,7 +385,7 @@ $.oFolder.prototype.move = function( destFolderPath, overwrite ){
return true;
}catch (err){
throw new Error ("Couldn't move folder "+this.path+" to new address "+destPath);
throw new Error ("Couldn't move folder "+this.path+" to new address "+destPath + ": " + err);
}
}
@ -383,7 +403,7 @@ $.oFolder.prototype.moveToFolder = function( destFolderPath, overwrite ){
var folder = destFolderPath.path;
var name = this.name;
destFolderPath.move(folder+"/"+name, overwrite);
this.move(folder+"/"+name, overwrite);
}
@ -393,6 +413,8 @@ $.oFolder.prototype.moveToFolder = function( destFolderPath, overwrite ){
*/
$.oFolder.prototype.rename = function(newName){
var destFolderPath = this.folder.path+"/"+newName
if ((new this.$.oFolder(destFolderPath)).exists) throw new Error("Can't rename folder "+this.path + " to "+newName+", a folder already exists at this location")
this.move(destFolderPath)
}
@ -460,8 +482,16 @@ $.oFolder.prototype.toString = function(){
* @property {string} path The path to the file.
*/
$.oFile = function(path){
this._type = "file";
this._path = fileMapper.toNativePath(path).split('\\').join('/');
this._type = "file";
this._path = fileMapper.toNativePath(path).split('\\').join('/');
// fix lowercase drive letter
var path_components = this._path.split("/");
if (path_components[0] && about.isWindowsArch()){
// local path that starts with a drive letter
path_components[0] = path_components[0].toUpperCase()
this._path = path_components.join("/");
}
}
@ -714,21 +744,15 @@ $.oFile.prototype.copy = function( destfolder, copyName, overwrite){
var _dest = new PermanentFile(destfolder+"/"+_fileName);
if (_dest.exists() && !overwrite){
this.$.debug("Destination file "+destfolder+"/"+_fileName+" exists and will not be overwritten. Can't copy file.", this.DEBUG_LEVEL.ERROR);
return false;
throw new Error("Destination file "+destfolder+"/"+_fileName+" exists and will not be overwritten. Can't copy file.", this.DEBUG_LEVEL.ERROR);
}
this.$.debug("copying "+_file.path()+" to "+_dest.path(), this.$.DEBUG_LEVEL.LOG)
try{
var success = _file.copy(_dest);
if (!success) throw new Error ();
}catch(err){
this.$.debug("Copy of file "+_file.path()+" to location "+_dest.path()+" has failed.", this.$.DEBUG_LEVEL.ERROR)
}
var success = _file.copy(_dest);
if (!success) throw new Error ("Copy of file "+_file.path()+" to location "+_dest.path()+" has failed.", this.$.DEBUG_LEVEL.ERROR)
if (success) return new this.$.oFile(_dest.path());
return false;
return new this.$.oFile(_dest.path());
}

View file

@ -47,8 +47,8 @@
// //
//////////////////////////////////////
//////////////////////////////////////
/**
* The constructor for the $.oFrame.
* @constructor
@ -64,7 +64,7 @@
* @example
* // to access the frames of a column, simply call oColumn.frames:
* var myColumn = $.scn.columns[O] // access the first column of the list of columns present in the scene
*
*
* var frames = myColumn.frames;
*
* // then you can iterate over them to check their properties:
@ -73,8 +73,8 @@
* $.log(frames[i].isKeyframe);
* $.log(frames[i].continuity);
* }
*
* // you can get and set the value of the frame
*
* // you can get and set the value of the frame
*
* frames[1].value = 5; // frame array values and frameNumbers are matched, so this sets the value of frame 1
*/
@ -82,26 +82,26 @@ $.oFrame = function( frameNumber, oColumnObject, subColumns ){
this._type = "frame";
this.frameNumber = frameNumber;
if( oColumnObject instanceof $.oAttribute ){ //Direct access to an attribute, when not keyable. We still provide a frame access for consistency. > MCNote ?????
this.column = false;
this.attributeObject = oColumnObject;
this.attributeObject = oColumnObject;
}else if( oColumnObject instanceof $.oColumn ){
this.column = oColumnObject;
if (this.column && typeof subColumns === 'undefined'){
var subColumns = this.column.subColumns;
}else{
var subColumns = { a : 1 };
}
this.attributeObject = this.column.attributeObject;
}
this.subColumns = subColumns;
}
// $.oFrame Object Properties
/**
* The value of the frame. Contextual to the attribute type.
@ -119,7 +119,7 @@ Object.defineProperty($.oFrame.prototype, 'value', {
return this.column.getValue(this.frameNumber);
}
/*
// this.$.log("Getting value of frame "+this.frameNumber+" of column "+this.column.name)
// this.$.log("Getting value of frame "+this.frameNumber+" of column "+this.column.name)
if (this.attributeObject){
return this.attributeObject.getValue(this.frameNumber);
}else{
@ -139,11 +139,11 @@ Object.defineProperty($.oFrame.prototype, 'value', {
/*// this.$.log("Setting frame "+this.frameNumber+" of column "+this.column.name+" to value: "+newValue)
if (this.attributeObject){
this.attributeObject.setValue( newValue, this.frameNumber );
this.attributeObject.setValue( newValue, this.frameNumber );
}else{
this.$.debug("setting unlinked column "+this.name+" value to "+newValue+" at frame "+this.frameNumber, this.$.DEBUG_LEVEL.ERROR);
this.$.debug("warning : setting a value on a column without attribute destroys value fidelity", this.$.DEBUG_LEVEL.ERROR);
var _subColumns = this.subColumns;
for (var i in _subColumns){
column.setEntry (this.name, _subColumns[i], this.frameNumber, newValue);
@ -151,7 +151,7 @@ Object.defineProperty($.oFrame.prototype, 'value', {
}*/
}
});
/**
* Whether the frame is a keyframe.
@ -162,7 +162,7 @@ Object.defineProperty($.oFrame.prototype, 'isKeyframe', {
get : function(){
if( !this.column ) return true;
if( this.frameNumber == 0 ) return false; // frames array start at 0 but first index is not a real frame
var _column = this.column.uniqueName;
if (this.column.type == 'DRAWING' || this.column.type == 'TIMING'){
if( column.getTimesheetEntry){
@ -175,14 +175,14 @@ Object.defineProperty($.oFrame.prototype, 'isKeyframe', {
}
return false;
},
set : function(keyframe){
this.$.log("setting keyframe for frame "+this.frameNumber);
var col = this.column;
if( !col ) return;
var _column = col.uniqueName;
if( col.type == "DRAWING" ){
if (keyframe){
column.addKeyDrawingExposureAt( _column, this.frameNumber );
@ -193,27 +193,27 @@ Object.defineProperty($.oFrame.prototype, 'isKeyframe', {
if (keyframe){
//Sanity check, in certain situations, the setKeyframe resets to 0 (specifically if there is no pre-existing key elsewhere.)
//This will check the value prior to the key, set the key, and enforce the value after.
//var val = 0.0;
// try{
var val = this.value;
// }catch(err){}
//this.$.log("setting keyframe for frame "+this.frameNumber);
column.setKeyFrame( _column, this.frameNumber );
// try{
//var post_val = this.value;
//if (val != post_val) {
this.value = val;
//}
// }catch(err){}
// }catch(err){}
}else{
column.clearKeyFrame( _column, this.frameNumber );
}
}
}
});
/**
* Whether the frame is a keyframe.
@ -225,13 +225,13 @@ Object.defineProperty($.oFrame.prototype, 'isKeyFrame', {
get : function(){
return this.isKeyframe;
},
set : function(keyframe){
this.$.debug("oFrame.isKeyFrame is deprecated. Use oFrame.isKeyframe instead.", this.$.DEBUG_LEVEL.ERROR);
this.isKeyframe = keyframe;
}
});
/**
* Whether the frame is a keyframe.
@ -242,13 +242,13 @@ Object.defineProperty($.oFrame.prototype, 'isKey', {
get : function(){
return this.isKeyframe;
},
set : function(keyFrame){
this.isKeyframe = keyFrame;
}
});
/**
* The duration of the keyframe exposure of the frame.
* @name $.oFrame#duration
@ -258,7 +258,7 @@ Object.defineProperty($.oFrame.prototype, 'duration', {
get : function(){
var _startFrame = this.startFrame;
var _sceneLength = frame.numberOf()
if( !this.column ){
return _sceneLength;
}
@ -270,13 +270,13 @@ Object.defineProperty($.oFrame.prototype, 'duration', {
}
return _sceneLength - _startFrame;
},
set : function( val ){
throw "Not implemented.";
}
});
/**
* Identifies if the frame is blank/empty.
* @name $.oFrame#isBlank
@ -288,21 +288,21 @@ Object.defineProperty($.oFrame.prototype, 'isBlank', {
if( !col ){
return false;
}
if ( col.type != "DRAWING") return false;
if( !column.getTimesheetEntry ){
return (this.value == "");
}
return column.getTimesheetEntry( col.uniqueName, 1, this.frameNumber ).emptyCell;
},
set : function( val ){
throw "Not implemented.";
}
});
/**
* Identifies the starting frame of the exposed drawing.
@ -315,10 +315,10 @@ Object.defineProperty($.oFrame.prototype, 'startFrame', {
if( !this.column ){
return 1;
}
if (this.isKeyframe) return this.frameNumber;
if (this.isBlank) return -1;
var _frames = this.column.frames;
for (var i=this.frameNumber-1; i>=1; i--){
if (_frames[i].isKeyframe) return _frames[i].frameNumber;
@ -326,8 +326,8 @@ Object.defineProperty($.oFrame.prototype, 'startFrame', {
return -1;
}
});
/**
* Returns the drawing types used in the drawing column. K = key drawings, I = inbetween, B = breakdown
* @name $.oFrame#marker
@ -338,24 +338,24 @@ Object.defineProperty($.oFrame.prototype, 'marker', {
if( !this.column ){
return "";
}
var _column = this.column;
if (_column.type != "DRAWING") return "";
return column.getDrawingType(_column.uniqueName, this.frameNumber);
},
set: function( marker ){
if( !this.column ){
return;
}
var _column = this.column;
if (_column.type != "DRAWING") throw "can't set 'marker' property on columns that are not 'DRAWING' type"
column.setDrawingType( _column.uniqueName, this.frameNumber, marker );
}
});
/**
* Find the index of this frame in the corresponding columns keyframes. -1 if unavailable.
* @name $.oFrame#keyframeIndex
@ -368,8 +368,8 @@ Object.defineProperty($.oFrame.prototype, 'keyframeIndex', {
return _kfIndex;
}
});
/**
* Find the the nearest keyframe to this, on the left. Returns itself if it is a key.
* @name $.oFrame#keyframeLeft
@ -380,8 +380,8 @@ Object.defineProperty($.oFrame.prototype, 'keyframeLeft', {
return (new this.$.oFrame(this.startFrame, this.column));
}
});
/**
* Find the the nearest keyframe to this, on the right.
* @name $.oFrame#keyframeRight
@ -392,7 +392,7 @@ Object.defineProperty($.oFrame.prototype, 'keyframeRight', {
return (new this.$.oFrame(this.startFrame+this.duration, this.column));
}
});
/**
* Access the velocity value of a keyframe from a 3DPATH column.
@ -418,7 +418,7 @@ Object.defineProperty($.oFrame.prototype, 'velocity', {
}
});
/**
* Gets a general ease object for the frame, which can be used to set frames to the same ease values. ease Objects contain the following properties:
* x : frame number
@ -446,7 +446,7 @@ Object.defineProperty($.oFrame.prototype, 'ease', {
constant : func.pointConstSeg(_columnName, _index),
continuity : func.pointContinuity(_columnName, _index)
}
if( _column.easeType == "BEZIER" ){
ease.easeIn = new this.$.oPoint(func.pointHandleLeftX(_columnName, _index), func.pointHandleLeftY(_columnName, _index),0);
ease.easeOut = new this.$.oPoint(func.pointHandleRightX(_columnName, _index), func.pointHandleRightY(_columnName, _index),0);
@ -529,7 +529,7 @@ Object.defineProperty($.oFrame.prototype, 'continuity', {
var _frame = this.keyframeLeft; //Works on the left keyframe, in the event that this is not a keyframe itself.
return _frame.ease.continuity;
},
},
set : function( newContinuity ){
var _frame = this.keyframeLeft; //Works on the left keyframe, in the event that this is not a keyframe itself.
@ -551,8 +551,8 @@ Object.defineProperty($.oFrame.prototype, 'constant', {
var _frame = this.keyframeLeft; //Works on the left keyframe, in the event that this is not a keyframe itself.
return _frame.ease.constant;
},
},
set : function( newConstant ){
if( this.column ){
var _frame = this.keyframeLeft; //Works on the left keyframe, in the event that this is not a keyframe itself.
@ -573,7 +573,7 @@ Object.defineProperty($.oFrame.prototype, 'constant', {
Object.defineProperty($.oFrame.prototype, 'tween', {
get : function(){
return !this.constant;
},
},
set : function( new_tween ){
this.constant = !new_tween;
}
@ -592,35 +592,32 @@ Object.defineProperty($.oFrame.prototype, 'tween', {
$.oFrame.prototype.extend = function( duration, replace ){
if (typeof replace === 'undefined') var replace = true;
// setting this to false will insert frames as opposed to overwrite existing ones
if( !this.column ){
return;
}
var _frames = this.column.frames;
if (typeof duration === 'undefined'){
// extend to next non blank keyframe if not set
var duration = 0;
var curFrameEnd = this.startFrame + this.duration;
var sceneLength = frame.numberOf();
var sceneLength = this.$.scene.length;
// find next non blank keyframe
while (_frames[ curFrameEnd + duration].isBlank && (curFrameEnd + duration) < sceneLength){
duration += _frames[curFrameEnd+duration].duration;
while ((curFrameEnd + duration) <= sceneLength && _frames[curFrameEnd + duration].isBlank){
duration ++;
}
// set to sceneEnd if sceneEnd is reached
if (curFrameEnd+duration >= sceneLength) duration = sceneLength-curFrameEnd+1;
}
var _value = this.value;
var startExtending = this.startFrame+this.duration;
for (var i = 0; i<duration; i++){
if (!replace){
// TODO : push all other frames back
}
_frames[startExtending+i].value = _value;
}
}
}

View file

@ -0,0 +1,717 @@
//////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////
//
// openHarmony Library
//
//
// Developped by Mathieu Chaptel, Chris Fourney
//
//
// This library is an open source implementation of a Document Object Model
// for Toonboom Harmony. It also implements patterns similar to JQuery
// for traversing this DOM.
//
// Its intended purpose is to simplify and streamline toonboom scripting to
// empower users and be easy on newcomers, with default parameters values,
// and by hiding the heavy lifting required by the official API.
//
// This library is provided as is and is a work in progress. As such, not every
// function has been implemented or is garanteed to work. Feel free to contribute
// improvements to its official github. If you do make sure you follow the provided
// template and naming conventions and document your new methods properly.
//
// This library doesn't overwrite any of the objects and classes of the official
// Toonboom API which must remains available.
//
// This library is made available under the Mozilla Public license 2.0.
// https://www.mozilla.org/en-US/MPL/2.0/
//
// The repository for this library is available at the address:
// https://github.com/cfourney/OpenHarmony/
//
//
// For any requests feel free to contact m.chaptel@gmail.com
//
//
//
//
//////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////
//////////////////////////////////////
// //
// //
// $.oPoint class //
// //
// //
//////////////////////////////////////
//////////////////////////////////////
/**
* The $.oPoint helper class - representing a 3D point.
* @constructor
* @classdesc $.oPoint Base Class
* @param {float} x Horizontal coordinate
* @param {float} y Vertical coordinate
* @param {float} [z] Depth Coordinate
*
* @property {float} x Horizontal coordinate
* @property {float} y Vertical coordinate
* @property {float} z Depth Coordinate
*/
$.oPoint = function(x, y, z){
if (typeof z === 'undefined') var z = 0;
this._type = "point";
this.x = x;
this.y = y;
this.z = z;
}
/**
* @name $.oPoint#polarCoordinates
* an object containing {angle (float), radius (float)} values that represents polar coordinates (angle in radians) for the point's x and y value (z not yet supported)
*/
Object.defineProperty( $.oPoint.prototype, 'polarCoordinates', {
get: function(){
var _angle = Math.atan2(this.y, this.x)
var _radius = Math.sqrt(this.x*this.x+this.y*this.y)
return {angle: _angle, radius: _radius};
},
set: function(polarPoint){
var _angle = polarPoint.angle;
var _radius = polarPoint.radius;
var _x = Math.cos(_angle)*_radius;
var _y = Math.sin(_angle)*_radius;
this.x = _x;
this.y = _y;
}
});
/**
* Translate the point by the provided values.
* @param {int} x the x value to move the point by.
* @param {int} y the y value to move the point by.
* @param {int} z the z value to move the point by.
*
* @returns { $.oPoint } Returns self (for inline addition).
*/
$.oPoint.prototype.translate = function( x, y, z){
if (typeof x === 'undefined') var x = 0;
if (typeof y === 'undefined') var y = 0;
if (typeof z === 'undefined') var z = 0;
this.x += x;
this.y += y;
this.z += z;
return this;
}
/**
* Translate the point by the provided values.
* @param {int} x the x value to move the point by.
* @param {int} y the y value to move the point by.
* @param {int} z the z value to move the point by.
*
* @return: { $.oPoint } Returns self (for inline addition).
*/
$.oPoint.prototype.add = function( x, y, z ){
if (typeof x === 'undefined') var x = 0;
if (typeof y === 'undefined') var y = 0;
if (typeof z === 'undefined') var z = 0;
var x = this.x + x;
var y = this.y + y;
var z = this.z + z;
return new this.$.oPoint(x, y, z);
}
/**
* The distance between two points.
* @param {$.oPoint} point the other point to calculate the distance from.
* @returns {float}
*/
$.oPoint.prototype.distance = function ( point ){
var distanceX = point.x-this.x;
var distanceY = point.y-this.y;
var distanceZ = point.z-this.z;
return Math.sqrt(distanceX*distanceX + distanceY*distanceY + distanceZ*distanceZ)
}
/**
* Adds the point to the coordinates of the current oPoint.
* @param {$.oPoint} add_pt The point to add to this point.
* @returns { $.oPoint } Returns itself (for inline addition).
*/
$.oPoint.prototype.pointAdd = function( add_pt ){
this.x += add_pt.x;
this.y += add_pt.y;
this.z += add_pt.z;
return this;
}
/**
* Adds the point to the coordinates of the current oPoint and returns a new oPoint with the result.
* @param {$.oPoint} oPoint The point to add to this point.
* @returns {$.oPoint}
*/
$.oPoint.prototype.addPoint = function( point ){
var x = this.x + point.x;
var y = this.y + point.y;
var z = this.z + point.z;
return new this.$.oPoint(x, y, z);
}
/**
* Subtracts the point to the coordinates of the current oPoint.
* @param {$.oPoint} sub_pt The point to subtract to this point.
* @returns { $.oPoint } Returns itself (for inline addition).
*/
$.oPoint.prototype.pointSubtract = function( sub_pt ){
this.x -= sub_pt.x;
this.y -= sub_pt.y;
this.z -= sub_pt.z;
return this;
}
/**
* Subtracts the point to the coordinates of the current oPoint and returns a new oPoint with the result.
* @param {$.oPoint} point The point to subtract to this point.
* @returns {$.oPoint} a new independant oPoint.
*/
$.oPoint.prototype.subtractPoint = function( point ){
var x = this.x - point.x;
var y = this.y - point.y;
var z = this.z - point.z;
return new this.$.oPoint(x, y, z);
}
/**
* Multiply all coordinates by this value.
* @param {float} float_val Multiply all coordinates by this value.
*
* @returns { $.oPoint } Returns itself (for inline addition).
*/
$.oPoint.prototype.multiply = function( float_val ){
this.x *= float_val;
this.y *= float_val;
this.z *= float_val;
return this;
}
/**
* Divides all coordinates by this value.
* @param {float} float_val Divide all coordinates by this value.
*
* @returns { $.oPoint } Returns itself (for inline addition).
*/
$.oPoint.prototype.divide = function( float_val ){
this.x /= float_val;
this.y /= float_val;
this.z /= float_val;
return this;
}
/**
* Find average of provided points.
* @param {$.oPoint[]} point_array The array of points to get the average.
*
* @returns { $.oPoint } Returns the $.oPoint average of provided points.
*/
$.oPoint.prototype.pointAverage = function( point_array ){
var _avg = new this.$.oPoint( 0.0, 0.0, 0.0 );
for (var x=0; x<point_array.length; x++) {
_avg.pointAdd(point_array[x]);
}
_avg.divide(point_array.length);
return _avg;
}
/**
* Converts a Drawing point coordinate into a scene coordinate, as used by pegs (since drawings are 2D, z is untouched)
* @returns {$.oPoint}
*/
$.oPoint.prototype.convertToSceneCoordinates = function () {
return new this.$.oPoint(this.x/this.$.scene.fieldVectorResolutionX, this.y/this.$.scene.fieldVectorResolutionY, this.z);
}
/**
* Converts a scene coordinate point into a Drawing space coordinate, as used by Drawing tools and $.oShape (since drawings are 2D, z is untouched)
* @returns {$.oPoint}
*/
$.oPoint.prototype.convertToDrawingSpace = function () {
return new this.$.oPoint(this.x * this.$.scene.fieldVectorResolutionX, this.y * this.$.scene.fieldVectorResolutionY, this.z);
}
/**
* Uses the scene settings to convert this as a worldspace point into an OpenGL point, used in underlying transformation operations in Harmony.
* OpenGL units have a square aspect ratio and go from -1 to 1 vertically in the camera field.
* @returns nothing
*/
$.oPoint.prototype.convertToOpenGL = function(){
var qpt = scene.toOGL( new Point3d( this.x, this.y, this.z ) );
this.x = qpt.x;
this.y = qpt.y;
this.z = qpt.z;
}
/**
* Uses the scene settings to convert this as an OpenGL point into a Harmony worldspace point.
* @returns nothing
*/
$.oPoint.prototype.convertToWorldspace = function(){
var qpt = scene.fromOGL( new Point3d( this.x, this.y, this.z ) );
this.x = qpt.x;
this.y = qpt.y;
this.z = qpt.z;
}
/**
* Linearily Interpolate between this (0.0) and the provided point (1.0)
* @param {$.oPoint} point The target point at 100%
* @param {double} perc 0-1.0 value to linearily interp
*
* @return: { $.oPoint } The interpolated value.
*/
$.oPoint.prototype.lerp = function( point, perc ){
var delta = new this.$.oPoint( point.x, point.y, point.z );
delta = delta.pointSubtract( this );
delta.multiply( perc );
delta.pointAdd( this );
return delta;
}
$.oPoint.prototype.toString = function(){
return this._type+": {x:"+this.x+", y:"+this.y+", z:"+this.z+"}";
}
//////////////////////////////////////
//////////////////////////////////////
// //
// //
// $.oBox class //
// //
// //
//////////////////////////////////////
//////////////////////////////////////
/**
* The $.oBox helper class - representing a 2D box.
* @constructor
* @classdesc $.oBox Base Class
* @param {float} left left horizontal bound
* @param {float} top top vertical bound
* @param {float} right right horizontal bound
* @param {float} bottom bottom vertical bound
*
* @property {float} left left horizontal bound
* @property {float} top top vertical bound
* @property {float} right right horizontal bound
* @property {float} bottom bottom vertical bound
*/
$.oBox = function( left, top, right, bottom ){
this._type = "box";
if (typeof top === 'undefined') var top = Infinity
if (typeof left === 'undefined') var left = Infinity
if (typeof right === 'undefined') var right = -Infinity
if (typeof bottom === 'undefined') var bottom = -Infinity
this.top = top;
this.left = left;
this.right = right;
this.bottom = bottom;
}
/**
* The width of the box.
* @name $.oBox#width
* @type {float}
*/
Object.defineProperty($.oBox.prototype, 'width', {
get : function(){
return this.right - this.left + 1; //Inclusive size.
}
})
/**
* The height of the box.
* @name $.oBox#height
* @type {float}
*/
Object.defineProperty($.oBox.prototype, 'height', {
get : function(){
return this.bottom - this.top;
}
})
/**
* The center of the box.
* @name $.oBox#center
* @type {$.oPoint}
*/
Object.defineProperty($.oBox.prototype, 'center', {
get : function(){
return new this.$.oPoint(this.left+this.width/2, this.top+this.height/2);
}
})
/**
* Adds the input box to the bounds of the current $.oBox.
* @param {$.oBox} box The $.oBox to include.
*/
$.oBox.prototype.include = function(box){
if (box.left < this.left) this.left = box.left;
if (box.top < this.top) this.top = box.top;
if (box.right > this.right) this.right = box.right;
if (box.bottom > this.bottom) this.bottom = box.bottom;
}
/**
* Checks wether the box contains another $.oBox.
* @param {$.oBox} box The $.oBox to check for.
* @param {bool} [partial=false] wether to accept partially contained boxes.
*/
$.oBox.prototype.contains = function(box, partial){
if (typeof partial === 'undefined') var partial = false;
var fitLeft = (box.left >= this.left);
var fitTop = (box.top >= this.top);
var fitRight =(box.right <= this.right);
var fitBottom = (box.bottom <= this.bottom);
if (partial){
return (fitLeft || fitRight) && (fitTop || fitBottom);
}else{
return fitLeft && fitRight && fitTop && fitBottom;
}
}
/**
* Adds the bounds of the nodes to the current $.oBox.
* @param {oNode[]} oNodeArray An array of nodes to include in the box.
*/
$.oBox.prototype.includeNodes = function(oNodeArray){
// convert to array if only one node is passed
if (!Array.isArray(oNodeArray)) oNodeArray = [oNodeArray];
for (var i in oNodeArray){
var _node = oNodeArray[i];
var _nodeBox = _node.bounds;
this.include(_nodeBox);
}
}
/**
* @private
*/
$.oBox.prototype.toString = function(){
return "{top:"+this.top+", right:"+this.right+", bottom:"+this.bottom+", left:"+this.left+"}"
}
//////////////////////////////////////
//////////////////////////////////////
// //
// //
// $.oMatrix class //
// //
// //
//////////////////////////////////////
//////////////////////////////////////
/**
* The $.oMatrix constructor.
* @constructor
* @classdesc The $.oMatrix is a subclass of the native Matrix4x4 object from Harmony. It has the same methods and properties plus the ones listed here.
* @param {Matrix4x4} matrixObject a matrix object to initialize the instance from
*/
$.oMatrix = function(matrixObject){
Matrix4x4.constructor.call(this);
if (matrixObject){
log(matrixObject)
this.m00 = matrixObject.m00;
this.m01 = matrixObject.m01;
this.m02 = matrixObject.m02;
this.m03 = matrixObject.m03;
this.m10 = matrixObject.m10;
this.m11 = matrixObject.m11;
this.m12 = matrixObject.m12;
this.m13 = matrixObject.m13;
this.m20 = matrixObject.m20;
this.m21 = matrixObject.m21;
this.m22 = matrixObject.m22;
this.m23 = matrixObject.m23;
this.m30 = matrixObject.m30;
this.m31 = matrixObject.m31;
this.m32 = matrixObject.m32;
this.m33 = matrixObject.m33;
}
}
$.oMatrix.prototype = Object.create(Matrix4x4.prototype)
/**
* A 2D array that contains the values from the matrix, rows by rows.
* @name $.oMatrix#values
* @type {Array}
*/
Object.defineProperty($.oMatrix.prototype, "values", {
get:function(){
return [
[this.m00, this.m01, this.m02, this.m03],
[this.m10, this.m11, this.m12, this.m13],
[this.m20, this.m21, this.m22, this.m23],
[this.m30, this.m31, this.m32, this.m33],
]
}
})
/**
* @private
*/
$.oMatrix.prototype.toString = function(){
return "< $.oMatrix object : \n"+this.values.join("\n")+">";
}
//////////////////////////////////////
//////////////////////////////////////
// //
// //
// $.oVector class //
// //
// //
//////////////////////////////////////
//////////////////////////////////////
/**
* The $.oVector constructor.
* @constructor
* @classdesc The $.oVector is a replacement for the Vector3d objects of Harmony.
* @param {float} x a x coordinate for this vector.
* @param {float} y a y coordinate for this vector.
* @param {float} [z=0] a z coordinate for this vector. If ommited, will be set to 0 and vector will be 2D.
*/
$.oVector = function(x, y, z){
if (typeof z === "undefined" || isNaN(z)) var z = 0;
// since Vector3d doesn't have a prototype, we need to cheat to subclass it.
this._vector = new Vector3d(x, y, z);
}
/**
* The X Coordinate of the vector.
* @name $.oVector#x
* @type {float}
*/
Object.defineProperty($.oVector.prototype, "x", {
get: function(){
return this._vector.x;
},
set: function(newX){
this._vector.x = newX;
}
})
/**
* The Y Coordinate of the vector.
* @name $.oVector#y
* @type {float}
*/
Object.defineProperty($.oVector.prototype, "y", {
get: function(){
return this._vector.y;
},
set: function(newY){
this._vector.y = newY;
}
})
/**
* The Z Coordinate of the vector.
* @name $.oVector#z
* @type {float}
*/
Object.defineProperty($.oVector.prototype, "z", {
get: function(){
return this._vector.z;
},
set: function(newX){
this._vector.z = newX;
}
})
/**
* The length of the vector.
* @name $.oVector#length
* @type {float}
* @readonly
*/
Object.defineProperty($.oVector.prototype, "length", {
get: function(){
return this._vector.length();
}
})
/**
* @static
* A function of the oVector class (not oVector objects) that gives a vector from two points.
*/
$.oVector.fromPoints = function(pointA, pointB){
return new $.oVector(pointB.x-pointA.x, pointB.y-pointA.y, pointB.z-pointA.z);
}
/**
* Adds another vector to this one.
* @param {$.oVector} vector2
* @returns {$.oVector} returns itself.
*/
$.oVector.prototype.add = function (vector2){
this.x += vector2.x;
this.y += vector2.y;
this.z += vector2.z;
return this;
}
/**
* Multiply this vector coordinates by a number (scalar multiplication)
* @param {float} num
* @returns {$.oVector} returns itself
*/
$.oVector.prototype.multiply = function(num){
this.x = num*this.x;
this.y = num*this.y;
this.z = num*this.z;
return this;
}
/**
* The dot product of the two vectors
* @param {$.oVector} vector2 a vector object.
* @returns {float} the resultant vector from the dot product of the two vectors.
*/
$.oVector.prototype.dot = function(vector2){
var _dot = this._vector.dot(new Vector3d(vector2.x, vector2.y, vector2.z));
return _dot;
}
/**
* The cross product of the two vectors
* @param {$.oVector} vector2 a vector object.
* @returns {$.oVector} the resultant vector from the dot product of the two vectors.
*/
$.oVector.prototype.cross = function(vector2){
var _cross = this._vector.cross(new Vector3d(vector2.x, vector2.y, vector2.z));
return new this.$.oVector(_cross.x, _cross.y, _cross.z);
}
/**
* The projected vectors resulting from the operation
* @param {$.oVector} vector2 a vector object.
* @returns {$.oVector} the resultant vector from the projection of the current vector.
*/
$.oVector.prototype.project = function(vector2){
var _projection = this._vector.project(new Vector3d(vector2.x, vector2.y, vector2.z));
return new this.$.oVector(_projection.x, _projection.y, _projection.z);
}
/**
* Normalize the vector.
* @returns {$.oVector} returns itself after normalization.
*/
$.oVector.prototype.normalize = function(){
this._vector.normalize();
return this;
}
/**
* The angle of this vector in radians.
* @name $.oVector#angle
* @type {float}
* @readonly
*/
Object.defineProperty($.oVector.prototype, "angle", {
get: function(){
return Math.atan2(this.y, this.x);
}
})
/**
* The angle of this vector in degrees.
* @name $.oVector#degreesAngle
* @type {float}
* @readonly
*/
Object.defineProperty($.oVector.prototype, "degreesAngle", {
get: function(){
return this.angle * (180 / Math.PI);
}
})
/**
* @private
*/
$.oVector.prototype.toString = function(){
return "<$.oVector ["+this.x+", "+this.y+", "+this.z+"]>";
}

View file

@ -46,45 +46,42 @@
//////////////////////////////////////
// //
// //
// $.oTools class //
// $.oUtils class //
// //
// //
//////////////////////////////////////
//////////////////////////////////////
/**
* The $.oUtils helper class -- providing generic utilities.
* @constructor
* @classdesc $.tools utility Class
* The $.oUtils helper class -- providing generic utilities. Doesn't need instanciation.
* @classdesc $.oUtils utility Class
*/
$.oUtils = function(){
this._type = "tools";
this._type = "utils";
}
/**
* Copies the file to the folder.
* @param {string} [folder] Content to write to the file.
* @param {string} [copyName] Name of the copied file.
* @param {bool} [overwrite] Whether to overwrite the file.
*
* @return: { object } The result of the copy.
* Finds the longest common substring between two strings.
* @param {string} str1
* @param {string} str2
* @returns {string} the found string
*/
$.oUtils.prototype.longestCommonSubstring = function( str1, str2 ){
$.oUtils.longestCommonSubstring = function( str1, str2 ){
if (!str1 || !str2)
return {
length: 0,
sequence: "",
offset: 0
};
var sequence = "",
str1Length = str1.length,
str2Length = str2.length,
num = new Array(str1Length),
maxlen = 0,
lastSubsBegin = 0;
for (var i = 0; i < str1Length; i++) {
var subArray = new Array(str2Length);
for (var j = 0; j < str2Length; j++)

View file

@ -1255,6 +1255,21 @@ Object.defineProperty($.oLink.prototype, 'isMultiLevel', {
});
/**
* Compares the start and end nodes groups to see if the path traverses several groups or not.
* @name $.oLink#isMultiLevel
* @readonly
* @type {bool}
*/
Object.defineProperty($.oLink.prototype, 'waypoints', {
get : function(){
if (!this.linked) return []
var _waypoints = waypoint.getAllWaypointsAbove (this.inNode, this.inPort)
return _waypoints;
}
});
/**
* Get a link that can be connected by working out ports that can be used. If a link already exists, it will be returned.
* @return {$.oLink} A separate $.oLink object that can be connected. Null if none could be constructed.
@ -1267,6 +1282,11 @@ $.oLink.prototype.getValidLink = function(createOutPorts, createInPorts){
var outPort = this._outPort;
var inPort = this._inPort;
if (!start || !end) {
$.debug("A valid link can't be found: node missing in link "+this.toString(), this.$.DEBUG_LEVEL.ERROR)
return null;
}
if (this.isMultiLevel) return null;
var _link = new this.$.oLink(start, end, outPort, inPort);
@ -1298,8 +1318,6 @@ $.oLink.prototype.getValidLink = function(createOutPorts, createInPorts){
}
}
// this.$.log("succesfully created abstract link : "+_link)
return _link;
}
@ -1310,7 +1328,6 @@ $.oLink.prototype.getValidLink = function(createOutPorts, createInPorts){
*/
$.oLink.prototype.connect = function(){
if (this._linked){
//this.$.debug("Nodes "+this._outNode+", "+this.inNode+" already linked", this.$.DEBUG_LEVEL.LOG);
return true;
}
@ -1341,9 +1358,8 @@ $.oLink.prototype.connect = function(){
return success;
}catch(err){
// this.$.debug(err, this.$.DEBUG_LEVEL.ERROR)
this.$.debug("linking nodes "+this._outNode+" to "+this._inNode+" through outPort: "+this._outPort+", inPort: "+this._inPort+", create outports: "+createOutPorts+", create inports:"+createInPorts, this.$.DEBUG_LEVEL.ERROR);
this.$.debug("linking nodes failed", this.$.DEBUG_LEVEL.ERROR);
this.$.debug("Error linking nodes: " +err, this.$.DEBUG_LEVEL.ERROR);
return false;
}
}
@ -1373,7 +1389,7 @@ $.oLink.prototype.disconnect = function(){
$.oLink.prototype.findPorts = function(){
// Unless some ports are specified, this will always find the first link and stop there. Provide more info in case of multiple links
if (this.outNode === undefined || this.inNode === undefined) {
if (!this.outNode|| !this.inNode) {
this.$.debug("calling 'findPorts' for invalid link: "+this.outNode+" > "+this.inNode, this.$.DEBUG_LEVEL.ERROR);
return false;
}

View file

@ -0,0 +1,381 @@
//////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////
//
// openHarmony Library v0.01
//
//
// Developped by Mathieu Chaptel, Chris Fourney...
//
//
// This library is an open source implementation of a Document Object Model
// for Toonboom Harmony. It also implements patterns similar to JQuery
// for traversing this DOM.
//
// Its intended purpose is to simplify and streamline toonboom scripting to
// empower users and be easy on newcomers, with default parameters values,
// and by hiding the heavy lifting required by the official API.
//
// This library is provided as is and is a work in progress. As such, not every
// function has been implemented or is garanteed to work. Feel free to contribute
// improvements to its official github. If you do make sure you follow the provided
// template and naming conventions and document your new methods properly.
//
// This library doesn't overwrite any of the objects and classes of the official
// Toonboom API which must remains available.
//
// This library is made available under the MIT license.
// https://opensource.org/licenses/mit
//
// The repository for this library is available at the address:
// https://github.com/cfourney/OpenHarmony/
//
//
// For any requests feel free to contact m.chaptel@gmail.com
//
//
//
//
//////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////
//////////////////////////////////////
// //
// //
// $.oPalette class //
// //
// //
//////////////////////////////////////
//////////////////////////////////////
//
/**
* $.oPalette constructor.
* @constructor
* @classdesc $.oPalette Base Class
* @param {palette} paletteObject The Harmony palette object.
* @param {paletteList} paletteListObject The Harmony paletteListObject object.
*
* @property {palette} paletteObject The Harmony palette object.
* @property {oSceneObject} scene The DOM Scene object.
*/
$.oPalette = function (paletteObject, paletteListObject) {
this._type = "palette";
this.paletteObject = paletteObject;
this._paletteList = paletteListObject;
this.scene = this.$.scn;
}
// Class properties
$.oPalette.location = {
"environment": PaletteObjectManager.Constants.Location.ENVIRONMENT,
"job": PaletteObjectManager.Constants.Location.JOB,
"scene": PaletteObjectManager.Constants.Location.SCENE,
"element": PaletteObjectManager.Constants.Location.ELEMENT,
"external": PaletteObjectManager.Constants.Location.EXTERNAL
}
// $.oPalette Object Properties
/**
* The palette ID.
* @name $.oPalette#id
* @type {string}
*/
Object.defineProperty($.oPalette.prototype, 'id', {
get: function () {
return this.paletteObject.id;
}
})
/**
* The palette name.
* @name $.oPalette#name
* @type {string}
*/
Object.defineProperty($.oPalette.prototype, 'name', {
get: function () {
return this.paletteObject.getName();
},
set: function (newName) {
// Rename palette file then unlink and relink the palette
this.$.debug("renaming palette " + this.name + " to " + newName, this.$.DEBUG_LEVEL.LOG)
var _paletteFile = this.path;
var _newPath = _paletteFile.folder + "/" + newName;
try{
_paletteFile.move(_newPath + ".plt", true);
}catch(err) {
throw new Error ("couldn't rename palette " + this.path + " to " + newName + ": "+ err)
}
var _list = this._paletteList;
var _name = this.name;
_list.removePaletteById(this.id);
var _paletteObject = _list.insertPalette(_newPath.replace(".plt", ""), this.index);
this.paletteObject = _paletteObject;
}
})
/**
* The palette index in the palette list.
* @name $.oPalette#index
* @type {int}
*/
Object.defineProperty($.oPalette.prototype, 'index', {
get: function () {
var _list = this._paletteList;
var _n = _list.numPalettes;
for (var i = 0; i < _n; i++) {
var _paletteId = _list.getPaletteByIndex(i).id;
if (_paletteId == this.id) return i;
}
},
set: function (newIndex) {
var _list = this._paletteList;
var _path = this.path.path.replace(".plt", "");
_list.removePaletteById(this.id);
_list.insertPalette(_path, newIndex);
}
})
/**
* The element containing the palette if stored in element folder.
* @name $.oPalette#element
* @type {$.oElement}
* @readonly
*/
Object.defineProperty($.oPalette.prototype, 'element', {
get: function () {
var _storage = this.paletteStorage;
var _paletteObject = this._paletteObject;
if (_storage != "element") return null;
return new this.$.oElement(_paletteObject.elementId);
}
})
/**
* The palette path on disk.
* @name $.oPalette#path
* @type {$.oFile}
* @readonly
*/
Object.defineProperty($.oPalette.prototype, 'path', {
get: function () {
var _path = this.paletteObject.getPath();
return new this.$.oFile(_path + "/" + this.name + ".plt");
}
})
/**
* The storage place for the palette (environment, scene, job, element or external)
* @name $.oPalette#paletteStorage
* @type {$.oFile}
*/
Object.defineProperty($.oPalette.prototype, 'paletteStorage', {
get: function () {
var _location = this.$.oPalette.location;
var _storage = {
environment: fileMapper.toNativePath(PaletteObjectManager.Locator.folderForLocation(_location.environment, 1)),
job: fileMapper.toNativePath(PaletteObjectManager.Locator.folderForLocation(_location.job, 1)),
scene: fileMapper.toNativePath(PaletteObjectManager.Locator.folderForLocation(_location.scene, 1))
}
var _path = this.path.folder.path;
if (_path.indexOf("/elements") != -1) {
// find out which element?
return "element";
}
for (var i in _storage) {
if (_storage[i].split("\\").join("/") == _path) return i;
}
return "external";
}
})
/**
* Whether the palette is selected.
* @name $.oPalette#selected
* @type {bool}
*/
Object.defineProperty($.oPalette.prototype, 'selected', {
get: function () {
var _currentId = PaletteManager.getCurrentPaletteId()
return this.id == _currentId;
},
set: function (isSelected) {
// TODO: find a way to work with index as more than one color can have the same id, also, can there be no selected color when removing selection?
if (isSelected) {
var _id = this.id;
PaletteManager.setCurrentPaletteById(_id);
}
}
})
/**
* The oColor objects contained in the palette.
* @name $.oPalette#colors
* @type {oColor[]}
*/
Object.defineProperty($.oPalette.prototype, 'colors', {
get: function () {
var _palette = this.paletteObject
var _colors = []
for (var i = 0; i < _palette.nColors; i++) {
_colors.push(new this.$.oColor(this, i))
}
return _colors
}
})
/**
* The color currently active in the palette view. 'null' if no color is currently selected
* @name $.oPalette#currentColor
* @type {oColor}
*/
Object.defineProperty($.oPalette.prototype, 'currentColor', {
get: function () {
var id = PaletteManager.getCurrentColorId()
return this.getColorById(id)
},
set: function (newColor) {
var id = newColor.id
if (!this.getColorById(id)) return
PaletteManager.setCurrentColorById(id)
}
})
// $.oPalette Class methods
/**
* Adds a solid color to the palette
* @param {string} name the display name for the newly created color
* @param {$.oColorValue} colorValue a $.oColorValue object describing the color
*/
$.oPalette.prototype.addColor = function (name, colorValue) {
var colorData = {r : colorValue.r, g: colorValue.g, b: colorValue.b, a : colorValue.a };
this.paletteObject.createNewSolidColor(name, colorData);
return this.colors.slice(-1)[0];
}
/**
* Adds a texture swatch to the palette
* @param {string} name
* @param {string} texturePath
* @param {bool} tiled Wether the texture will be tiled or not
*/
$.oPalette.prototype.addTexture = function (name, texturePath, tiled) {
if (typeof texturePath === this.$.oFile) texturePath = texturePath.path;
this.paletteObject.createNewTexture(name, texturePath, tiled);
return this.colors.slice(-1)[0];
}
/**
* Adds a gradient to the palette, using the passed values
* @param {string} name
* @param {object} colorValues an object with keys between 0 and 1 containing a colorValue for each "tack". ex: {0: new $.oColorValue("000000ff"), 1:new $.oColorValue("ffffffff")}
* @param {bool} radial
*/
$.oPalette.prototype.addGradient = function (name, colorValues, radial) {
if (typeof radial === 'undefined') var radial = false;
var types = PaletteObjectManager.Constants.ColorType;
var type = radial?types.RADIAL_GRADIENT:types.LINEAR_GRADIENT;
var gradient = []
for (var i in colorValues){
var color = colorValues[i];
var tack = {t:parseFloat(i, 10), r:color.r, g:color.g, b:color.b, a:color.a};
gradient.push[tack];
}
this.paletteObject.createNewColor(type, name, gradient);
return this.colors.slice(-1)[0];
}
/**
* Gets a oColor object based on id. 'null' if the color is not found in this palette
* @param {string} id the color id as found in toonboom palette file.
*
* @return: {oColor} the found oColor object.
*/
$.oPalette.prototype.getColorById = function (id) {
var _colors = this.colors;
for (var i in _colors){
if (_colors[i].id == id) return _colors[i];
}
return null;
}
/**
* Gets a oColor object based on name. Warning: more than one color can have the same name in a palette, the first one found will be returned.
* @param {string} name the color name for the color in the palette.
*
* @return: {oColor} the found oColor object.
*/
$.oPalette.prototype.getColorByName = function (name) {
var _colors = this.colors;
var _names = _colors.map(function (x) { return x.name })
var _colorIndex = _names.indexOf(name)
if (_colorIndex != -1) return _colors[_colorIndex]
return null;
}
/**
* Removes the palette file from the filesystem and palette list.
* @param {bool} removeFile Whether the palette file should be removed on the filesystem.
*
* @return: {bool} The success-result of the removal.
*/
$.oPalette.prototype.remove = function (removeFile) {
if (typeof removeFile === 'undefined') var removeFile = false;
var success = false;
if (removeFile) {
try {
if (this.$.batchMode) {
this.path.remove();
success = this._paletteList.removePaletteById(this.id);
} else {
success = PaletteObjectManager.removePaletteReferencesAndDeleteOnDisk(this.id)
}
} catch (err) {
success = false;
}
} else {
success = this._paletteList.removePaletteById(this.id);
}
//Todo: should actually check for its removal.
return success;
}
$.oPalette.prototype.toString = function(){
return this.path.path || this.name;
}

View file

@ -49,14 +49,14 @@
/**
* The base class for the oList.
* The constructor for the $.oPathPoint class.
* @constructor
* @classdesc oList Base Class
* @param {oColumn} oColumnObject The array that this oList represents.
* @param {oFrame} oFrameObject The index at which this list starts.
* @classdesc The $.oPathPoint Class represents a point on a column of type 3DPath. This class is used to access information about the curve that this point belongs to.
* @param {oColumn} oColumnObject The 3DPath column that contains this information
* @param {oFrame} oFrameObject The frame on which the point is placed
*
* @property {oColumn} column The indexed object of the item.
* @property {oFrame} frame The indexed object of the item.
* @property {oColumn} column The column this point belongs to
* @property {oFrame} frame The frame on which the point is placed.
*/
$.oPathPoint = function(oColumnObject, oFrameObject){
this.column = oColumnObject;
@ -86,7 +86,7 @@ Object.defineProperty($.oPathPoint.prototype, 'x', {
var _column = this.column.uniqueName;
var _index = this.pointIndex;
var _x = func.pointXPath3d(_column, _index);
return _x;
},

View file

@ -64,7 +64,7 @@
$.oPreferences = function( ){
this._type = "preferences";
this._addedPreferences = []
this.refresh();
}
@ -83,22 +83,22 @@ $.oPreferences.prototype.refresh = function(){
this.$.debug( "Unable to find preference file: " + fl, this.$.DEBUG_LEVEL.ERROR );
return;
}
var xmlDom = new QDomDocument();
xmlDom.setContent( nfl.read() );
if( !xmlDom ){
return;
}
var prefXML = xmlDom.elementsByTagName( "preferences" );
if( prefXML.length() == 0 ){
this.$.debug( "Unable to find preferences in file: " + fl, this.$.DEBUG_LEVEL.ERROR );
return;
}
var XMLpreferences = prefXML.at(0);
//Clear this objects previous getter/setters to make room for new ones.
if( this._preferences ){
for( n in this._preferences ){ //Remove them if they've disappeared.
@ -111,16 +111,16 @@ $.oPreferences.prototype.refresh = function(){
}
}
this._preferences = {};
if( !XMLpreferences.hasChildNodes() ){
this.$.debug( "Unable to find preferences in file: " + fl, this.$.DEBUG_LEVEL.ERROR );
return;
}
//THE DEFAULT SETTER
var set_val = function( pref, name, val ){
var prefObj = pref._preferences[name];
//Check against types, unable to set types differently.
switch( typeof val ){
case 'string':
@ -156,13 +156,13 @@ $.oPreferences.prototype.refresh = function(){
if( prefObj["type"] != "color" ){
throw ReferenceError( "Harmony does not support preference type-changes. Preference must remain " + prefObj["type"] );
}
value = preferences.setColor( name, new ColorRGBA( val.r, val.g, val.b, val.a ) );
set = true;
}
}catch(err){
}
if(!set){
if( prefObj["type"] != "string" ){
throw ReferenceError( "Harmony does not support preference type-changes. Preference must remain " + prefObj["type"] );
@ -172,18 +172,18 @@ $.oPreferences.prototype.refresh = function(){
}
break
}
{
pref._preferences[name].value = val;
}
}
//THE DEFAULT GETTER
var get_val = function( pref, name ){
return pref._preferences[name].value;
}
var getterSetter_create = function( targ, id, type ){
switch( type ){
case 'color':
@ -199,7 +199,7 @@ $.oPreferences.prototype.refresh = function(){
case 'bool':
value = preferences.getBool( id, false );
break
case 'string':
case 'string':
value = preferences.getString( id, "unknown" );
if( value.slice( 0, 5 ) == "json(" ){
var obj = value.slice( 5, value.length-1 );
@ -210,9 +210,9 @@ $.oPreferences.prototype.refresh = function(){
break;
}
if( value === null ) return;
targ._preferences[ id ] = { "value": value, "type":type };
//Create a getter/setter for it!
Object.defineProperty( targ, id, {
enumerable : true,
@ -221,8 +221,8 @@ $.oPreferences.prototype.refresh = function(){
get : eval( 'val = function(){ return get_val( targ, "'+id+'"); }' )
});
}
//Get all the children preferences.
var childNodes = XMLpreferences.childNodes();
for( var cn=0;cn<childNodes.length();cn++ ){
@ -232,9 +232,9 @@ $.oPreferences.prototype.refresh = function(){
var e = thisChild.toElement();
var type = e.tagName();
var id = e.attribute( "id", "null" );
if( id == "null" ){ continue; }
var value = null;
getterSetter_create( this, id, type );
@ -244,15 +244,15 @@ $.oPreferences.prototype.refresh = function(){
System.println( err );
}
}
for( var n=0;n<this._addedPreferences.length;n++ ){
var pref = this._addedPreferences[n];
var id = pref["name"];
var type = pref["type"];
getterSetter_create( this, id, type );
}
}
@ -266,7 +266,7 @@ $.oPreferences.prototype.create = function( name, val ){
if( this[ name ] ){
throw ReferenceError( "Preference already exists by name: " + name );
}
var type = '';
//Check against types, unable to set types differently.
switch( typeof val ){
@ -289,13 +289,13 @@ $.oPreferences.prototype.create = function( name, val ){
var set = false;
try{
if( val.r && val.g && val.b && val.a ){
type = 'color';
type = 'color';
value = preferences.setColor( name, new ColorRGBA( val.r, val.g, val.b, val.a ) );
set = true;
}
}catch(err){
}
if(!set){
type = 'string';
var json_val = 'json('+JSON.stringify( val )+')';
@ -303,8 +303,8 @@ $.oPreferences.prototype.create = function( name, val ){
}
break
}
this._addedPreferences.push( {"type":type, "name":name } );
this._addedPreferences.push( {"type":type, "name":name } );
this.refresh();
}
@ -319,9 +319,9 @@ $.oPreferences.prototype.create = function( name, val ){
* //This new preference won't be available in the file until Harmony closes.
* //So if preferences are reinstantiated, it won't be readily available -- but it can still be retrieved with get.
*
* var pref2 = $.getPreferences();
* var pref2 = $.getPreferences();
* pref["MyNewPreferenceName"]; // Provides: undefined -- its not in the Harmony preference file.
* pref.get("MyNewPreferenceName"); // Provides: MyPreferenceValue, its still available
* pref.get("MyNewPreferenceName"); // Provides: MyPreferenceValue, its still available
*/
$.oPreferences.prototype.get = function( name ){
if( this[name] ){
@ -331,55 +331,41 @@ $.oPreferences.prototype.get = function( name ){
var testTime = (new Date()).getTime();
var doubleExist = preferences.getDouble( name, testTime );
if( doubleExist!= testTime ){
this._addedPreferences.push( {"type":'double', "name":name } );
this._addedPreferences.push( {"type":'double', "name":name } );
this.refresh();
return doubleExist;
}
var intExist = preferences.getInt( name, testTime );
if( intExist!= testTime ){
this._addedPreferences.push( {"type":'int', "name":name } );
this._addedPreferences.push( {"type":'int', "name":name } );
this.refresh();
return intExist;
}
var colorExist = preferences.getColor( name, new ColorRGBA(1,2,3,4) );
if( !( (colorExist.r==1) && (colorExist.g==2) && (colorExist.b==3) && (colorExist.a==4) ) ){
this._addedPreferences.push( {"type":'color', "name":name } );
this._addedPreferences.push( {"type":'color', "name":name } );
this.refresh();
return colorExist;
}
var stringExist = preferences.getString( name, "doesntExist" );
if( stringExist != "doesntExist" ){
this._addedPreferences.push( {"type":'color', "name":name } );
this._addedPreferences.push( {"type":'color', "name":name } );
this.refresh();
return this[name];
}
return preferences.getBool( name, false );
}
//////////////////////////////////////
//////////////////////////////////////
// //
// //
// $.oPreference class //
// //
// //
//////////////////////////////////////
//////////////////////////////////////
//////////////////////////////////////
//////////////////////////////////////
// //
@ -393,7 +379,7 @@ $.oPreferences.prototype.get = function( name ){
/**
* The constructor for the oPreference Class.
* @classdesc
* The oPreference class wraps a single preference item.
* The oPreference class wraps a single preference item.
* @constructor
* @param {string} category The category of the preference
* @param {string} keyword The keyword used by the preference
@ -403,24 +389,24 @@ $.oPreferences.prototype.get = function( name ){
* @example
* // To access the preferences of Harmony, grab the preference object in the $.oApp class:
* var prefs = $.app.preferences;
*
*
* // It's then possible to access all available preferences of the software:
* for (var i in prefs){
* log (i+" "+prefs[i]);
* }
*
*
* // accessing the preference value can be done directly by using the dot notation:
* prefs.USE_OVERLAY_UNDERLAY_ART = true;
* log (prefs.USE_OVERLAY_UNDERLAY_ART);
*
*
* //the details objects of the preferences object allows access to more information about each preference
* var details = prefs.details
* log(details.USE_OVERLAY_UNDERLAY_ART.category+" "+details.USE_OVERLAY_UNDERLAY_ART.id+" "+details.USE_OVERLAY_UNDERLAY_ART.type);
*
*
* for (var i in details){
* log(i+" "+JSON.stringify(details[i])) // each object inside detail is a complete oPreference instance
* }
*
*
* // the preference object also holds a categories array with the list of all categories
* log (prefs.categories)
*/
@ -459,9 +445,9 @@ Object.defineProperty ($.oPreference.prototype, 'value', {
var _value = preferences.getString(this.keyword, this.defaultValue);
}
}catch(err){
this.$.debug(err, this.$.DEBUG_LEVEL.ERROR)
this.$.debug(err, this.$.DEBUG_LEVEL.ERROR)
}
this.$.debug("Getting value of Preference "+this.keyword+" : "+_value, this.$.DEBUG_LEVEL.ERROR)
this.$.debug("Getting value of Preference "+this.keyword+" : "+_value, this.$.DEBUG_LEVEL.LOG)
return _value;
},
@ -483,7 +469,7 @@ Object.defineProperty ($.oPreference.prototype, 'value', {
default:
preferences.setString(this.keyword, newValue);
}
this.$.debug("Preference "+this.keyword+" was set to : "+newValue, this.$.DEBUG_LEVEL.ERROR)
this.$.debug("Preference "+this.keyword+" was set to : "+newValue, this.$.DEBUG_LEVEL.LOG)
}
})

View file

@ -123,6 +123,24 @@ Object.defineProperty($.oScene.prototype, 'paletteFolder', {
}
});
/**
* The temporary folder where files are created before being saved.
* If the folder doesn't exist yet, it will be created.
* @name $.oScene#tempFolder
* @type {$.oFolder}
* @readonly
*/
Object.defineProperty($.oScene.prototype, 'tempFolder', {
get : function(){
if (!this.hasOwnProperty("_tempFolder")){
this._tempFolder = new this.$.oFolder(scene.tempProjectPathRemapped());
if (!this._tempFolder.exists) this._tempFolder.create()
}
return this._tempFolder;
}
});
/**
* The name of the scene.
* @name $.oScene#name
@ -144,7 +162,7 @@ Object.defineProperty($.oScene.prototype, 'name', {
*/
Object.defineProperty($.oScene.prototype, 'online', {
get : function(){
return scene.currentJob() != "Digital";
return about.isDatabaseMode()
}
});
@ -157,7 +175,7 @@ Object.defineProperty($.oScene.prototype, 'online', {
Object.defineProperty($.oScene.prototype, 'environnement', {
get : function(){
if (!this.online) return null;
return scene.currentScene();
return scene.currentEnvironment();
}
});
@ -248,7 +266,19 @@ Object.defineProperty($.oScene.prototype, 'framerate', {
/**
* The horizontal aspect ratio.
* The Field unit aspect ratio as a coefficient (width/height).
* @name $.oScene#unitsAspectRatio
* @type {double}
*/
Object.defineProperty($.oScene.prototype, 'unitsAspectRatio', {
get : function(){
return this.aspectRatioX/this.aspectRatioY;
}
});
/**
* The horizontal aspect ratio of Field units.
* @name $.oScene#aspectRatioX
* @type {double}
*/
@ -262,7 +292,7 @@ Object.defineProperty($.oScene.prototype, 'aspectRatioX', {
});
/**
* The vertical aspect ratio.
* The vertical aspect ratio of Field units.
* @name $.oScene#aspectRatioY
* @type {double}
*/
@ -276,7 +306,7 @@ Object.defineProperty($.oScene.prototype, 'aspectRatioY', {
});
/**
* The horizontal unit count.
* The horizontal Field unit count.
* @name $.oScene#unitsX
* @type {double}
*/
@ -290,7 +320,7 @@ Object.defineProperty($.oScene.prototype, 'unitsX', {
});
/**
* The vertical unit count.
* The vertical Field unit count.
* @name $.oScene#unitsY
* @type {double}
*/
@ -304,7 +334,7 @@ Object.defineProperty($.oScene.prototype, 'unitsY', {
});
/**
* The depth unit count.
* The depth Field unit count.
* @name $.oScene#unitsZ
* @type {double}
*/
@ -333,9 +363,38 @@ Object.defineProperty($.oScene.prototype, 'center', {
});
/**
* The amount of drawing units represented by 1 field on the horizontal axis.
* @name $.oScene#fieldVectorResolutionX
* @type {double}
* @readonly
*/
Object.defineProperty($.oScene.prototype, 'fieldVectorResolutionX', {
get : function(){
var yUnit = this.fieldVectorResolutionY;
var unit = yUnit * this.unitsAspectRatio;
return unit
}
});
/**
* The horizontal resolution.
* The amount of drawing units represented by 1 field on the vertical axis.
* @name $.oScene#fieldVectorResolutionY
* @type {double}
* @readonly
*/
Object.defineProperty($.oScene.prototype, 'fieldVectorResolutionY', {
get : function(){
var verticalResolution = 1875 // the amount of drawing units for the max vertical field value
var unit = verticalResolution/12; // the vertical number of units on drawings is always 12 regardless of $.scn.unitsY
return unit
}
});
/**
* The horizontal resolution in pixels (for rendering).
* @name $.oScene#resolutionX
* @readonly
* @type {int}
@ -347,7 +406,7 @@ Object.defineProperty($.oScene.prototype, 'resolutionX', {
});
/**
* The vertical resolution.
* The vertical resolution in pixels (for rendering).
* @name $.oScene#resolutionY
* @type {int}
*/
@ -358,7 +417,7 @@ Object.defineProperty($.oScene.prototype, 'resolutionY', {
});
/**
* The default horizontal resolution.
* The default horizontal resolution in pixels.
* @name $.oScene#defaultResolutionX
* @type {int}
*/
@ -372,7 +431,7 @@ Object.defineProperty($.oScene.prototype, 'defaultResolutionX', {
});
/**
* The default vertical resolution.
* The default vertical resolution in pixels.
* @name $.oScene#defaultResolutionY
* @type {int}
*/
@ -429,12 +488,11 @@ Object.defineProperty($.oScene.prototype, 'unsaved', {
});
/**
* The root group of the scene.
* @name $.oScene#root
* @readonly
* @type {$.oGroupNode}
* @readonly
*/
Object.defineProperty($.oScene.prototype, 'root', {
get : function(){
@ -444,9 +502,6 @@ Object.defineProperty($.oScene.prototype, 'root', {
});
/**
* Contains the list of all the nodes present in the scene.
* @name $.oScene#nodes
@ -460,6 +515,7 @@ Object.defineProperty($.oScene.prototype, 'nodes', {
}
});
/**
* Contains the list of columns present in the scene.
* @name $.oScene#columns
@ -569,7 +625,7 @@ Object.defineProperty($.oScene.prototype, 'currentFrame', {
/**
* Retrieve and change the selected frames. This is an array with the start frame and the end frame (non included)
* Retrieve and change the selection of nodes.
* @name $.oScene#selectedNodes
* @type {$.oNode[]}
*/
@ -580,7 +636,9 @@ Object.defineProperty($.oScene.prototype, 'selectedNodes', {
set : function(nodesToSelect){
selection.clearSelection ();
selection.addNodesToSelection(nodesToSelect.map(function(x){return x.path}));
for (var i in nodesToSelect){
selection.addNodeToSelection(nodesToSelect[i].path);
};
}
});
@ -592,7 +650,12 @@ Object.defineProperty($.oScene.prototype, 'selectedNodes', {
*/
Object.defineProperty($.oScene.prototype, 'selectedFrames', {
get : function(){
var _selectedFrames = [selection.startFrame(), selection.startFrame()+selection.numberOfFrames()];
if (selection.isSelectionRange()){
var _selectedFrames = [selection.startFrame(), selection.startFrame()+selection.numberOfFrames()];
}else{
var _selectedFrames = [this.currentFrame, this.currentFrame+1];
}
return _selectedFrames;
},
@ -604,7 +667,8 @@ Object.defineProperty($.oScene.prototype, 'selectedFrames', {
/**
* Retrieve and set the selected palette from the scene palette list.
* @type {$.oPalette} oPalette with provided name.
* @name $.oScene#selectedPalette
* @type {$.oPalette}
*/
Object.defineProperty($.oScene.prototype, "selectedPalette", {
get: function(){
@ -623,16 +687,61 @@ Object.defineProperty($.oScene.prototype, "selectedPalette", {
/**
* The current drawing of the scene.
* The selected strokes on the currently active Drawing
* @name $.oScene#selectedShapes
* @type {$.oStroke[]}
*/
Object.defineProperty($.oScene.prototype, "selectedShapes", {
get : function(){
var _currentDrawing = this.activeDrawing;
var _shapes = _currentDrawing.selectedShapes;
return _shapes;
}
})
/**
* The selected strokes on the currently active Drawing
* @name $.oScene#selectedStrokes
* @type {$.oStroke[]}
*/
Object.defineProperty($.oScene.prototype, "selectedStrokes", {
get : function(){
var _currentDrawing = this.activeDrawing;
var _strokes = _currentDrawing.selectedStrokes;
return _strokes;
}
})
/**
* The selected strokes on the currently active Drawing
* @name $.oScene#selectedContours
* @type {$.oContour[]}
*/
Object.defineProperty($.oScene.prototype, "selectedContours", {
get : function(){
var _currentDrawing = this.activeDrawing;
var _strokes = _currentDrawing.selectedContours;
return _strokes;
}
})
/**
* The currently active drawing in the harmony UI.
* @name $.oScene#activeDrawing
* @type {in$.oDrawing}
* @type {$.oDrawing}
*/
Object.defineProperty($.oScene.prototype, 'activeDrawing', {
get : function(){
var _curDrawing = Tools.getToolSettings().currentDrawing;
if (!_curDrawing) return null;
var _element = this.getElementById(_curDrawing.elementId);
var _element = this.selectedNodes[0].element;
var _drawings = _element.drawings;
for (var i in _drawings){
if (_drawings[i].id == _curDrawing.drawingId) return _drawings[i];
@ -647,6 +756,21 @@ Object.defineProperty($.oScene.prototype, 'activeDrawing', {
});
/**
* The current timeline using the default Display.
* @name $.oScene#currentTimeline
* @type {$.oTimeline}
* @readonly
*/
Object.defineProperty($.oScene.prototype, 'currentTimeline', {
get : function(){
if (!this.hasOwnProperty("_timeline")){
this._timeline = this.getTimeline();
}
return this._timeline;
}
});
@ -664,14 +788,7 @@ Object.defineProperty($.oScene.prototype, 'activeDrawing', {
*/
$.oScene.prototype.getNodeByPath = function(fullPath){
var _type = node.type(fullPath);
if (_type == "") return null; // TODO: remove this if we implement a .exists property for oNode
if( this.$.cache_oNode[fullPath] ){
//Check for consistent type.
if ( this.$.cache_oNode[fullPath].type == _type ){
return this.$.cache_oNode[fullPath];
}
}
if (_type == "") return null;
var _node;
switch(_type){
@ -681,6 +798,12 @@ $.oScene.prototype.getNodeByPath = function(fullPath){
case "PEG" :
_node = new this.$.oPegNode( fullPath, this );
break;
case "COLOR_OVERRIDE_TVG" :
_node = new this.$.oColorOverrideNode( fullPath, this );
break;
case "TransformationSwitch" :
_node = new this.$.oTransformSwitchNode( fullPath, this );
break;
case "GROUP" :
_node = new this.$.oGroupNode( fullPath, this );
break;
@ -688,10 +811,19 @@ $.oScene.prototype.getNodeByPath = function(fullPath){
_node = new this.$.oNode( fullPath, this );
}
this.$.cache_oNode[fullPath] = _node;
return _node;
}
/**
* Returns the nodes of a certain type in the entire scene.
* @param {string} typeName The name of the node.
*
* @return {$.oNode[]} The nodes found.
*/
$.oScene.prototype.getNodesByType = function(typeName){
return this.root.getNodesByType(typeName, true);
}
/**
* Gets a column by the name.
* @param {string} uniqueName The unique name of the column as a string.
@ -1089,7 +1221,7 @@ $.oScene.prototype.addNode = function( type, name, group, nodePosition ){
$.oScene.prototype.addColumn = function( type, name, oElementObject ){
// Defaults for optional parameters
if( !type ){ return; }
if (!type) throw new Error ("Must provide a type when creating a new column.");
if (typeof name === 'undefined'){
if( column.generateAnonymousName ){
@ -1109,9 +1241,10 @@ $.oScene.prototype.addColumn = function( type, name, oElementObject ){
}
this.$.debug( "CREATING THE COLUMN: " + name, this.$.DEBUG_LEVEL.LOG );
System.println( "CREATING COLUMN: "+type );
column.add(_columnName, type);
if (column.type(_columnName)!= type) throw new Error ("Couldn't create column with name '"+name+"' and type "+type)
var _column = new this.$.oColumn( _columnName );
if (type == "DRAWING" && typeof oElementObject !== 'undefined'){
@ -1120,7 +1253,6 @@ $.oScene.prototype.addColumn = function( type, name, oElementObject ){
column.setElementIdOfDrawing(_column.uniqueName, oElementObject.id);
}
//column.update();
return _column;
}
@ -1143,9 +1275,12 @@ $.oScene.prototype.addElement = function(name, imageFormat, fieldGuide, scanType
var _fileFormat = (imageFormat == "TVG")?"SCAN":imageFormat;
var _vectorFormat = (imageFormat == "TVG")?imageFormat:"None";
name = name.split(" ").join("_");
// sanitize input to graciously handle forbidden characters
name = name.replace(/[^A-Za-z\d_\-]/g, "_").replace(/ /g, "_");
var _id = element.add(name, scanType, fieldGuide, _fileFormat, _vectorFormat);
if (_id <0) throw new Error("Couldn't create an element with settings {name:'"+name+"', imageFormat:"+ imageFormat+", fieldGuide:"+fieldGuide+", scanType:"+scanType+"}")
var _element = new this.$.oElement( _id )
return _element;
@ -1162,7 +1297,7 @@ $.oScene.prototype.addElement = function(name, imageFormat, fieldGuide, scanType
* @param {object} drawingColumn The column to attach to the drawing module.
* @param {object} options The creation options, nothing available at this point.
* @return {$.oNode} The created node, or bool as false.
* @return {$.oDrawingNode} The created node.
*/
$.oScene.prototype.addDrawingNode = function( name, group, nodePosition, oElementObject, drawingColumn, options ){
var _group = (group instanceof this.$.oGroupNode)?group:this.$node(group);
@ -1185,9 +1320,9 @@ $.oScene.prototype.addDrawingNode = function( name, group, nodePosition, oElemen
* @param {$.oPoint} addComposite Whether to add a composite.
* @param {bool} addPeg Whether to add a peg.
* @param {string} group The group in which the node is added.
* @param $.{oPoint} nodePosition The position for the node to be placed in the network.
* @param {$.oPoint} nodePosition The position for the node to be placed in the network.
* @return {$.oGroupNode} The created node, or bool as false.
* @return {$.oGroupNode} The created node.
*/
$.oScene.prototype.addGroup = function( name, includeNodes, addComposite, addPeg, group, nodePosition ){
var _group = (group instanceof this.$.oGroupNode)?group:this.$node(group);
@ -1209,24 +1344,35 @@ $.oScene.prototype.addGroup = function( name, includeNodes, addComposite, addPeg
* @return {$.oTimeline} The timelne object given the display.
*/
$.oScene.prototype.getTimeline = function(display){
if (typeof display === 'undefined') var display = '';
return new this.$.oTimeline( display, this );
}
/**
* Gets a palette by the name.
* Gets a scene palette by the name.
* @param {string} name The palette name to query and find.
*
* @return {$.oPalette} The oPalette found given the query.
*/
$.oScene.prototype.getPaletteByName = function(name){
var _paletteList = PaletteObjectManager.getScenePaletteList();
for (var i=0; i<_paletteList.numPalettes; i++){
var _palette = _paletteList.getPaletteByIndex(i);
if (_palette.getName() == name) return new this.$.oPalette(_palette, _paletteList);
}
return null;
var _palettes = this.palettes;
for (var i in _palettes){
if (_palettes[i].name == name) return _palettes[i];
}
return null;
}
/**
* Gets a scene palette by the path of the plt file.
* @param {string} path The palette path to find.
* @return {$.oPalette} The oPalette or null if not found.
*/
$.oScene.prototype.getPaletteByPath = function(path){
var _palettes = this.palettes;
for (var i in _palettes){
if (_palettes[i].path.path == path) return _palettes[i];
}
return null;
}
@ -1281,15 +1427,22 @@ $.oScene.prototype.addPalette = function(name, insertAtIndex, paletteStorage, st
/**
* Imports a palette to the scene palette list and into the specified storage location.
* @param {string} path The palette file to import.
* @param {string} name The name for the palette.
* @param {string} index Index at which to insert the palette.
* @param {string} paletteStorage Storage type: environment, job, scene, element, external.
* @param {$.oElement} storeInElement The element to store the palette in. If paletteStorage is set to "external", provide a destination folder for the palette here.
* @param {string} path The palette file to import.
* @param {string} name The name for the palette.
* @param {string} index Index at which to insert the palette.
* @param {string} paletteStorage Storage type: environment, job, scene, element, external.
* @param {$.oElement} storeInElement The element to store the palette in. If paletteStorage is set to "external", provide a destination folder for the palette here.
*
* @return {$.oPalette} oPalette with provided name.
*/
$.oScene.prototype.importPalette = function(filename, name, index, paletteStorage, storeInElement){
var _paletteFile = new this.$.oFile(filename);
if (!_paletteFile.exists){
throw new Error ("Cannot import palette from file "+filename+" because it doesn't exist", this.$.DEBUG_LEVEL.ERROR);
}
if (typeof name === 'undefined') var name = _paletteFile.name;
var _list = PaletteObjectManager.getScenePaletteList();
if (typeof index === 'undefined') var index = _list.numPalettes;
if (typeof paletteStorage === 'undefined') var paletteStorage = "scene";
@ -1300,14 +1453,6 @@ $.oScene.prototype.importPalette = function(filename, name, index, paletteStorag
var storeInElement = 1;
}
var _paletteFile = new this.$.oFile(filename);
if (typeof name === 'undefined') var name = _paletteFile.name;
if (!_paletteFile.exists){
this.$.debug("Error: cannot import palette from file "+filename+" because it doesn't exist", this.$.DEBUG_LEVEL.ERROR);
return null;
}
var _location = this.$.oPalette.location;
switch (paletteStorage){
case 'environment' :
@ -1329,19 +1474,19 @@ $.oScene.prototype.importPalette = function(filename, name, index, paletteStorag
var paletteFolder = new this.$.oFolder(paletteFolder);
if (!paletteFolder.exists && !paletteFolder.create()) {
this.$.debug("Error: couldn't create missing palette folder "+paletteFolder, this.$.DEBUG_LEVEL.ERROR);
return null;
if (!paletteFolder.exists){
try{
paletteFolder.create();
}catch(err){
throw new Error ("Couldn't create missing palette folder " + paletteFolder +": " + err);
}
}
var _copy = _paletteFile.copy(paletteFolder.path, name, true);
if (!_copy) {
this.$.debug("Error: couldn't copy palette "+filename, this.$.DEBUG_LEVEL.ERROR);
return null;
if (_paletteFile.folder.path != paletteFolder.path) {
_paletteFile = _paletteFile.copy(paletteFolder.path, name, true);
}
var _palette = _list.insertPalette(_copy.toonboomPath.replace(".plt", ""), index);
var _palette = _list.insertPalette(_paletteFile.toonboomPath.replace(".plt", ""), index);
_newPalette = new this.$.oPalette(_palette, _list);
@ -1362,29 +1507,14 @@ $.oScene.prototype.createPaletteFromNodes = function(nodes, paletteName, colorNa
if (typeof colorName ==='undefined') var colorName = false;
// get unique Color Ids
var _usedColorIds = [];
var _usedColors = {};
for (var i in nodes){
var _ids = nodes[i].usedColorIds;
for (var j in _ids){
if (_usedColorIds.indexOf(_ids[j]) == -1) _usedColorIds.push(_ids[j]);
}
}
// find used Palettes and Colors
// find RGB values
var _palettes = this.palettes;
var _usedColors = new Array(_usedColorIds.length);
for (var i in _usedColorIds){
for (var j in _palettes){
var _color = _palettes[j].getColorById(_usedColorIds[i]);
// color found
if (_color != null){
_usedColors[i] = _color;
break;
}
_colors = nodes[i].usedColors;
for (var j in _colors){
_usedColors[_colors[j].id] = _colors[j];
}
}
_usedColors = Object.keys(_usedColors).map(function(x){return _usedColors[x]});
// create single palette
var _newPalette = this.addPalette(paletteName);
@ -1535,11 +1665,11 @@ $.oScene.prototype.mergeNodes = function (nodes, resultName, deleteMerged){
/**
* export a template from the specified nodes.
* @param {$.oNodes[]} nodes The path of the TPL file to import.
* @param {bool} [exportPath] Whether to extend the exposures of the content imported.
* @param {string} [exportPalettesMode] can have the values : "usedOnly", "all", "createPalette"
* @param {string} [renameUsedColors] if creating a palette, optionally set here the name for the colors (they will have a number added to each)
* @param {copyOptions} [copyOptions] An object containing paste options as per Harmony's standard paste options.
* @param {$.oNodes[]} nodes The list of nodes included in the template.
* @param {bool} [exportPath] The path of the TPL file to export.
* @param {string} [exportPalettesMode='usedOnly'] can have the values : "usedOnly", "all", "createPalette"
* @param {string} [renameUsedColors=] if creating a palette, optionally set here the name for the colors (they will have a number added to each)
* @param {copyOptions} [copyOptions] An object containing paste options as per Harmony's standard paste options.
*
* @return {bool} The success of the export.
* @todo turn exportPalettesMode into an enum?
@ -1613,10 +1743,7 @@ $.oScene.prototype.exportTemplate = function(nodes, exportPath, exportPalettesMo
this.$.debug("exporting selection :"+this.selectedFrames+"\n\n"+this.selectedNodes.join("\n")+"\n\n to folder : "+_folder+"/"+_name, this.$.DEBUG_LEVEL.LOG)
// this.$.alert ("exporting now selection :"+this.selectedFrames+"\n\n"+this.selectedNodes.join("\n")+"\n\n to folder : "+_folder+"/"+_name)
try{
// THIS CRASHES OCCASIONALLY AND I DONT KNOW WHY :(
var success = copyPaste.createTemplateFromSelection (_name, _folder);
if (success == "") throw new Error("export failed")
}catch(error){
@ -1625,7 +1752,6 @@ $.oScene.prototype.exportTemplate = function(nodes, exportPath, exportPalettesMo
}
this.$.debug("export of template "+_name+" finished, cleaning palettes", this.$.DEBUG_LEVEL.LOG);
// this.$.alert ("export done - cleaning palettes")
if (_readNodes.length > 0 && exportPalettesMode != "all"){
// deleting the extra palettes from the exported template
@ -1667,8 +1793,6 @@ $.oScene.prototype.exportTemplate = function(nodes, exportPath, exportPalettesMo
}
}
// alert ("cleaned palettes")
selection.clearSelection();
return true;
}
@ -1848,6 +1972,8 @@ $.oScene.prototype.importPSD = function( path, group, nodePosition, separateLaye
* @deprecated
* @param {string} path The PSD file to update.
* @param {bool} [separateLayers] Whether the PSD was imported as separate layers.
*
* @returns {$.oNode[]} The nodes affected by the update
*/
$.oScene.prototype.updatePSD = function( path, group, separateLayers ){
if (typeof group === 'undefined') var group = this.root;
@ -1868,7 +1994,7 @@ $.oScene.prototype.updatePSD = function( path, group, separateLayers ){
* @param {string} path The sound file to import.
* @param {string} layerName The name to give the layer created.
*
* @return {$.oNode} The imported Quicktime Node.
* @return {$.oNode} The imported sound column.
*/
$.oScene.prototype.importSound = function(path, layerName){
var _audioFile = new this.$.oFile(path);
@ -1890,7 +2016,7 @@ $.oScene.prototype.updatePSD = function( path, group, separateLayers ){
* @param {bool} exportSound Whether to include the sound in the export.
* @param {bool} exportPreviewArea Whether to only export the preview area of the timeline.
*
* @return {$.oNode} The imported Quicktime Node.
* @return {bool} The success of the export
*/
$.oScene.prototype.exportQT = function( path, display, scale, exportSound, exportPreviewArea){
if (typeof display === 'undefined') var display = node.getName(node.getNodes(["DISPLAY"])[0]);
@ -2003,7 +2129,7 @@ $.oScene.prototype.save = function( ){
* @param {string} newPath the new location for the scene (must be a folder path and not a .xstage)
*/
$.oScene.prototype.saveAs = function(newPath){
if (!this.online) {
if (this.online) {
this.$.debug("Can't use saveAs() in database mode.", this.$.DEBUG_LEVEL.ERROR);
return;
}
@ -2026,7 +2152,7 @@ $.oScene.prototype.saveNewVersion = function(newVersionName, markAsDefault){
/**
* Renders the write nodes of the scene
* Renders the write nodes of the scene. This action saves the scene.
* @param {bool} [renderInBackground=true] Whether to do the render on the main thread and block script execution
* @param {int} [startFrame=1] The first frame to render
* @param {int} [endFrame=oScene.length] The end of the render (non included)
@ -2034,6 +2160,7 @@ $.oScene.prototype.saveNewVersion = function(newVersionName, markAsDefault){
* @param {int} [resY] The vertical resolution of the render. Uses the scene resolution by default.
* @param {string} [preRenderScript] The path to the script to execute on the scene before doing the render
* @param {string} [postRenderScript] The path to the script to execute on the scene after the render is finished
* @return {$.oProcess} In case of using renderInBackground, will return the oProcess object doing the render
*/
$.oScene.prototype.renderWriteNodes = function(renderInBackground, startFrame, endFrame, resX, resY, preRenderScript, postRenderScript){
if (typeof renderInBackground === 'undefined') var renderInBackground = true;
@ -2073,10 +2200,14 @@ $.oScene.prototype.renderWriteNodes = function(renderInBackground, startFrame, e
this.$.log("Starting render of scene "+this.name);
if (renderInBackground){
var length = endFrame - startFrame;
var length = endFrame - startFrame + 1;
var progressDialogue = new this.$.oProgressDialog("Rendering : ",length,"Render Write Nodes", true);
var self = this;
var cancelRender = function(){
p.kill();
this.$.alert("Render was canceled.")
}
var renderProgress = function(message){
// reporting progress to log window
@ -2086,21 +2217,30 @@ $.oScene.prototype.renderWriteNodes = function(renderInBackground, startFrame, e
matches.push(match[1]);
}
if (matches.length!=0){
var progress = parseInt(matches.pop(),10)
progressDialogue.label = "Rendering Frame: "+progress+"/"+length
var progress = parseInt(matches.pop(), 10) - startFrame;
progressDialogue.label = "Rendering Frame: " + progress + "/" + length;
progressDialogue.value = progress;
var percentage = Math.round(progress/length*100);
self.$.log("render : "+percentage+"% complete");
var percentage = Math.round(progress/length * 100);
this.$.log("render : " + percentage + "% complete");
}
}
var renderFinished = function(exitCode){
progressDialogue.label = "Rendering Finished"
progressDialogue.value = length;
self.$.log(exitCode+" : render finished");
if (exitCode == 0){
// render success
progressDialogue.label = "Rendering Finished"
progressDialogue.value = length;
this.$.log(exitCode + " : render finished");
}else{
this.$.log(exitCode + " : render cancelled");
}
}
p.launchAndRead(renderProgress, renderFinished);
progressDialogue.canceled.connect(this, cancelRender);
p.readyRead.connect(this, renderProgress);
p.finished.connect(this, renderFinished);
p.launchAndRead();
return p;
}else{
var readout = p.execute();
this.$.log("render finished");

View file

@ -1,7 +1,7 @@
//////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////
//
// openHarmony Library
// openHarmony Library
//
//
// Developped by Mathieu Chaptel, Chris Fourney
@ -47,7 +47,7 @@
// //
//////////////////////////////////////
//////////////////////////////////////
/**
* The base class for the $.oThread -- WIP, NOT TRULY THREADED AS THE EVENT MANAGER DOESNT ALLOW FOR THREADS YET.
@ -72,9 +72,9 @@ $.oThread = function( kernel, list, threadCount, start, timeout, reserveThread )
if (typeof threadCount === 'undefined') var threadCount = "2";
if (typeof start === 'undefined') var start = false;
if (typeof reserveThread === 'undefined') reserveThread = true;
threadCount = Math.min( threadCount, list.length );
this.list = list;
this.threadCount = threadCount;
this.threads = [];
@ -82,24 +82,24 @@ $.oThread = function( kernel, list, threadCount, start, timeout, reserveThread )
this.results_thread = [];
this.error_thread = [];
this.complete_thread = [];
this.started = false;
this.startAtInstantiation = start;
this.threads_available = false;
this.reserveThread = reserveThread;
this.reservedThread = false;
this.timeout = 1000.0 * 60.0;
if ( timeout ) this.timeout = timeout;
//Instantiate the results.
for( var n=0;n<list.length;n++ ){
this.results_thread.push( false );
this.complete_thread.push( false );
this.error_thread.push( false );
}
var context = {
"kernel" : kernel,
"list" : list,
@ -107,7 +107,7 @@ $.oThread = function( kernel, list, threadCount, start, timeout, reserveThread )
"complete" : this.complete_thread,
"error" : this.error_thread
};
this.kernel = function( thread, from, to ){
var local_context = context;
for( var n=from;n<to;n++ ){
@ -129,7 +129,7 @@ $.oThread = function( kernel, list, threadCount, start, timeout, reserveThread )
/**
* The completion state of all the threads.
* @name $.oTimeline#complete
* @name $.oThread#complete
* @type {bool}
*/
Object.defineProperty($.oThread.prototype, 'complete', {
@ -138,20 +138,20 @@ Object.defineProperty($.oThread.prototype, 'complete', {
System.println( "Not yet started" );
return false;
}
for( var n=0;n<this.complete_thread.length;n++ ){
if( !this.complete_thread[n] ){
return false;
}
}
return true;
}
});
/**
* The indices that have completed results.
* @name $.oTimeline#completedIndices
* @name $.oThread#completedIndices
* @type {int[]}
*/
Object.defineProperty($.oThread.prototype, 'completedIndices', {
@ -162,14 +162,14 @@ Object.defineProperty($.oThread.prototype, 'completedIndices', {
indices.push( n );
}
}
return indices;
}
});
/**
* The errors, if any, in form { "index" : int, "error" : string }
* @name $.oTimeline#errorsWithIndex
* @name $.oThread#errorsWithIndex
* @type {object[]}
*/
Object.defineProperty($.oThread.prototype, 'errorsWithIndex', {
@ -180,14 +180,14 @@ Object.defineProperty($.oThread.prototype, 'errorsWithIndex', {
errors.push( { "index" : n, "error":this.error_thread[n] } );
}
}
return errors;
}
});
/**
* The results, if any, in form { "index" : int, "results" : object }
* @name $.oTimeline#resultsWithIndex
* @name $.oThread#resultsWithIndex
* @type {object[]}
*/
Object.defineProperty($.oThread.prototype, 'resultsWithIndex', {
@ -198,14 +198,14 @@ Object.defineProperty($.oThread.prototype, 'resultsWithIndex', {
results.push( { "index" : n, "results":this.results_thread[n] } );
}
}
return results;
}
});
/**
* The errors, matching index of input list.
* @name $.oTimeline#errors
* @name $.oThread#errors
* @type {string[]}
*/
Object.defineProperty($.oThread.prototype, 'errors', {
@ -216,7 +216,7 @@ Object.defineProperty($.oThread.prototype, 'errors', {
/**
* The errors, matching index of input list.
* @name $.oTimeline#results
* @name $.oThread#results
* @type {object[]}
*/
Object.defineProperty($.oThread.prototype, 'results', {
@ -232,13 +232,13 @@ Object.defineProperty($.oThread.prototype, 'results', {
*/
$.oThread.prototype.start = function( block ){
if (typeof block === 'undefined') block = true;
if( !this.threads_available ){
if( !this.prepareThreads() ){
return;
}
}
for( var n=0;n<this.threads.length;n++ ){
// System.println( "THREAD STARTING: " + n );
if( this.started_thread[ n ] ){
@ -247,12 +247,12 @@ $.oThread.prototype.start = function( block ){
this.threads[n].start( 0 );
QCoreApplication.processEvents();
this.started_thread[ n ] = true;
}
this.started = true;
if( block ){
this.wait();
}
@ -265,27 +265,27 @@ $.oThread.prototype.start = function( block ){
*/
$.oThread.prototype.prepareThreads = function( start ){
if (start) this.startAtInstantiation = start;
if( this.threads_available ){
return false;
}
try{
for( var thread_num=0;thread_num<this.threadCount;thread_num++ ){
this.started_thread.push( false );
var from_val = Math.floor( ( thread_num / this.threadCount ) * this.list.length );
var to_val = Math.floor( ( (thread_num+1) / this.threadCount ) * this.list.length );
if( this.reserveThread && thread_num == this.threadCount-1 ){
this.reservedThread = eval( 'kernel = function(){ this.kernel('+thread_num+', '+from_val+','+to_val+') }' );
continue;
}
this.threads.push( new QTimer() );
this.threads[thread_num].singleShot = true;
this.threads[thread_num].singleShot = true;
this.threads[thread_num]["timeout"].connect( this, eval( 'kernel = function(){ this.kernel('+thread_num+', '+from_val+','+to_val+') }' ) );
if( this.startAtInstantiation ){
this.threads[thread_num].start(0);
QCoreApplication.processEvents();
@ -296,8 +296,8 @@ $.oThread.prototype.prepareThreads = function( start ){
}catch(err){
System.println( err + " (" +err.lineNumber+ " " + err.fileName + ")" );
}
this.threads_available = true;
this.threads_available = true;
return true;
}
@ -308,18 +308,18 @@ $.oThread.prototype.prepareThreads = function( start ){
*/
$.oThread.prototype.wait = function( block_time ){
if ( block_time ) this.timeout = block_time;
if( this.reserveThread && this.reservedThread ){
this.reservedThread();
}
if( !this.started ){
return;
}
var start_time = (new Date()).getTime();
var curr_time = (new Date()).getTime();
var completed = false;
while( (curr_time - start_time) < this.timeout ){
QCoreApplication.processEvents();
@ -356,21 +356,27 @@ $.oThread.prototype.runSingleThreaded = function( ){
/**
* The constructor for $.oProcess.
* @name $.oProcess
* @classdesc
* @classdesc
* Process class that allows user to launch executables outside harmony and get feedback from them.
* @constructor
* @param {string} bin The path to the binary executable that will be launched.
* @param {string[]} queryArgs A string array of the different arguments given to the command.
*
*
* @property {$.oSignal} readyRead A $.oSignal that can be connected to a callback, emitted every time new messages are outputted by the oProcess. Signature: readyRead(stdout (string))
* @property {$.oSignal} finished A $.oSignal that can be connected to a callback, emitted when the oProcess has finished. Signature: finished(returnCode(int), stdout(string))
* @property {QProcess} process the QProcess object wrapped by the $.oProcess object.
* @property {string} bin The path to the binary executable that will be launched.
* @property {string[]} queryArgs A string array of the different arguments given to the command.
* @property {string[]} queryArgs A string array of the different arguments given to the command.
* @property {string} log The full log of all the messages outputted over the course of the process lifetime.
*/
$.oProcess = function(bin, queryArgs){
this.readyRead = new this.$.oSignal()
this.finished = new this.$.oSignal()
this.bin = bin;
this.queryArgs = queryArgs;
this.process = new QProcess();
this.readChannel = "All";
this.log = "";
}
@ -403,17 +409,33 @@ Object.defineProperty($.oProcess.prototype, 'readChannel', {
/**
* Execute a process and read the result as a string.
* kills the process instantly (useful for hanging processes, etc).
*/
$.oProcess.prototype.kill = function(){
if (!this.process) return;
this.process.kill()
}
/**
* Attempts to terminate the process execution by asking it to close itself.
*/
$.oProcess.prototype.terminate = function(){
if (!this.process) return;
this.process.terminate()
}
/**
* Execute a process and read the result as a string.
* @param {function} [readCallback] User can provide a function to execute when new info can be read. This function's first argument will contain the available output from the process.
* @param {function} [finishedCallback] User can provide a function to execute when new process has finished
* @example
* // This example from the openHarmony oScene.renderWriteNodes() function code
* // This example from the openHarmony oScene.renderWriteNodes() function code
* // uses the oProcess class to launch an async process and print its progress
* // to the MessageLog.
*
*
* // declaring the binary called by the process
* var harmonyBin = specialFolders.bin+"/HarmonyPremium";
*
*
* // building the list of arguments based on user provided input
* var args = ["-batch", "-frames", startFrame, endFrame, "-res", resX, resY];
*
@ -432,17 +454,17 @@ Object.defineProperty($.oProcess.prototype, 'readChannel', {
* // Create the process with the arguments above
* var p = new this.$.oProcess(harmonyBin, args);
* p.readChannel = "All"; // specifying which channel of the process we will listen to: here we listen to both stdout and error.
*
*
* // creating an async process
* if (renderInBackground){
* var length = endFrame - startFrame;
*
* // Creating a function to respond to new readable information on the output channel.
* // This function takes a "message" argument which will contain the returned output of the process.
*
*
* var progressDialogue = new this.$.oProgressDialog("Rendering : ",length,"Render Write Nodes", true);
* var self = this;
*
*
* var renderProgress = function(message){
* // parsing the message to find a Rendered frame number.
* var progressRegex = /Rendered Frame ([0-9]+)/igm;
@ -459,10 +481,10 @@ Object.defineProperty($.oProcess.prototype, 'readChannel', {
* self.$.log("render : "+percentage+"% complete");
* }
* }
*
* // Creating a function that will trigger when process exits.
*
* // Creating a function that will trigger when process exits.
* // This function can take an "exit code" argument that will tell if the process terminated without problem.
*
*
* var renderFinished = function(exitCode){
* // here we simply output that the render completed successfully.
* progressDialogue.label = "Rendering Finished"
@ -471,51 +493,56 @@ Object.defineProperty($.oProcess.prototype, 'readChannel', {
* }
*
* // launching the process in async mode by providing true as first argument, and then the functions created above.
*
*
* p.launchAndRead(renderProgress, renderFinished);
* this.$.log("Starting render of scene "+this.name);
* }else{
*
*
* // if we don't want to use an async process and prefer to freeze the execution while waiting, we can simply call:
* var readout = p.execute();
* }
*
*
* // we return the output of the process in case we didn't use async.
* return readout
*
*
*/
$.oProcess.prototype.launchAndRead = function(readCallback, finishedCallback){
if (typeof timeOut === 'undefined') var timeOut = -1;
var bin = this.bin.split("/");
var app = bin.pop();
var directory = bin.join("\\");
var p = this.process;
p.setWorkingDirectory(directory);
this.$.debug("Executing Process with arguments : "+this.bin+" "+this.queryArgs.join(" "), this.$.DEBUG_LEVEL.LOG);
p.start(app, this.queryArgs);
// start process and attach functions to "readyRead" and "finished" signals
var self = this;
if (typeof readCallback !== 'undefined'){
var onRead = function(){
var stdout = self.read();
readCallback(stdout);
}
p.readyRead.connect(onRead);
// start process and attach functions to "readyRead" and "finished" signals
function onRead(){
var stdout = this.read();
this.readyRead.emit(stdout);
}
if (typeof finishedCallback !== 'undefined') p["finished(int)"].connect(finishedCallback);
function onFinished(returnCode){
var stdout = this.read(); // reading any extra messages issued since last read() call to add to log
this.finished.emit(returnCode, this.log);
}
p.readyRead.connect(this, onRead);
p["finished(int)"].connect(this, onFinished);
if (typeof readCallback !== 'undefined') this.readyRead.connect(readCallback);
if (typeof finishedCallback !== 'undefined') this.finished.connect(onFinished);
p.start(app, this.queryArgs);
}
/**
* read the output of a process.
* @return {string} The lines as returned by the process since the last "read" instruction
* read the output of a process.
* @return {string} The lines as returned by the process since the last "read" instruction
*/
$.oProcess.prototype.read = function (){
var p = this.process;
@ -530,12 +557,15 @@ $.oProcess.prototype.read = function (){
output = output.slice (0, -1);
}
this.log += output;
return output;
}
/**
* Execute a process and waits for the end of the execution.
* @return {string} The lines as returned by the process.
* Execute a process and waits for the end of the execution.
* @return {string} The lines as returned by the process.
*/
$.oProcess.prototype.execute = function(){
this.$.debug("Executing Process with arguments : "+this.bin+" "+this.queryArgs.join(" "), this.$.DEBUG_LEVEL.LOG);
@ -548,10 +578,107 @@ $.oProcess.prototype.execute = function(){
}
/**
* Execute a process as a separate application, which doesn't block the script execution and stops the script from interacting with it further.
*/
$.oProcess.prototype.launchAndDetach = function(){
$.oProcess.prototype.launchAndDetach = function(){
QProcess.startDetached(this.bin, this.queryArgs);
}
}
//////////////////////////////////////
//////////////////////////////////////
// //
// //
// $.oSignal class //
// //
// //
//////////////////////////////////////
//////////////////////////////////////
/**
* The constructor for $.oSignal.
* @name $.oSignal
* @classdesc
* A Qt like custom signal that can be defined, connected and emitted.
* As this signal is not actually threaded, the connected callbacks will be executed
* directly when the signal is emited, and the rest of the code will execute after.
* @constructor
*/
$.oSignal = function(type){
// this.emitType = type;
this.connexions = [];
this.blocked = false;
}
/**
* Register the calling object and the slot.
* @param {object} context
* @param {function} slot
*/
$.oSignal.prototype.connect = function (context, slot){
// support slot.connect(callback) synthax
if (typeof slot === 'undefined'){
var slot = context;
var context = null;
}
this.connexions.push ({context: context, slot:slot});
}
/**
* Remove a connection registered with this Signal.
* @param {function} [slot] the function to disconnect from the signal. If not specified, all connexions will be removed.
*/
$.oSignal.prototype.disconnect = function(slot){
if (typeof slot === "undefined"){
this.connexions = [];
return
}
for (var i in this.connexions){
if (this.connexions[i].slot == slot){
this.connexions.splice(i, 1);
}
}
}
/**
* Call the slot function using the provided context and and any arguments.
*/
$.oSignal.prototype.emit = function () {
if (this.blocked) return;
// if (!(value instanceof this.type)){ // can't make it work for primitives, might try to fix later?
// throw new error ("Signal can't emit type "+ (typeof value) + ". Must be : " + this.type)
// }
var args = [];
for (var i=0; i<arguments.length; i++){
args.push(arguments[i]);
}
this.$.debug("emiting signal with "+ args, this.$.DEBUG_LEVEL.LOG);
for (var i in this.connexions){
var context = this.connexions[i].context;
var slot = this.connexions[i].slot;
// support connecting signals to each other
if (slot instanceof this.$.oSignal){
slot.emit.apply(context, args)
}else{
slot.apply(context, args);
}
}
}
$.oSignal.prototype.toString = function(){
return "Signal";
}

View file

@ -0,0 +1,607 @@
//////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////
//
// openHarmony Library v0.01
//
//
// Developped by Mathieu Chaptel, Chris Fourney...
//
//
// This library is an open source implementation of a Document Object Model
// for Toonboom Harmony. It also implements patterns similar to JQuery
// for traversing this DOM.
//
// Its intended purpose is to simplify and streamline toonboom scripting to
// empower users and be easy on newcomers, with default parameters values,
// and by hiding the heavy lifting required by the official API.
//
// This library is provided as is and is a work in progress. As such, not every
// function has been implemented or is garanteed to work. Feel free to contribute
// improvements to its official github. If you do make sure you follow the provided
// template and naming conventions and document your new methods properly.
//
// This library doesn't overwrite any of the objects and classes of the official
// Toonboom API which must remains available.
//
// This library is made available under the MIT license.
// https://opensource.org/licenses/mit
//
// The repository for this library is available at the address:
// https://github.com/cfourney/OpenHarmony/
//
//
// For any requests feel free to contact m.chaptel@gmail.com
//
//
//
//
//////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////
//////////////////////////////////////
// //
// //
// $.oLayer class //
// //
// //
//////////////////////////////////////
//////////////////////////////////////
/**
* Constructor for $.oLayer class
* @classdesc
* The $.oLayer class represents a single line in the timeline.
* @constructor
* @param {oTimeline} oTimelineObject The timeline associated to this layer.
* @param {int} layerIndex The index of the layer on the timeline (all layers included, node and columns).
*
* @property {int} index The index of the layer on the timeline.
* @property {oTimeline} timeline The timeline associated to this layer.
* @property {oNode} node The node associated to the layer.
*/
$.oLayer = function( oTimelineObject, layerIndex){
this.timeline = oTimelineObject;
this.index = layerIndex;
}
/**
* The node associated to the layer.
* @name $.oLayer#node
* @type {$.oNode}
*/
Object.defineProperty($.oLayer.prototype, "node", {
get: function(){
if (this.$.batchMode){
_node = this.timeline.nodes[this.index];
} else {
_node = this.$.scn.getNodeByPath(Timeline.layerToNode(this.index));
}
return _node
}
})
/**
* the parent layer for this layer in the timeline. Returns the root group if layer is top level.
* @name $.oLayer#parent
* @type {$.oLayer}
*/
Object.defineProperty($.oLayer.prototype, "parent", {
get: function(){
var _parentIndex = Timeline.parentNodeIndex(this.index);
if (_parentIndex == -1) return $.scn.root;
var _parent = this.timeline.allLayers[_parentIndex];
return _parent;
}
})
/**
* wether or not the layer is selected.
* @name $.oLayer#selected
* @type {bool}
* @readonly
*/
Object.defineProperty($.oLayer.prototype, "selected", {
get: function(){
var selectionLength = Timeline.numLayerSel
for (var i=0; i<selectionLength; i++){
if (Timeline.selToLayer(i) == this.index) return true;
}
return false;
},
set:function(){
throw new Error ("unnamed layers selection cannot be set.")
}
})
/**
* The name of this layer/node.
* @name $.oLayer#name
* @type {string}
* @readonly
*/
Object.defineProperty($.oLayer.prototype, "name", {
get: function(){
return "unnamed layer";
}
})
/**
* @private
*/
$.oLayer.prototype.toString = function(){
return "<$.oLayer '"+this.name+"'>";
}
//////////////////////////////////////
//////////////////////////////////////
// //
// //
// $.oNodeLayer class //
// //
// //
//////////////////////////////////////
//////////////////////////////////////
/**
* Constructor for $.oNodeLayer class
* @classdesc
* The $.oNodeLayer class represents a timeline layer corresponding to a node from the scene.
* @constructor
* @extends $.oLayer
* @param {oTimeline} oTimelineObject The timeline associated to this layer.
* @param {int} layerIndex The index of the layer on the timeline.
*
* @property {int} index The index of the layer on the timeline.
* @property {oTimeline} timeline The timeline associated to this layer.
* @property {oNode} node The node associated to the layer.
*/
$.oNodeLayer = function( oTimelineObject, layerIndex){
this.$.oLayer.apply(this, [oTimelineObject, layerIndex]);
}
$.oNodeLayer.prototype = Object.create($.oLayer.prototype);
/**
* The name of this layer/node.
* @name $.oNodeLayer#name
* @type {string}
*/
Object.defineProperty($.oNodeLayer.prototype, "name", {
get: function(){
return this.node.name;
},
set: function(newName){
this.node.name = newName;
}
})
/**
* The layer index when ignoring subLayers.
* @name $.oNodeLayer#layerIndex
* @type {int}
*/
Object.defineProperty($.oNodeLayer.prototype, "layerIndex", {
get: function(){
var _layers = this.timeline.layers.map(function(x){return x.node.path});
return _layers.indexOf(this.node.path);
}
})
/**
* wether or not the layer is selected.
* @name $.oNodeLayer#selected
* @type {bool}
*/
Object.defineProperty($.oNodeLayer.prototype, "selected", {
get: function(){
if ($.batchMode) return this.node.selected;
var selectionLength = Timeline.numLayerSel
for (var i=0; i<selectionLength; i++){
if (Timeline.selToLayer(i) == this.index) return true;
}
return false;
},
set: function(selected){
this.node.selected = selected;
}
})
/**
* The column layers associated with this node.
* @name $.oNodeLayer#subLayers
* @type {$.oColumnLayer[]}
*/
Object.defineProperty($.oNodeLayer.prototype, "subLayers", {
get: function(){
var _node = this.node;
var _nodeLayerType = this.$.oNodeLayer;
return this.timeline.allLayers.filter(function (x){return x.node.path == _node.path && !(x instanceof _nodeLayerType)});
}
})
/**
* @private
*/
$.oNodeLayer.prototype.toString = function(){
return "<$.oNodeLayer '"+this.name+"'>";
}
//////////////////////////////////////
//////////////////////////////////////
// //
// //
// $.oDrawingLayer class //
// //
// //
//////////////////////////////////////
//////////////////////////////////////
/**
* Constructor for $.oDrawingLayer class
* @classdesc
* The $.oDrawingLayer class represents a timeline layer corresponding to a 'READ' node (or Drawing in Toonboom UI) from the scene.
* @constructor
* @extends $.oNodeLayer
* @param {oTimeline} oTimelineObject The timeline associated to this layer.
* @param {int} layerIndex The index of the layer on the timeline.
*
* @property {int} index The index of the layer on the timeline.
* @property {oTimeline} timeline The timeline associated to this layer.
* @property {oNode} node The node associated to the layer.
*/
$.oDrawingLayer = function( oTimelineObject, layerIndex){
this.$.oNodeLayer.apply(this, [oTimelineObject, layerIndex]);
}
$.oDrawingLayer.prototype = Object.create($.oNodeLayer.prototype);
/**
* The oFrame objects that hold the drawings for this layer.
* @name oDrawingLayer#drawingColumn
* @type {oFrame[]}
*/
Object.defineProperty($.oDrawingLayer.prototype, "drawingColumn", {
get: function(){
return this.node.attributes.drawing.elements.column;
}
})
/**
* The oFrame objects that hold the drawings for this layer.
* @name oDrawingLayer#exposures
* @type {oFrame[]}
*/
Object.defineProperty($.oDrawingLayer.prototype, "exposures", {
get: function(){
return this.drawingColumn.frames;
}
})
/**
* @private
*/
$.oDrawingLayer.prototype.toString = function(){
return "<$.oDrawingLayer '"+this.name+"'>";
}
//////////////////////////////////////
//////////////////////////////////////
// //
// //
// $.oColumnLayer class //
// //
// //
//////////////////////////////////////
//////////////////////////////////////
/**
* Constructor for $.oColumnLayer class
* @classdesc
* The $.oColumnLayer class represents a timeline layer corresponding to the animated values of a column linked to a node.
* @constructor
* @extends $.oLayer
* @param {oTimeline} oTimelineObject The timeline associated to this layer.
* @param {int} layerIndex The index of the layer on the timeline.
*
* @property {int} index The index of the layer on the timeline.
* @property {oTimeline} timeline The timeline associated to this layer.
* @property {oNode} node The node associated to the layer.
*/
$.oColumnLayer = function( oTimelineObject, layerIndex){
this.$.oLayer.apply(this, [oTimelineObject, layerIndex]);
}
$.oColumnLayer.prototype = Object.create($.oLayer.prototype);
/**
* The name of this layer.
* (corresponding to the display name of the column, not the name displayed in timeline, not exposed by the Toonboom API).
* @name $.oColumnLayer#name
* @type {string}
*/
Object.defineProperty($.oColumnLayer.prototype, "name", {
get: function(){
return this.column.name;
}
})
/**
* the node attribute associated with this layer. Only available if the attribute has a column.
* @name $.oColumnLayer#attribute
* @type {$.oColumn}
*/
Object.defineProperty($.oColumnLayer.prototype, "attribute", {
get: function(){
if (!this._attribute){
this._attribute = this.column.attributeObject;
}
return this._attribute
}
})
/**
* the node associated with this layer
* @name $.oColumnLayer#column
* @type {$.oColumn}
*/
Object.defineProperty($.oColumnLayer.prototype, "column", {
get: function(){
if (!this._column){
var _name = Timeline.layerToColumn(this.index);
var _attribute = this.node.getAttributeByColumnName(_name);
this._column = _attribute.column;
}
return this._column;
}
})
/**
* The layer representing the node to which this column is linked
*/
Object.defineProperty($.oColumnLayer.prototype, "nodeLayer", {
get: function(){
var _node = this.node;
var _nodeLayerType = this.$.oNodeLayer;
this.timeline.allLayers.filter(function (x){return x.node == _node && x instanceof _nodeLayerType})[0];
}
})
/**
* @private
*/
$.oColumnLayer.prototype.toString = function(){
return "<$.oColumnLayer '"+this.name+"'>";
}
//////////////////////////////////////
//////////////////////////////////////
// //
// //
// $.oTimeline class //
// //
// //
//////////////////////////////////////
//////////////////////////////////////
/**
* The $.oTimeline constructor.
* @constructor
* @classdesc The $.oTimeline class represents a timeline corresponding to a specific display.
* @param {string} [display] The display node's path. By default, the defaultDisplay of the scene.
*
* @property {string} display The display node's path.
*/
$.oTimeline = function(display){
if (typeof display === 'undefined') var display = this.$.scn.defaultDisplay;
if (display instanceof this.$.oNode) display = display.path;
this.display = display;
}
/**
* Gets the list of node layers in timeline.
* @name $.oTimeline#layers
* @type {$.oLayer[]}
*/
Object.defineProperty($.oTimeline.prototype, 'layers', {
get : function(){
var nodeLayer = this.$.oNodeLayer;
return this.allLayers.filter(function (x){return x instanceof nodeLayer})
}
});
/**
* Gets the list of all layers in timeline, nodes and columns. In batchmode, will only return the nodes, not the sublayers.
* @name $.oTimeline#allLayers
* @type {$.oLayer[]}
*/
Object.defineProperty($.oTimeline.prototype, 'allLayers', {
get : function(){
if (!this._layers){
var _layers = [];
if (!$.batchMode){
for( var i=0; i < Timeline.numLayers; i++ ){
if (Timeline.layerIsNode(i)){
var _layer = new this.$.oNodeLayer(this, i);
if (_layer.node.type == "READ") var _layer = new this.$.oDrawingLayer(this, i);
}else if (Timeline.layerIsColumn(i)) {
var _layer = new this.$.oColumnLayer(this, i);
}else{
var _layer = new this.$.oLayer(this, i);
}
_layers.push(_layer);
}
} else {
var _tl = this;
var _layers = this.nodes.map(function(x, index){
if (x.type == "READ") return new _tl.$.oDrawingLayer(_tl, index);
return new _tl.$.oNodeLayer(_tl, index)
})
}
this._layers = _layers;
}
return this._layers;
}
});
/**
* Gets the list of selected layers as oTimelineLayer objects.
* @name $.oTimeline#selectedLayers
* @type {oTimelineLayer[]}
*/
Object.defineProperty($.oTimeline.prototype, 'selectedLayers', {
get : function(){
return this.allLayers.filter(function(x){return x.selected});
}
});
/**
* The node layers in the scene, based on the timeline's order given a specific display.
* @name $.oTimeline#compositionLayers
* @type {oNode[]}
* @deprecated use oTimeline.nodes instead if you want the nodes
*/
Object.defineProperty($.oTimeline.prototype, 'compositionLayers', {
get : function(){
return this.nodes;
}
});
/**
* The nodes present in the timeline.
* @name $.oTimeline#nodes
* @type {oNode[]}
*/
Object.defineProperty($.oTimeline.prototype, 'nodes', {
get : function(){
var _timeline = this.compositionLayersList;
var _scene = this.$.scene;
_timeline = _timeline.map( function(x){return _scene.getNodeByPath(x)} );
return _timeline;
}
});
/**
* Gets the paths of the nodes displayed in the timeline.
* @name $.oTimeline#nodesList
* @type {string[]}
* @deprecated only returns node path strings, use oTimeline.layers insteads
*/
Object.defineProperty($.oTimeline.prototype, 'nodesList', {
get : function(){
return this.compositionLayersList;
}
});
/**
* Gets the paths of the layers in order, given the specific display's timeline.
* @name $.oTimeline#compositionLayersList
* @type {string[]}
* @deprecated only returns node path strings
*/
Object.defineProperty($.oTimeline.prototype, 'compositionLayersList', {
get : function(){
var _composition = this.composition;
var _timeline = _composition.map(function(x){return x.node})
return _timeline;
}
});
/**
* gets the composition for this timeline (array of native toonboom api 'compositionItems' objects)
* @deprecated exposes native harmony api objects
*/
Object.defineProperty($.oTimeline.prototype, "composition", {
get: function(){
return compositionOrder.buildCompositionOrderForDisplay(this.display);
}
})
/**
* Refreshes the oTimeline's cached listing- in the event it changes in the runtime of the script.
* @deprecated oTimeline.composition is now always refreshed when accessed.
*/
$.oTimeline.prototype.refresh = function( ){
if (!node.type(this.display)) {
this.composition = compositionOrder.buildDefaultCompositionOrder();
}else{
this.composition = compositionOrder.buildCompositionOrderForDisplay(this.display);
}
}
/**
* Build column to oNode/Attribute lookup cache. Makes the layer generation faster if using oTimeline.layers, oTimeline.selectedLayers
* @deprecated
*/
$.oTimeline.prototype.buildLayerCache = function( forced ){
if (typeof forced === 'undefined') forced = false;
var cdate = (new Date).getTime();
var rebuild = forced;
if( !this.$.cache_columnToNodeAttribute_date ){
rebuild = true;
}else if( !rebuild ){
if( ( cdate - this.$.cache_columnToNodeAttribute_date ) > 1000*10 ){
rebuild = true;
}
}
if(rebuild){
var nodeLayers = this.compositionLayers;
if( this.$.cache_nodeAttribute ){
this.$.cache_columnToNodeAttribute = {};
}
for( var n=0;n<nodeLayers.length;n++ ){
this.$.cache_columnToNodeAttribute = nodeLayers[n].getAttributesColumnCache( this.$.cache_columnToNodeAttribute );
}
this.$.cache_columnToNodeAttribute_date = cdate;
}
}

View file

@ -4,8 +4,8 @@
"description": "An Open Source Imlementation of a Document Object Model for the Toonboom Harmony scripting interface",
"main": "openHarmony.js",
"scripts": {
"test": "$"
"docs": "jsdoc -c jsdoc.json"
"test": "$",
"docs": "jsdoc -c jsdoc.json",
"generate-docs": "node_modules/.bin/jsdoc -c .jsdoc.json -d docs --verbose"
},
"repository": {

View file

@ -6,7 +6,7 @@
"extensions": [
{
"name": "openHarmony",
"version": "0.2.10",
"version": "0.8.0",
"compatibility": "Harmony Premium 15",
"description": "<p>The openHarmony library enables people to create scripts for Harmony using less code and a simpler synthax.</p><p>The complete documentation is available at this address:<a href='https://cfourney.github.io/OpenHarmony/'>https://cfourney.github.io/OpenHarmony/</a></p><p>Install this library to be able to use the scripts that require it.</p>",
"repository": "https://github.com/cfourney/OpenHarmony/",
@ -58,7 +58,7 @@
"openHarmony",
"palettes"
],
"author": "OpenHarmony",
"author": "Chris F",
"license": "MPL-v2.0",
"website": "https://github.com/cfourney/OpenHarmony/",
"localFiles": ""

View file

@ -72,7 +72,7 @@ def menu_install():
menu.addSeparator()
workfiles_action = menu.addAction("Work Files ...")
workfiles_action = menu.addAction("Work Files...")
workfiles_action.setIcon(QtGui.QIcon("icons:Position.png"))
workfiles_action.triggered.connect(launch_workfiles_app)
@ -82,30 +82,36 @@ def menu_install():
menu.addSeparator()
publish_action = menu.addAction("Publish ...")
publish_action = menu.addAction("Publish...")
publish_action.setIcon(QtGui.QIcon("icons:Output.png"))
publish_action.triggered.connect(
lambda *args: publish(hiero.ui.mainWindow())
)
creator_action = menu.addAction("Create ...")
creator_action = menu.addAction("Create...")
creator_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))
creator_action.triggered.connect(
lambda: host_tools.show_creator(parent=main_window)
)
loader_action = menu.addAction("Load ...")
loader_action = menu.addAction("Load...")
loader_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))
loader_action.triggered.connect(
lambda: host_tools.show_loader(parent=main_window)
)
sceneinventory_action = menu.addAction("Manage ...")
sceneinventory_action = menu.addAction("Manage...")
sceneinventory_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))
sceneinventory_action.triggered.connect(
lambda: host_tools.show_scene_inventory(parent=main_window)
)
library_action = menu.addAction("Library...")
library_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))
library_action.triggered.connect(
lambda: host_tools.show_library_loader(parent=main_window)
)
if os.getenv("OPENPYPE_DEVELOP"):
menu.addSeparator()
reload_action = menu.addAction("Reload pipeline")

View file

@ -51,16 +51,6 @@ def install():
# register callback for switching publishable
pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled)
# Disable all families except for the ones we explicitly want to see
family_states = [
"write",
"review",
"plate"
]
avalon.data["familiesStateDefault"] = False
avalon.data["familiesStateToggled"] = family_states
# install menu
menu.menu_install()

View file

@ -182,8 +182,11 @@ def get_output_parameter(node):
return node.parm("filename")
elif node_type == "comp":
return node.parm("copoutput")
else:
raise TypeError("Node type '%s' not supported" % node_type)
elif node_type == "arnold":
if node.evalParm("ar_ass_export_enable"):
return node.parm("ar_ass_file")
raise TypeError("Node type '%s' not supported" % node_type)
@contextmanager

View file

@ -60,12 +60,6 @@ def install():
"instanceToggled", on_pyblish_instance_toggled
)
log.info("Setting default family states for loader..")
avalon.api.data["familiesStateToggled"] = [
"imagesequence",
"review"
]
self._has_been_setup = True
# add houdini vendor packages
hou_pythonpath = os.path.join(os.path.dirname(HOST_DIR), "vendor")

View file

@ -0,0 +1,53 @@
from openpype.hosts.houdini.api import plugin
class CreateArnoldAss(plugin.Creator):
"""Arnold .ass Archive"""
label = "Arnold ASS"
family = "ass"
icon = "magic"
defaults = ["Main"]
# Default extension: `.ass` or `.ass.gz`
ext = ".ass"
def __init__(self, *args, **kwargs):
super(CreateArnoldAss, self).__init__(*args, **kwargs)
# Remove the active, we are checking the bypass flag of the nodes
self.data.pop("active", None)
self.data.update({"node_type": "arnold"})
def process(self):
node = super(CreateArnoldAss, self).process()
basename = node.name()
node.setName(basename + "_ASS", unique_name=True)
# Hide Properties Tab on Arnold ROP since that's used
# for rendering instead of .ass Archive Export
parm_template_group = node.parmTemplateGroup()
parm_template_group.hideFolder("Properties", True)
node.setParmTemplateGroup(parm_template_group)
filepath = '$HIP/pyblish/`chs("subset")`.$F4{}'.format(self.ext)
parms = {
# Render frame range
"trange": 1,
# Arnold ROP settings
"ar_ass_file": filepath,
"ar_ass_export_enable": 1
}
node.setParms(parms)
# Lock the ASS export attribute
node.parm("ar_ass_export_enable").lock(True)
# Lock some Avalon attributes
to_lock = ["family", "id"]
for name in to_lock:
parm = node.parm(name)
parm.lock(True)

Some files were not shown because too many files have changed in this diff Show more