├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TODO.md ├── examples └── comp │ ├── ask_user_dialog.py │ ├── assign_random_colors.py │ ├── copy_paste.py │ ├── disconnect_all_inputs_selected.py │ ├── set_backgrounds_to_1920x1080_32bit.py │ └── set_input_on_selected_loaders.py ├── fusionless ├── __init__.py ├── classes │ ├── __init__.py │ └── gl_view.py ├── context.py ├── core.py ├── standalone.py └── version.py └── tests └── test_tools.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.pyc 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ********** 2 | What's New 3 | ********** 4 | 5 | ================================== 6 | Version 0.1.1 7 | ================================== 8 | 9 | ---------------------------------- 10 | Additions 11 | ---------------------------------- 12 | - core: Added extra methods to PyObject and updated its TODO to reflect those still needing to added. 13 | - core: Added `name` parameter to Comp.create_tool() to easily allow direct naming of the tool. 14 | - core: Implemented Comp, Flow, Tool, Input, Output classes 15 | 16 | ---------------------------------- 17 | Changes 18 | ---------------------------------- 19 | - core: Refactored PyNode to PyObject class 20 | - core: Refactored Attribute to Link class (to be closer to Fusion's internal naming) 21 | - core: Refactored Comp.create_node() to Comp.create_tool() 22 | 23 | ================================== 24 | Version 0.1.0 25 | ================================== 26 | 27 | ---------------------------------- 28 | Additions 29 | ---------------------------------- 30 | - core: Implemented PyNode base class 31 | - core: Implemented Comp, Flow, Tool, Input, Output classes 32 | - context: Implemented LockComp, UndoChunk and LockAndUndoChunk context managers. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Roy Nieterau 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 3. The name of the author may not be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 16 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fusionless 2 | Python in Black Magic Design's Fusion that sucks less. 3 | 4 | Similar to how **Pymel** tries to do Python the right way in Maya this is the same thing for Fusion. 5 | 6 | We'll make it *suck less*. 7 | 8 | ### Supported platforms 9 | 10 | **Fusionless** works with Fusion 8.0 (but is compatible since Fusion 6.0+). 11 | With support for both Python 2 and 3 on all platforms (win/mac/linux). 12 | 13 | ### Goals 14 | 15 | Our highest priority goals are similar to those of **Pymel**: 16 | 17 | - Create an open-source python module for Fusion that is intuitive to python users 18 | - Fix bugs and design limitations in Fusion's python module 19 | - Keep code concise and readable 20 | - Add organization through class hierarchy and sub-modules 21 | - Provide documentation accessible via html and the builtin `help() function 22 | - Make it "just work" 23 | 24 | Because it should all be that much simpler. 25 | It's time for that change. Now. 26 | 27 | ### Installation 28 | 29 | To quickly give **fusionless** a spin run this in Fusion (Python): 30 | 31 | ```python 32 | fusionless_dir = "path/to/fusionless" # <- set the path to fusionless 33 | 34 | import sys 35 | sys.path.append('path/to/fusionless') 36 | import fusionless 37 | ``` 38 | 39 | If no errors occured you should be able to use fusionless. For example 40 | add a blur tool to the currently active comp: 41 | 42 | ```python 43 | import fusionless 44 | 45 | current_comp = fusionless.Comp() 46 | current_comp.create_tool("Blur") 47 | ``` 48 | 49 | For more examples see the *[examples](examples)* directory in the repository. 50 | 51 | ##### PeyeonScript (Fusion pre-8.0 dependency) 52 | 53 | To get any access to Fusion (before 8.0) in Python you need PeyeonScript. 54 | As such **fusionless* has this same requirement as it needs to be 55 | able to call the Fusion API. 56 | 57 | Unfortunately distribution of it is sparse, so 58 | [here](http://www.steakunderwater.com/wesuckless/viewtopic.php?t=387)'s a 59 | topic that has a working download link. All you need is the installer for your 60 | Python version. Peyeonscript (for 6+) seems to work fine with newer versions of 61 | Fusion (6.4 or 7+), but is **not** required for Fusion 8+. 62 | 63 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | Before 0.2: 2 | - Implement a basic logging handler 3 | 4 | Before 0.3: 5 | - Implement a standalone initializer (if possible) 6 | 7 | Before 0.4: 8 | - Find and implement the code to find the main threads event handler so we can hook up external UI events 9 | 10 | Before 1.0: 11 | - Implement examples to get PySide up and running with Fusion in a stable manner. -------------------------------------------------------------------------------- /examples/comp/ask_user_dialog.py: -------------------------------------------------------------------------------- 1 | """Example showing the Ask User dialog controls and overall usage.""" 2 | 3 | import fusionless as fu 4 | 5 | dialog = fu.AskUserDialog("Example Ask User Dialog") 6 | dialog.add_text("text", default="Default text value") 7 | dialog.add_position("position", default=(0.2, 0.8)) 8 | dialog.add_slider("slider", default=0.5, min=-10, max=10) 9 | dialog.add_screw("screw") 10 | dialog.add_file_browse("file", default="C:/path/to/foo") 11 | dialog.add_path_browse("path") 12 | dialog.add_clip_browse("clip") 13 | dialog.add_checkbox("checkbox", name="Do not check this!") 14 | dialog.add_dropdown("dropdown", options=["A", "B", "C"]) 15 | dialog.add_multibutton("multibutton", options=["Foo", "Bar", "Nugget"]) 16 | result = dialog.show() 17 | 18 | if result is None: 19 | # Dialog was cancelled 20 | pass 21 | else: 22 | checked = result['checkbox'] 23 | if checked: 24 | print("You sure are living on the edge!") 25 | 26 | import pprint 27 | pprint.pprint(result) 28 | -------------------------------------------------------------------------------- /examples/comp/assign_random_colors.py: -------------------------------------------------------------------------------- 1 | import fusionless as fu 2 | import random 3 | 4 | 5 | def get_random_color(): 6 | clr = {} 7 | for key in ['R', 'G', 'B']: 8 | clr[key] = random.uniform(0, 1) 9 | return clr 10 | 11 | 12 | comp = fu.Comp() 13 | for tool in comp.get_selected_tools(): 14 | tool.set_tile_color(get_random_color()) 15 | tool.set_text_color(get_random_color()) -------------------------------------------------------------------------------- /examples/comp/copy_paste.py: -------------------------------------------------------------------------------- 1 | import fusionless as fu 2 | 3 | comp = fu.Comp() 4 | tools = comp.get_selected_tools() 5 | comp.copy(tools) 6 | comp.paste() 7 | -------------------------------------------------------------------------------- /examples/comp/disconnect_all_inputs_selected.py: -------------------------------------------------------------------------------- 1 | """Disconnect all inputs for selected tools""" 2 | 3 | import fusionless as fu 4 | import fusionless.context as fuCtx 5 | 6 | c = fu.Comp() 7 | with fuCtx.lock_and_undo_chunk(c, "Disconnect inputs"): 8 | 9 | for tool in c.get_selected_tools(): 10 | for input in tool.inputs(): 11 | input.disconnect() -------------------------------------------------------------------------------- /examples/comp/set_backgrounds_to_1920x1080_32bit.py: -------------------------------------------------------------------------------- 1 | """Sets all Backgrounds in the currently active comp to 1920x1080 (32 bit). 2 | 3 | This example shows how to list tool of a specific type and set some of its 4 | inputs. Additionally this shows off how `fusionless` is able to automatically 5 | interpret an enum value (like "float32" for image depth) to the corresponding 6 | float value that Fusion requires to be set internally. 7 | 8 | """ 9 | 10 | import fusionless as fu 11 | import fusionless.context as fuCtx 12 | 13 | c = fu.Comp() 14 | with fuCtx.lock_and_undo_chunk(c, "Set all backgrounds to 1920x1080 (32 bit)"): 15 | 16 | # Get all backgrounds in the current comp 17 | tools = c.get_tool_list(selected=False, 18 | node_type="Background") 19 | 20 | for tool in tools: 21 | tool.input("Width").set_value(1920) 22 | tool.input("Height").set_value(1080) 23 | 24 | # Set the depth to "float32". Note that 25 | # fusion internally uses float value indices 26 | # for the different values. `fusionless` will 27 | # automatically convert enum values to their 28 | # corresponding float value when possible. 29 | tool.input("Depth").set_value("float32") 30 | 31 | # So the depth would internally get set like 32 | # tool.input("Depth").set_value(4.0) -------------------------------------------------------------------------------- /examples/comp/set_input_on_selected_loaders.py: -------------------------------------------------------------------------------- 1 | """Enable the 'Loop' input for all selected loaders""" 2 | 3 | import fusionless as fu 4 | import fusionless.context as fuCtx 5 | 6 | c = fu.Comp() 7 | with fuCtx.lock_and_undo_chunk(c, "Set loaders to loop"): 8 | 9 | for tool in c.get_selected_tools(node_type="Loader"): 10 | loop = tool.input("Loop").set_value(True) 11 | -------------------------------------------------------------------------------- /fusionless/__init__.py: -------------------------------------------------------------------------------- 1 | """ The primary module for fusion commands and node classes """ 2 | 3 | from .version import * 4 | from .core import * 5 | -------------------------------------------------------------------------------- /fusionless/classes/__init__.py: -------------------------------------------------------------------------------- 1 | from gl_view import GLView, GLViewer, GLImageViewer 2 | 3 | __all__ = ['GLView', 'GLViewer', 'GLImageViewer'] -------------------------------------------------------------------------------- /fusionless/classes/gl_view.py: -------------------------------------------------------------------------------- 1 | import fusionscript.core 2 | 3 | 4 | class GLView(fusionscript.core.PyObject): 5 | """ The view object has methods that deal with general properties of the view, 6 | 7 | like enabling the sub views or switching between the A & B buffers. 8 | 9 | Each ChildFrame (i.e. composition window) has at least a left and a right view, represented by GLView objects. 10 | To reach one of these, use something like this: 11 | 12 | >>> # .Left will return the left preview object, the GLView object can be reached via .View 13 | >>> comp:GetPreviewList().Left.View 14 | 15 | Or this: 16 | 17 | >>> comp.CurrentFrame.LeftView 18 | """ 19 | # TODO: Implement `GLView` 20 | # Reference: http://www.steakunderwater.com/VFXPedia/96.0.243.189/index74ef.html?title=Eyeon:Script/Reference/Applications/Fusion/Classes/GLView 21 | def current_viewer(self): 22 | self.CurrentViewer 23 | 24 | 25 | class GLViewer(fusionscript.core.PyObject): 26 | """ 27 | Each buffer can in turn contain a viewer, represented by either GLViewer or GLImageViewer objects. 28 | Use a view's :GetViewerList() method or .CurrentViewer to reach these objects. 29 | 30 | They provides further methods specific to a single display buffer like showing guides, enabling LUTs or switching 31 | color channels: 32 | 33 | For example, to switch to A buffer and show the red channel: 34 | >>> left = comp.CurrentFrame.LeftView 35 | >>> left.SetBuffer(0) # switch to A buffer 36 | >>> viewer = left.CurrentViewer 37 | >>> if viewer is not None: 38 | >>> viewer.SetChannel(0) # switch to red channel 39 | >>> viewer.Redraw() 40 | 41 | This is a parent class for 2D and 3D viewers. 2D image viewers are instances of the GLImageViewer subclass and have 42 | additional methods to set and show the DoD, RoI or LUT. 43 | 44 | """ 45 | pass 46 | # TODO: Implement `GLViewer` 47 | # Reference: http://www.steakunderwater.com/VFXPedia/96.0.243.189/indexc3e5.html?title=Eyeon:Script/Reference/Applications/Fusion/Classes/GLViewer 48 | 49 | 50 | class GLImageViewer(GLViewer): 51 | """ The GLImageViewer is the subclass of GLViewer that is used for 2D views. 52 | 53 | The GLImageViewer has additional methods for the 2D to set and show the DoD, RoI or LUT. 54 | """ 55 | pass 56 | # TODO: Implement `GLImageViewer` 57 | # Reference: http://www.steakunderwater.com/VFXPedia/96.0.243.189/index1cee.html?title=Eyeon:Script/Reference/Applications/Fusion/Classes/GLImageViewer -------------------------------------------------------------------------------- /fusionless/context.py: -------------------------------------------------------------------------------- 1 | """ A set of Context managers to provide a simple and safe way to manage Fusion's context. 2 | 3 | Example 4 | >>> with lock_comp(): 5 | >>> print "Everything in here is done while the composition is locked." 6 | 7 | """ 8 | 9 | import contextlib 10 | 11 | @contextlib.contextmanager 12 | def lock_comp(comp): 13 | comp.lock() 14 | try: 15 | yield 16 | finally: 17 | comp.unlock() 18 | 19 | @contextlib.contextmanager 20 | def undo_chunk(comp, undoQueueName="Script CMD"): 21 | comp.start_undo(undoQueueName) 22 | try: 23 | yield 24 | finally: 25 | comp.end_undo() 26 | 27 | 28 | @contextlib.contextmanager 29 | def lock_and_undo_chunk(comp, undoQueueName="Script CMD"): 30 | with lock_comp(comp): 31 | with undo_chunk(comp, undoQueueName): 32 | yield -------------------------------------------------------------------------------- /fusionless/core.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains the main `PyNode` base class and its derived classes. 3 | 4 | ### Reference 5 | 6 | For a community-built class list of Fusion's built-in classes: 7 | http://www.steakunderwater.com/VFXPedia/96.0.243.189/index8c76.html?title=Eyeon:Script/Reference/Applications/Fusion/Classes 8 | """ 9 | 10 | import sys 11 | 12 | 13 | class PyObject(object): 14 | """This is the base class for all classes referencing Fusion's classes. 15 | 16 | Upon initialization of any PyObject class it checks whether the referenced 17 | data is of the correct type usingbPython's special `__new__` method. 18 | 19 | This way we convert the instance to correct class type based on the 20 | internal Fusion type. 21 | 22 | The `PyObject` is *fusionless*'s class representation of Fusion's 23 | internal Object class. All the other classes representing Fusion objects 24 | are derived from PyObject. 25 | 26 | Example 27 | >>> node = PyObject(comp) 28 | >>> print type(node) 29 | >>> # Comp() 30 | 31 | At any time you can access Fusion's python object from the instance using 32 | the `_reference` attribute. 33 | 34 | Example 35 | >>> node = PyObject() 36 | >>> reference = node._reference 37 | >>> print reference 38 | 39 | """ 40 | 41 | _reference = None # reference to PyRemoteObject 42 | _default_reference = None 43 | 44 | def __new__(cls, *args, **kwargs): 45 | """Convert the class instantiation to the correct type. 46 | 47 | This is where the magic happens that automatically maps any of the 48 | PyNode objects to the correct class type. 49 | 50 | """ 51 | 52 | reference = args[0] if args else None 53 | # if argument provided is already of correct class type return it 54 | if isinstance(reference, cls): 55 | return reference 56 | # if no arguments assume reference to default type for cls (if any) 57 | elif reference is None: 58 | if cls._default_reference is not None: 59 | reference = cls._default_reference() 60 | if reference is None: 61 | raise RuntimeError("No default reference for: " 62 | "{0}".format(type(reference).__name__)) 63 | else: 64 | raise ValueError("Can't instantiate a PyObject with a " 65 | "reference to None") 66 | 67 | # Python crashes whenever you perform `type()` or `dir()` on the 68 | # PeyeonScript.scripapp() retrieved applications. As such we try to 69 | # get the attributes before that check before type-checking in case 70 | # of errors. 71 | attrs = None 72 | try: 73 | attrs = reference.GetAttrs() 74 | except AttributeError: 75 | # Check if the reference is a PyRemoteObject. 76 | # Since we don't have access to the class type that fusion returns 77 | # outside of Fusion we use a hack based on its name 78 | if type(reference).__name__ != 'PyRemoteObject': 79 | raise TypeError("Reference is not of type PyRemoteObject " 80 | "but {0}".format(type(reference).__name__)) 81 | 82 | newcls = None 83 | if attrs: 84 | # Acquire an attribute to check for type (start prefix) 85 | # Comp, Tool, Input, Output, View, etc. all return attributes 86 | # that define its type 87 | data_type = next(iter(attrs)) 88 | 89 | # Define the new class type 90 | if data_type.startswith("COMP"): 91 | newcls = Comp 92 | elif data_type.startswith("TOOL"): 93 | newcls = Tool 94 | elif data_type.startswith("INP"): 95 | newcls = Input 96 | elif data_type.startswith("OUT"): 97 | newcls = Output 98 | elif data_type.startswith("VIEW"): 99 | newcls = Flow 100 | elif data_type.startswith("FUSION"): 101 | newcls = Fusion 102 | 103 | else: 104 | # Image (output value) does not return attributes from GetAttrs() 105 | # so we use some data from the PyRemoteObject 106 | str_data_type = str(reference).split(' ', 1)[0] 107 | 108 | if str_data_type == "Image": 109 | newcls = Image 110 | 111 | # Ensure we convert to a type preferred by the user 112 | # eg. Tool() would come out as Comp() since no arguments are provided. 113 | # so instead we raise a TypeError() to be clear in those cases. 114 | if cls is not PyObject: 115 | if not issubclass(newcls, cls): 116 | raise TypeError("PyObject did not convert to preferred " 117 | "type. '{0}' is not an instance " 118 | "of '{1}'".format(newcls, cls)) 119 | 120 | # Instantiate class and return 121 | if newcls: 122 | klass = super(PyObject, cls).__new__(newcls) 123 | klass._reference = reference 124 | return klass 125 | 126 | return None 127 | 128 | def set_attr(self, key, value): 129 | self.set_attrs({key: value}) 130 | 131 | def get_attr(self, key): 132 | attrs = self.get_attrs() 133 | return attrs[key] 134 | 135 | def set_attrs(self, attr_values): 136 | self._reference.SetAttrs(attr_values) 137 | 138 | def get_attrs(self): 139 | return self._reference.GetAttrs() 140 | 141 | def set_data(self, name, value): 142 | """ Set persistent data on this object. 143 | 144 | Persistent data is a very useful way to store names, dates, filenames, 145 | notes, flags, or anything else, in such a way that they are permanently 146 | associated with this instance of the object, and are stored along with 147 | the object. This data can be retrieved at any time by using 148 | `get_data()`. 149 | 150 | The method of storage varies by object: 151 | - Fusion application: 152 | SetData() called on the Fusion app itself will save its data in the 153 | Fusion.prefs file, and will be available whenever that copy of 154 | Fusion is running. 155 | 156 | - Objects associated with a composition: 157 | Calling SetData() on any object associated with a Composition will 158 | cause the data to be saved in the .comp file, or in any settings 159 | files that may be saved directly from that object. 160 | 161 | - Ephemeral objects not associated with composition: 162 | Some ephemeral objects that are not associated with any 163 | composition and are not otherwise saved in any way, may not have 164 | their data permanently stored at all, and the data will only 165 | persist as long as the object itself does. 166 | 167 | .. note:: 168 | You can use SetData to add a key called HelpPage to any tool. 169 | Its value can be a URL to a web page (for example a link to a 170 | page on Vfxpedia) and will override this tool's default help when 171 | the user presses F1 (requires Fusion 6.31 or later). 172 | It's most useful for macros. 173 | 174 | Example 175 | >>> Comp().set_data('HelpPage', 176 | >>> 'https://github.com/BigRoy/fusionless') 177 | 178 | Args: 179 | name (str): The name the name of the attribute to set. 180 | As of 5.1, this name can be in "table.subtable" format, 181 | to allow setting persistent data within subtables. 182 | value: This is the value to be recorded in the object's persistent 183 | data. It can be of almost any type. 184 | 185 | Returns: 186 | None 187 | 188 | """ 189 | self._reference.SetData(name, value) 190 | 191 | def get_data(self, name): 192 | """ Get persistent data from this object. 193 | 194 | Args: 195 | name (str): The name of the attribute to fetch. 196 | 197 | Returns: 198 | The value fetched from the object's persistent data. 199 | It can be of almost any type. 200 | 201 | """ 202 | return self._reference.GetData(name) 203 | 204 | def name(self): 205 | """The internal Fusion Name as string of the node this PyObject 206 | references. 207 | 208 | Returns: 209 | str: A string value containing the internal Name of this node. 210 | 211 | """ 212 | return self._reference.Name 213 | 214 | def id(self): 215 | """Returns the ID key as string. 216 | 217 | For example the id label of an Input is used to retrieve it from the 218 | Tool. 219 | 220 | Returns: 221 | str: Internal ID of the referenced Fusion object 222 | 223 | """ 224 | return self._reference.ID 225 | 226 | def get_help(self): 227 | """Returns a formatted string of internal help information to Fusion. 228 | 229 | Returns: 230 | str: internal Fusion information 231 | 232 | """ 233 | return self._reference.GetHelp() 234 | 235 | def get_reg(self): 236 | """ Returns the related Registry instance for this PyObject. 237 | 238 | Returns: 239 | Registry: The registry related to this object. 240 | 241 | """ 242 | return self._reference.GetReg() 243 | 244 | def get_id(self): 245 | """Returns the internal Fusion ID as string. 246 | 247 | .. note:: This uses the internal `GetID()` method on the object 248 | instance that this PyObject references. 249 | 250 | Returns: 251 | str: Internal ID of the referenced Fusion object 252 | 253 | """ 254 | return self._reference.GetID() 255 | 256 | def comp(self): 257 | """ Return the Comp this instance belongs to. 258 | 259 | Returns: 260 | Comp: The composition from this instance. 261 | 262 | """ 263 | return Comp(self._reference.Comp()) 264 | 265 | def __getattr__(self, attr): 266 | """Allow access to Fusion's built-in methods on the reference directly. 267 | 268 | .. note:: 269 | Since the normal behaviour is to raise a very confusing TypeError 270 | whenever an unknown method is called on the PyRemoteObject so we 271 | added a raise a more explanatory error if we retrieve unknown data. 272 | 273 | """ 274 | result = getattr(self._reference, attr) 275 | if result is None: 276 | raise AttributeError("{0} object has no attribute " 277 | "'{1}'".format(self, attr)) 278 | 279 | return result 280 | 281 | def __repr__(self): 282 | return '{0}("{1}")'.format(self.__class__.__name__, 283 | str(self._reference.Name)) 284 | 285 | # TODO: Implement PyObject.GetApp 286 | # TODO: Implement PyObject.TriggerEvent 287 | 288 | 289 | class Comp(PyObject): 290 | """ A Comp instance refers to a Fusion composition. 291 | 292 | Here you can perform the global changes to the current composition. 293 | """ 294 | # TODO: Implement the rest of the `Comp` methods and its documentations. 295 | 296 | @staticmethod 297 | def _default_reference(): 298 | """Fallback for the default reference""" 299 | 300 | # this would be accessible within Fusion scripts as "comp" 301 | ref = getattr(sys.modules["__main__"], "comp", None) 302 | if ref is not None: 303 | return ref 304 | 305 | # this would be accessible within Fusion's console and is not always 306 | # present in the locals() or globals(). It's seems to be magically get 307 | # served up when requested. So we try it get it served. 308 | # Note: this doesn't seem to work in Fusion 8+ but worked in older 309 | # versions 310 | try: 311 | return comp 312 | except NameError: 313 | pass 314 | 315 | # this would be accessible within Fusion's console (if in globals) 316 | # haven't seen this happen yet. 317 | ref = globals().get("comp", None) 318 | if ref is not None: 319 | return ref 320 | 321 | # if the above fails we can try to find an active "fusion" and get 322 | # the currently active composition (fallback method) 323 | try: 324 | fusion = Fusion() 325 | comp = fusion.get_current_comp() 326 | return comp._reference 327 | except RuntimeError: 328 | pass 329 | 330 | def get_current_time(self): 331 | """ Returns the current time in this composition. 332 | 333 | :return: Current time. 334 | :rtype: int 335 | """ 336 | return self._reference.CurrentTime 337 | 338 | def get_tool_list(self, selected=False, node_type=None): 339 | """ Returns the tool list of this composition. 340 | 341 | Args: 342 | selected (bool): Whether to return only tools from the current 343 | selection. When False all tools in the composition will be 344 | considered. 345 | node_type (str): If provided filter to only tools of this type. 346 | 347 | Returns: 348 | list: A list of Tool instances 349 | 350 | """ 351 | args = (node_type,) if node_type is not None else tuple() 352 | return [Tool(x) for x in self._reference.GetToolList(selected, 353 | *args).values()] 354 | 355 | def get_selected_tools(self, node_type=None): 356 | """Returns the currently selected tools. 357 | 358 | .. warning:: 359 | Fusion does NOT return the selected tool list in the order of 360 | selection! 361 | 362 | Returns: 363 | list: A list of selected Tool instances 364 | 365 | """ 366 | return self.get_tool_list(True, node_type=node_type) 367 | 368 | def current_frame(self): 369 | """ Returns the currently active ChildFrame for this composition. 370 | 371 | ..note :: 372 | This does not return the current time, but a UI element. 373 | To get the current time use `get_current_time()` 374 | 375 | Returns: 376 | The currently active ChildFrame for this Composition. 377 | 378 | """ 379 | return self.CurrentFrame 380 | 381 | def get_active_tool(self): 382 | """ Return active tool. 383 | 384 | Returns: 385 | Tool or None: Currently active tool on this comp 386 | 387 | """ 388 | tool = self._reference.ActiveTool 389 | return Tool(tool) if tool else None 390 | 391 | def set_active_tool(self, tool): 392 | """ Set the current active tool in the composition to the given tool. 393 | 394 | If tool is None it ensure nothing is active. 395 | 396 | Args: 397 | tool (Tool): The tool instance to make active. 398 | If None provided active tool will be deselected. 399 | 400 | Returns: 401 | None 402 | 403 | """ 404 | if tool is None: # deselect if None 405 | self._reference.SetActiveTool(None) 406 | return 407 | 408 | if not isinstance(tool, Tool): 409 | tool = Tool(tool) 410 | 411 | self._reference.SetActiveTool(tool._reference) 412 | 413 | def create_tool(self, node_type, attrs=None, insert=False, name=None): 414 | """ Creates a new node in the composition based on the node type. 415 | 416 | Args: 417 | node_type (str): The type id of the node to create. 418 | attrs (dict): A dictionary of input values to set. 419 | insert (bool): When True the node gets created and automatically 420 | inserted/connected to the active tool. 421 | name (str): When provided the created node is automatically 422 | renamed to the provided name. 423 | 424 | Returns: 425 | Tool: The created Tool instance. 426 | 427 | """ 428 | 429 | # Fusion internally uses the magic 'position' (-32768, -32768) to trigger an automatic connection and insert 430 | # when creating a new node. So we use that internal functionality when `insert` parameter is True. 431 | args = (-32768, -32768) if insert else tuple() 432 | tool = Tool(self._reference.AddTool(node_type, *args)) 433 | 434 | if attrs: # Directly set attributes if any provided 435 | tool.set_attrs(attrs) 436 | 437 | if name: # Directly set a name if any provided 438 | tool.rename(name) 439 | 440 | return tool 441 | 442 | def copy(self, tools): 443 | """Copy a list of tools to the Clipboard. 444 | 445 | The copied Tools can be pasted into the Composition by using its 446 | corresponding `paste` method. 447 | 448 | Args: 449 | tools (list): The Tools list to be copied to the clipboard 450 | 451 | """ 452 | return self._reference.Copy([tool._reference for tool in tools]) 453 | 454 | def paste(self, settings=None): 455 | """Pastes a tool from the Clipboard or a settings table. 456 | 457 | Args: 458 | settings (dict or None): If settings dictionary provided it will be 459 | used as the settings table to be copied, instead of using the 460 | Comp's current clipboard. 461 | 462 | Returns: 463 | None 464 | 465 | """ 466 | args = tuple() if settings is None else (settings,) 467 | return self._reference.Paste(*args) 468 | 469 | def lock(self): 470 | """Sets the composition to a locked state. 471 | 472 | Sets a composition to non-interactive ("batch", or locked) mode. 473 | This makes Fusion suppress any dialog boxes which may appear, and 474 | additionally prevents any re-rendering in response to changes to the 475 | controls. A locked composition can be unlocked with the unlock() 476 | function, which returns the composition to interactive mode. 477 | 478 | It is often useful to surround a script with Lock() and Unlock(), 479 | especially when adding tools or modifying a composition. Doing this 480 | ensures Fusion won't pop up a dialog to ask for user input, e.g. when 481 | adding a Loader, and can also speed up the operation of the script 482 | since no time will be spent rendering until the comp is unlocked. 483 | 484 | For convenience this is also available as a Context Manager as 485 | `fusionless.context.LockComp`. 486 | 487 | """ 488 | self._reference.Lock() 489 | 490 | def unlock(self): 491 | """Sets the composition to an unlocked state.""" 492 | self._reference.Unlock() 493 | 494 | def redo(self, num=1): 495 | """Redo one or more changes to the composition. 496 | 497 | Args: 498 | num (int): Amount of redo changes to perform. 499 | 500 | """ 501 | self._reference.Redo(num) 502 | 503 | def undo(self, num): 504 | """ Undo one or more changes to the composition. 505 | 506 | Args: 507 | num (int): Amount of undo changes to perform. 508 | 509 | """ 510 | self._reference.Undo(num) 511 | 512 | def start_undo(self, name): 513 | """Start an undo block. 514 | 515 | This should always be paired with an end_undo call. 516 | 517 | The StartUndo() function is always paired with an EndUndo() function. 518 | Any changes made to the composition by the lines of script between 519 | StartUndo() and EndUndo() are stored as a single Undo event. 520 | 521 | Changes captured in the undo event can be undone from the GUI using 522 | CTRL-Z, or the Edit menu. They can also be undone from script, by 523 | calling the `undo()` method. 524 | 525 | .. note:: 526 | If the script exits before `end_undo()` is called Fusion will 527 | automatically close the undo event. 528 | 529 | Args: 530 | name (str): Specifies the name displayed in the Edit/Undo menu of 531 | the Fusion GUI a string containing the complete path and name 532 | of the composition to be saved. 533 | 534 | """ 535 | self._reference.StartUndo(name) 536 | 537 | def end_undo(self, keep=True): 538 | """Close an undo block. 539 | 540 | This should always be paired with a start_undo call. 541 | 542 | The `start_undo()` is always paired with an `end_undo()` call. 543 | Any changes made to the composition by the lines of script between 544 | `start_undo()` and `end_undo()` are stored as a single Undo event. 545 | 546 | Changes captured in the undo event can be undone from the GUI using 547 | CTRL-Z, or the Edit menu. They can also be undone from script, by 548 | calling the `undo()` method. 549 | 550 | Specifying 'True' results in the undo event being added to the undo 551 | stack, and appearing in the appropriate menu. Specifying False' will 552 | result in no undo event being created. This should be used sparingly, 553 | as the user (or script) will have no way to undo the preceding 554 | commands. 555 | 556 | .. note:: 557 | If the script exits before `end_undo()` is called Fusion will 558 | automatically close the undo event. 559 | 560 | Args: 561 | keep (bool): Determines whether the captured undo event is to kept 562 | or discarded. 563 | 564 | Returns: 565 | None 566 | 567 | """ 568 | self._reference.EndUndo(keep) 569 | 570 | def clear_undo(self): 571 | """Use this function to clear the undo/redo history for the 572 | composition.""" 573 | self._reference.ClearUndo() 574 | 575 | def save(self, filename=None): 576 | """Save the composition. 577 | 578 | This function causes the composition to be saved to disk. The compname 579 | argument must specify a path relative to the filesystem of the Fusion 580 | which is saving the composition. In other words - if system `a` is 581 | using the Save() function to instruct a Fusion on system `b` to save a 582 | composition, the path provided must be valid from the perspective of 583 | system `b`. 584 | 585 | Arguments: 586 | filename (str): Full path to save to. When None it will save over 587 | the current comp path (if alreaady available). 588 | 589 | Returns: 590 | bool: Whether saving succeeded 591 | 592 | """ 593 | 594 | if filename is None and not self.filename(): 595 | # When not saved yet we raise an error instead of 596 | # silently failing without explanation 597 | raise ValueError("Can't save comp without filename.") 598 | 599 | return self._reference.Save(filename) 600 | 601 | def play(self): 602 | """This function is used to turn on the play control in the playback 603 | controls of the composition. 604 | """ 605 | self._reference.Play() 606 | 607 | def stop(self): 608 | """This function is used to turn off the play control in the playback 609 | controls of the composition. 610 | """ 611 | self._reference.Stop() 612 | 613 | def loop(self, mode): 614 | """This function is used to turn on the loop control in the playback 615 | controls of the composition. 616 | 617 | Args: 618 | mode (bool): Enables looping interactive playback. 619 | 620 | Returns: 621 | None 622 | 623 | """ 624 | self._reference.Loop(mode) 625 | 626 | def render(self, wait_for_render, **kwargs): 627 | """ Renders the composition. 628 | 629 | Args: 630 | wait_for_render (bool): Whether the script should wait for the 631 | render to complete or continue processing once the render has 632 | begun. Defaults to False. 633 | 634 | Kwargs: 635 | start (int): The frame to start rendering at. 636 | Default: Comp's render start settings. 637 | end (int): The frame to stop rendering at. 638 | Default: Comp's render end settings. 639 | high_quality (bool): Render in High Quality (HiQ). 640 | Default True. 641 | render_all (bool): Render all tools, even if not required by a 642 | saver. Default False. 643 | motion_blur (bool): Do motion blur in render, where specified in 644 | tools. Default true. 645 | size_type (int): Resize the output: 646 | -1. Custom (only used by PreviewSavers during 647 | preview render) 648 | 0. Use prefs setting 649 | 1. Full Size (default) 650 | 2. Half Size 651 | 3. Third Size 652 | 4. Quarter Size 653 | width (int): Width of result when doing a Custom preview 654 | (defaults to pref) 655 | height (int): Height of result when doing a Custom preview 656 | (defaults to pref) 657 | keep_aspect (bool): Maintains the frame aspect when doing a Custom 658 | preview. Defaults to Preview prefs setting. 659 | step_render (bool): Render only 1 out of every X frames ("shoot on 660 | X frames") or render every frame. Defaults to False. 661 | steps (int): If step rendering, how many to step. Default 5. 662 | use_network (bool): Enables rendering with the network. 663 | Defaults to False. 664 | groups (str): Use these network slave groups to render on (when 665 | net rendering). Default "all". 666 | flags (number): Number specifying render flags, usually 0 667 | (the default). Most flags are specified by other means, but a 668 | value of 262144 is used for preview renders. 669 | tool (Tool): A tool to render up to. If this is specified only 670 | sections of the comp up to this tool will be rendered. eg you 671 | could specify comp.Saver1 to only render *up to* Saver1, 672 | ignoring any tools (including savers) after it. 673 | frame_range (str): Describes which frames to render. 674 | (eg "1..100,150..180"). Defaults to "Start".."End" 675 | 676 | Returns: 677 | True if the composition rendered successfully, None if it failed 678 | to start or complete. 679 | 680 | """ 681 | # convert our 'Pythonic' keyword arguments to Fusion's internal ones. 682 | conversion = {'start': 'Start', 683 | 'end': 'End', 684 | 'high_quality': 'HiQ', 685 | 'render_all': 'RenderAll', 686 | 'motion_blur': 'MotionBlur', 687 | 'size_type': 'SizeType', 688 | 'width': 'Width', 689 | 'height': 'Height', 690 | 'keep_aspect': 'KeepAspect', 691 | 'step_render': 'StepRender', 692 | 'steps': 'Steps', 693 | 'use_network': 'UseNetwork', 694 | 'groups': 'Groups', 695 | 'flags': 'Flags ', 696 | 'tool': 'Tool ', 697 | 'frame_range': 'FrameRange'} 698 | for key, new_key in conversion.iteritems(): 699 | if key in kwargs: 700 | value = kwargs.pop(key) 701 | kwargs[new_key] = value 702 | 703 | # use our required argument 704 | required_kwargs = {'Wait': wait_for_render} 705 | kwargs.update(required_kwargs) 706 | 707 | return self._reference.Render(**kwargs) 708 | 709 | def render_range(self, wait_to_render, start, end, steps=1, **kwargs): 710 | """ A render that specifies an explicit render range. 711 | 712 | Args: 713 | wait_for_render (bool): Whether the script should wait for the render to complete or continue processing 714 | once the render has begun. Defaults to False 715 | start (int): The frame to start rendering at. 716 | end (int): The frame to stop rendering at. 717 | steps (int): If step rendering, how many to step. Default 1. 718 | 719 | Kwargs: 720 | See `Comp.render()` method. 721 | 722 | Returns: 723 | True if the composition rendered successfully, None if it failed to start or complete. 724 | """ 725 | range_kwargs = {'start': start, 726 | 'end': end, 727 | 'steps': steps} 728 | kwargs.update(range_kwargs) 729 | 730 | return self.render(wait_to_render=wait_to_render, **kwargs) 731 | 732 | def run_script(self, filename): 733 | """ Run a script within the composition's script context 734 | 735 | Use this function to run a script in the composition environment. 736 | This is similar to launching a script from the comp's Scripts menu. 737 | 738 | The script will be started with 'fusion' and 'composition' variables set to the Fusion and currently active 739 | Composition objects. The filename given may be fully specified, or may be relative to the comp's Scripts: path. 740 | 741 | Since version 6.3.2 you can run Python and eyeonScripts. 742 | Fusion supports .py .py2 and .py3 extensions to differentiate python script versions. 743 | 744 | Arguments: 745 | filename (str): The filename of a script to be run in the 746 | composition environment. 747 | 748 | """ 749 | self._reference.RunScript(filename) 750 | 751 | def is_rendering(self): 752 | """ Returns True if the comp is busy rendering. 753 | 754 | Use this method to determine whether a composition object is currently rendering. 755 | It will return True if it is playing, rendering, or just rendering a tool after trying to view it. 756 | 757 | Returns: 758 | bool: Whether composition is currently rendering 759 | 760 | """ 761 | return self._reference.IsRendering() 762 | 763 | def is_playing(self): 764 | """ Returns True if the comp is being played. 765 | 766 | Use this method to determine whether a composition object is currently playing. 767 | 768 | Returns: 769 | bool: Whether comp is currently being played. 770 | 771 | """ 772 | return self._reference.IsPlaying() 773 | 774 | def is_locked(self): 775 | """ Returns True if the comp is locked. 776 | 777 | Use this method to see whether a composition is locked or not. 778 | 779 | Returns: 780 | bool: Whether comp is currently locked. 781 | 782 | """ 783 | return self._reference.IsPlaying() 784 | 785 | def filename(self): 786 | """Return the current file path of the composition. 787 | 788 | Returns: 789 | str: Full path to current comp. (empty string if not saved yet) 790 | 791 | """ 792 | return self._reference.GetAttrs()['COMPS_FileName'] 793 | 794 | def __repr__(self): 795 | return '{0}("{1}")'.format(self.__class__.__name__, self.filename()) 796 | 797 | 798 | class Tool(PyObject): 799 | """A Tool is a single operator/node in your composition. 800 | 801 | You can use this object to perform changes to a single tool, make 802 | connections with another or query information. For example renaming, 803 | deleting, connecting and retrieving its inputs/outputs. 804 | 805 | """ 806 | 807 | def get_pos(self): 808 | """Return the X and Y position of this tool in the FlowView. 809 | 810 | Returns: 811 | list(float, float): The X and Y coordinate of the tool. 812 | 813 | """ 814 | flow = self.comp().current_frame().FlowView 815 | # possible optimization: self._reference.Comp.CurrentFrame.FlowView 816 | return flow.GetPosTable(self._reference).values() 817 | 818 | def set_pos(self, pos): 819 | """Reposition this tool. 820 | 821 | Arguments: 822 | pos (list(float, float)): The X and Y coordinate to apply. 823 | 824 | Returns: 825 | None 826 | 827 | """ 828 | flow = self.comp().current_frame().FlowView 829 | # possible optimization: self._reference.Comp.CurrentFrame.FlowView 830 | flow.SetPos(self._reference, *pos) 831 | 832 | # region inputs 833 | def main_input(self, index): 834 | """Returns the main (visible) Input knob of the tool, by index. 835 | 836 | Note: 837 | The index starts at 1! 838 | 839 | Arguments: 840 | index (int): numerical index of the knob (starts at 1) 841 | 842 | Returns: 843 | Input: input that the given index. 844 | 845 | """ 846 | return Input(self._reference.FindMainInput(index)) 847 | 848 | def input(self, id): 849 | """Returns an Input by ID. 850 | 851 | Arguments: 852 | id (str): ID name of the input. 853 | 854 | Returns: 855 | Input: input at the given index. 856 | 857 | """ 858 | return Input(self._reference[id]) 859 | 860 | def inputs(self): 861 | """Return all Inputs of this Tools 862 | 863 | Returns: 864 | list: inputs of the tool. 865 | 866 | """ 867 | return [Input(x) for x in self._reference.GetInputList().values()] 868 | # endregion 869 | 870 | # region outputs 871 | def main_output(self, index): 872 | """ Returns the main (visible) Output knob of the tool, by index. 873 | 874 | Note: 875 | The index starts at 1! 876 | 877 | Arguments: 878 | index (int): numerical index of the knob (starts at 1) 879 | 880 | Returns: 881 | Output: output that the given index. 882 | 883 | """ 884 | return Output(self._reference.FindMainOutput(index)) 885 | 886 | def output(self, id): 887 | """ Returns the Output knob by ID. 888 | 889 | Arguments: 890 | id (str): ID name of the output. 891 | 892 | Returns: 893 | Output: The resulting output. 894 | 895 | """ 896 | # TODO: Optimize: there must be a more optimized way than this. 897 | output_reference = next(x for x in self.outputs() if x.ID == id) 898 | if not output_reference: 899 | return None 900 | else: 901 | return Output(output_reference) 902 | 903 | def outputs(self): 904 | """ Return all Outputs of this Tools """ 905 | return [Output(x) for x in self._reference.GetOutputList().values()] 906 | # endregion 907 | 908 | # region connections 909 | def connect_main(self, tool, from_index=1, to_index=1): 910 | """Helper function that quickly connects main outputs to another 911 | tool's main inputs. 912 | 913 | Arguments: 914 | tool (Tool): The other tool to connect to. 915 | from_index (int): Index of main output on this instance. 916 | (index start at 1) 917 | to_index (int): Index of main input on the other tool. 918 | (index start at 1) 919 | 920 | Returns: 921 | None 922 | 923 | """ 924 | if not isinstance(tool, Tool): 925 | tool = Tool(tool) 926 | 927 | id = tool._reference.FindMainInput(1).ID 928 | tool._reference[id] = self._reference.FindMainOutput(1) # connect 929 | 930 | def disconnect(self, inputs=True, outputs=True): 931 | """Disconnect all inputs and outputs of this tool. 932 | 933 | Arguments: 934 | inputs (bool): Whether to disconnect all inputs 935 | outputs (bool): Whether to disconnect all outputs 936 | 937 | Returns: 938 | None 939 | 940 | """ 941 | if inputs: 942 | for input in self.inputs(): 943 | input.disconnect() 944 | 945 | if outputs: 946 | for output in self.outputs(): 947 | output.disconnect() 948 | 949 | def connections_iter(self, inputs=True, outputs=True): 950 | """Yield each Input and Output connection for this Tool instance. 951 | 952 | Each individual connection is yielded in the format: `(Output, Input)` 953 | 954 | Arguments: 955 | inputs (bool): Whether to include inputs of this Tool instance. 956 | outputs (bool): Whether to include outputs of this Tool instance. 957 | 958 | Yields: 959 | (Output, Input): representing a connection to or from this Tool. 960 | 961 | """ 962 | if inputs: 963 | for input in self.inputs(): 964 | connected_output = input.get_connected_output() 965 | if connected_output: 966 | yield (connected_output, input) 967 | 968 | if outputs: 969 | for output in self.outputs(): 970 | connected_inputs = output.get_connected_inputs() 971 | if connected_inputs: 972 | for connected_input in connected_inputs: 973 | yield (output, connected_input) 974 | 975 | def connections(self, inputs=True, outputs=True): 976 | """Return all Input and Output connections of this Tools. 977 | 978 | Each individual connection is a 2-tuple in the list in the format: 979 | `(Output, Input)` 980 | 981 | For example: 982 | `[(Output, Input), (Output, Input), (Output, Input)]` 983 | 984 | Args: 985 | inputs (bool): If True include the inputs of this Tool, else they 986 | are excluded. 987 | outputs (bool): If True include the outputs of this Tool, else they 988 | are excluded. 989 | 990 | Returns: 991 | A list of 2-tuples (Output, Input) representing each connection to 992 | or from this Tool. 993 | """ 994 | return list(self.connections_iter(inputs=inputs, outputs=outputs)) 995 | # endregion 996 | 997 | def rename(self, name): 998 | """Sets the name for this Tool to `name`. 999 | 1000 | Args: 1001 | name (str): The new name to change to. 1002 | 1003 | """ 1004 | self._reference.SetAttrs({'TOOLB_NameSet': True, 'TOOLS_Name': name}) 1005 | 1006 | def clear_name(self): 1007 | """Clears user-defined name reverting to automated internal name.""" 1008 | self._reference.SetAttrs({'TOOLB_NameSet': False, 'TOOLS_Name': ''}) 1009 | 1010 | def delete(self): 1011 | """Removes the tool from the composition. 1012 | 1013 | .. note:: 1014 | This also releases the handle to the Fusion Tool object, 1015 | setting it to None. As such it invalidates this Tool instance. 1016 | 1017 | """ 1018 | self._reference.Delete() 1019 | 1020 | def refresh(self): 1021 | """Refreshes the tool, showing updated user controls. 1022 | 1023 | .. note:: 1024 | Internally calling Refresh in Fusion will invalidate the handle to 1025 | internal object this tool references. You'd have to save the new 1026 | handle that is returned (even though the documentation says nothing 1027 | is returned). Calling this function on this Tool will invalidate 1028 | other Tool instances referencing this same object. But it will 1029 | update the reference in this instance on which the function call 1030 | is made. 1031 | 1032 | """ 1033 | new_ref = self._reference.Refresh() 1034 | self._reference = new_ref 1035 | 1036 | def parent(self): 1037 | """Return the parent Group this Tool belongs to, if any.""" 1038 | return self._reference.ParentTool 1039 | 1040 | def save_settings(self, path=None): 1041 | """Saves the tool's settings to a dict, or to a .setting file 1042 | specified by the path argument. 1043 | 1044 | Arguments: 1045 | path (str): The path to save the .setting file. 1046 | 1047 | Returns: 1048 | bool/dict: If a path is given, the tool's settings will be saved 1049 | to that file, and a boolean is returned to indicate success. 1050 | If no path is given, SaveSettings() will return a table of the 1051 | tool's settings instead. 1052 | 1053 | """ 1054 | args = tuple() if path is None else (path,) 1055 | return self._reference.SaveSettings(*args) 1056 | 1057 | def load_settings(self, settings): 1058 | """Loads .setting files or settings dict into the tool. 1059 | 1060 | This is potentially useful for any number of applications, such as 1061 | loading curve data into fusion, for which there is currently no simple 1062 | way to script interactively in Fusion. Beyond that, it could possibly 1063 | be used to sync updates to tools over project management systems. 1064 | 1065 | Args: 1066 | settings (str, dict): The path to a valid .setting file or a 1067 | settings dict. A valid dict of settings, such as produced by 1068 | SaveSettings() or read from a .setting file. 1069 | 1070 | Returns: 1071 | None 1072 | 1073 | """ 1074 | self._reference.LoadSettings(settings) 1075 | 1076 | def comp(self): 1077 | """ Return the Comp this Tool associated with. """ 1078 | return Comp(self._reference.Composition) 1079 | 1080 | def get_text_color(self): 1081 | """Gets the Tool's text color. 1082 | 1083 | Returns: 1084 | dict: The Tool's current text color. 1085 | 1086 | """ 1087 | return self._reference.TextColor 1088 | 1089 | def set_text_color(self, color): 1090 | """ Sets the Tool's text color. 1091 | 1092 | Color should be assigned as a dictionary holding the RGB values between 0-1, like: 1093 | {"R": 1, "G": 0, "B": 0} 1094 | 1095 | Example 1096 | >>> tool.set_text_color({'R':0.5, 'G':0.1, 'B': 0.0}) 1097 | >>> tool.set_text_color(None) 1098 | 1099 | """ 1100 | self._reference.TextColor = color 1101 | 1102 | def get_tile_color(self): 1103 | """ Gets the Tool's tile color. 1104 | 1105 | Returns: 1106 | dict: The Tool's current tile color. 1107 | """ 1108 | return self._reference.TileColor 1109 | 1110 | def set_tile_color(self, color): 1111 | """ Sets the Tool's tile color. 1112 | 1113 | Example 1114 | >>> tool.set_tile_color({'R':0.5, 'G':0.1, 'B': 0.0}) 1115 | >>> tool.set_tile_color(None) # reset 1116 | 1117 | """ 1118 | self._reference.TileColor = color 1119 | 1120 | def get_keyframes(self): 1121 | """Return a list of keyframe times, in order, for the tool only. 1122 | 1123 | These are NOT the keyframes on Inputs of this tool! 1124 | Any animation splines or modifiers attached to the tool's inputs are 1125 | not considered. 1126 | 1127 | .. note:: 1128 | Most Tools will return only the start and end of their valid 1129 | region. Certain types of tools and modifiers such as BezierSplines 1130 | may return a longer list of keyframes. 1131 | 1132 | Returns: 1133 | list: List of int values indicating frames. 1134 | 1135 | """ 1136 | keyframes = self._reference.GetKeyFrames() 1137 | if keyframes: 1138 | return keyframes.values() 1139 | else: 1140 | return None 1141 | 1142 | def __eq__(self, other): 1143 | if isinstance(other, Tool): 1144 | self.name() == other.name() 1145 | return False 1146 | 1147 | def __hash__(self): 1148 | return hash(self.name()) 1149 | 1150 | 1151 | class Flow(PyObject): 1152 | """The Flow is the node-based overview of you Composition. 1153 | 1154 | Fusion's internal name: `FlowView` 1155 | 1156 | """ 1157 | 1158 | def set_pos(self, tool, pos): 1159 | """Reposition the given Tool to the position in the FlowView. 1160 | 1161 | Args: 1162 | tool (Tool): The tool to reposition in the FlowView. 1163 | pos (tuple): The x and y co-ordinates to apply. 1164 | 1165 | Returns: 1166 | None 1167 | 1168 | """ 1169 | 1170 | if not isinstance(tool, Tool): 1171 | tool = Tool(tool) 1172 | 1173 | self._reference.SetPos(tool._reference, pos[0], pos[1]) 1174 | 1175 | def get_pos(self, tool): 1176 | """return the X and Y position of a tool's tile in the FlowView. 1177 | 1178 | Args: 1179 | tool (Tool): The tool to return the position of. 1180 | 1181 | Returns: 1182 | tuple: The x and y co-ordinates of the tool. 1183 | 1184 | """ 1185 | if not isinstance(tool, Tool): 1186 | tool = Tool(tool) 1187 | 1188 | return self._reference.GetPos(tool._reference) 1189 | 1190 | def queue_set_pos(self, tool, pos): 1191 | """ Queues the moving of a tool to a new position. 1192 | 1193 | This function improves performance if you want to move a lot of tools 1194 | at the same time. For big graphs and loops this is preferred over 1195 | `set_pos` and `get_pos`. 1196 | 1197 | Added in Fusion 6.1: FlowView::QueueSetPos() 1198 | 1199 | Example 1200 | >>> c = Comp() 1201 | >>> tools = c.get_selected_tools() 1202 | >>> flow = c.flow() 1203 | >>> for i, tool in enumerate(tools): 1204 | >>> pos = [i, 0] 1205 | >>> flow.queue_set_pos(tool, pos) 1206 | >>> flow.flush_set_pos() # here the tools are actually moved 1207 | """ 1208 | return self._reference.QueueSetPos(tool._reference, pos[0], pos[1]) 1209 | 1210 | def flush_set_pos_queue(self): 1211 | """Moves all tools queued with `queue_set_pos`. 1212 | 1213 | This function improves performance if you want to move a lot of tools 1214 | at the same time. For big graphs and loops this is preferred over 1215 | `set_pos` and `get_pos`. 1216 | 1217 | Added in Fusion 6.1: FlowView::FlushSetPosQueue() 1218 | 1219 | Returns: 1220 | None 1221 | 1222 | """ 1223 | return self._reference.FlushSetPosQueue() 1224 | 1225 | def get_scale(self): 1226 | """Returns the current scale of the FlowView. 1227 | 1228 | Returns: 1229 | float: value indicating the current scale of the FlowView. 1230 | 1231 | """ 1232 | return self._reference.GetScale() 1233 | 1234 | def set_scale(self, scale): 1235 | """ Rescales the FlowView to the amount specified. 1236 | 1237 | A value of 1 for the scale argument would set the FlowView to 100%. 1238 | While a value of 0.1 would set it to 10% of the default scale. 1239 | 1240 | Args: 1241 | scale (float): The scaling to apply to this FlowView. 1242 | 1243 | """ 1244 | return self._reference.SetScale(scale) 1245 | 1246 | def frame_all(self): 1247 | """Frames all tools so they fit in the view. 1248 | 1249 | This function will rescale and reposition the FlowView to contain all 1250 | tools. 1251 | 1252 | Returns: 1253 | None 1254 | 1255 | """ 1256 | self._reference.FrameAll() 1257 | 1258 | def select(self, tool=None, state=True): 1259 | """Select, deselect or clear the selection of Tools in this Flow. 1260 | 1261 | This function will add or remove the tool specified in it's first 1262 | argument from the current tool selection set. The second argument 1263 | should be set to False to remove the tool from the selection, or to 1264 | True to add it. If called with no arguments, the function will clear 1265 | all tools from the current selection. 1266 | 1267 | Args: 1268 | tool (Tool): The tool to add or remove. 1269 | state (bool): When False the tools will be removed from selection, 1270 | otherwise the tools will ba added to the current selection. 1271 | 1272 | Returns: 1273 | None 1274 | 1275 | """ 1276 | if tool is None: 1277 | return self._reference.Select() # clear selection 1278 | elif not isinstance(tool, Tool): 1279 | tool = Tool(tool) 1280 | 1281 | self._reference.Select(tool._reference, state) 1282 | 1283 | 1284 | class Link(PyObject): 1285 | """The Link is the base class for Fusion's Input and Output types""" 1286 | 1287 | def tool(self): 1288 | """ Return the Tool this Link belongs to """ 1289 | return Tool(self._reference.GetTool()) 1290 | 1291 | 1292 | class Input(Link): 1293 | """An Input is any attribute that can be set or connected to by the user 1294 | on the incoming side of a tool. 1295 | 1296 | .. note:: 1297 | These are the input knobs in the Flow view, but also the input values 1298 | inside the Control view for a Tool. 1299 | 1300 | Because of the way node-graphs work any value that goes into a Tool 1301 | required to process the information should result (in most scenarios) in a 1302 | reproducible output under the same conditions. 1303 | 1304 | """ 1305 | 1306 | def __current_time(self): 1307 | # optimize over going through PyNodes (??) 1308 | # instead of: time = self.tool().comp().get_current_time() 1309 | return self._reference.GetTool().Composition.CurrentTime 1310 | 1311 | def get_value(self, time=None): 1312 | """Get the value of this Input at the given time. 1313 | 1314 | Arguments: 1315 | time (float): The time to set the value at. If None provided the 1316 | current time is used. 1317 | 1318 | Returns: 1319 | A value directly from the internal input object. 1320 | 1321 | """ 1322 | if time is None: 1323 | time = self.__current_time() 1324 | 1325 | return self._reference[time] 1326 | 1327 | def set_value(self, value, time=None): 1328 | """Set the value of the input at the given time. 1329 | 1330 | When an attribute is an enum type it will try to perform a correct 1331 | conversion when the Input requires a float value and a string was 1332 | given. Similarly when a float was given and a string id would be 1333 | required it will peform a correct conversion. 1334 | 1335 | This also allows settings checkboxes and alike using a boolean value 1336 | instead of requiring an integer or float input value. (This will 1337 | convert it as required by the input.) 1338 | 1339 | Arguments: 1340 | time (float): The time to set the value at. If None provided the 1341 | currentt time is used. 1342 | 1343 | """ 1344 | 1345 | if time is None: 1346 | time = self.__current_time() 1347 | 1348 | attrs = self.get_attrs() 1349 | data_type = attrs['INPS_DataType'] 1350 | 1351 | # Setting boolean values doesn't work. So instead set an integer value 1352 | # allow settings checkboxes with True/False 1353 | if isinstance(value, bool): 1354 | value = int(value) 1355 | 1356 | # Convert float/integer to enum if datatype == "FuID" 1357 | elif isinstance(value, (int, float)) and data_type == "FuID": 1358 | 1359 | # We must compare it with a float value. We add 1 to interpret 1360 | # as zero based indices. (Zero would be 1.0 in the fusion id 1361 | # dictionary, etc.) 1362 | v = float(value) + 1.0 1363 | 1364 | enum_keys = ("INPIDT_MultiButtonControl_ID", 1365 | "INPIDT_ComboControl_ID") 1366 | for enum_key in enum_keys: 1367 | if enum_key in attrs: 1368 | enum = attrs[enum_key] 1369 | if v in enum: 1370 | value = enum[v] 1371 | break 1372 | 1373 | # Convert enum string value to its corresponding integer value 1374 | elif (isinstance(value, basestring) and 1375 | data_type != "Text" and 1376 | data_type != "FuID"): 1377 | 1378 | enum_keys = ("INPST_MultiButtonControl_String", 1379 | "INPIDT_MultiButtonControl_ID", 1380 | "INPIDT_ComboControl_ID") 1381 | for enum_key in enum_keys: 1382 | if enum_key in attrs: 1383 | enum = dict((str(key), value) for value, key in 1384 | attrs[enum_key].items()) 1385 | if value in enum: 1386 | value = enum[str(value)] - 1.0 1387 | break 1388 | 1389 | self._reference[time] = value 1390 | 1391 | def connect_to(self, output): 1392 | """Connect an Output as incoming connection to this Input. 1393 | 1394 | .. note:: 1395 | This function behaves similarly to right clicking on a property, 1396 | selecting Connect To, and selecting the property you wish to 1397 | connect the input to. In that respect, if you try to connect 1398 | non-similar data types (a path's value to a polygon's level, for 1399 | instance) it will not connect the values. Such an action will 1400 | yield NO error message. 1401 | 1402 | Args: 1403 | output (Output): The output that should act as incoming connection. 1404 | 1405 | Returns: 1406 | None 1407 | 1408 | """ 1409 | 1410 | # disconnect 1411 | if output is None: 1412 | self._reference.ConnectTo(None) 1413 | return 1414 | 1415 | # or connect 1416 | if not isinstance(output, Output): 1417 | output = Output(output) 1418 | 1419 | self._reference.ConnectTo(output._reference) 1420 | 1421 | def disconnect(self): 1422 | """Disconnect the Output this Input is connected to, if any.""" 1423 | self.connect_to(None) 1424 | 1425 | def get_connected_output(self): 1426 | """ Returns the output that is connected to a given input. 1427 | 1428 | Returns: 1429 | Output: The Output this Input is connected to if any, else None. 1430 | 1431 | """ 1432 | other = self._reference.GetConnectedOutput() 1433 | if other: 1434 | return Output(other) 1435 | 1436 | def get_expression(self): 1437 | """Return the expression string shown in the Input's Expression field. 1438 | 1439 | Returns: 1440 | str: the simple expression string from a given input if any else 1441 | an empty string is returned. 1442 | 1443 | """ 1444 | return self._reference.GetExpression() 1445 | 1446 | def set_expression(self, expression): 1447 | """Set the Expression field for the Input to the given string. 1448 | 1449 | Args: 1450 | expression (str): An expression string. 1451 | 1452 | """ 1453 | self._reference.SetExpression(expression) 1454 | 1455 | def get_keyframes(self): 1456 | """Return the times at which this Input has keys. 1457 | 1458 | Returns: 1459 | list: List of int values indicating frames. 1460 | """ 1461 | keyframes = self._reference.GetKeyFrames() 1462 | if keyframes: 1463 | return keyframes.values() 1464 | else: 1465 | return None 1466 | 1467 | def remove_keyframes(self, time=None, index=None): 1468 | """Remove the keyframes on this Input (if any) 1469 | 1470 | """ 1471 | # TODO: Implement Input.remove_keyframes() 1472 | raise NotImplementedError() 1473 | 1474 | def is_connected(self): 1475 | """Return whether the Input is an incoming connection from an Output 1476 | 1477 | Returns: 1478 | bool: True if connected, otherwise False 1479 | 1480 | """ 1481 | return bool(self._reference.GetConnectedOutput()) 1482 | 1483 | def data_type(self): 1484 | """Returns the type of Parameter 1485 | 1486 | For example the (Number, Point, Text, Image) types this Input accepts. 1487 | 1488 | Returns: 1489 | str: Type of parameter. 1490 | 1491 | """ 1492 | return self._reference.GetAttrs()['OUTS_DataType'] 1493 | 1494 | # TODO: implement `Input.WindowControlsVisible` 1495 | # TODO: implement `Input.HideWindowControls` 1496 | # TODO: implement `Input.ViewControlsVisible` 1497 | # TODO: implement `Input.HideViewControls` 1498 | 1499 | 1500 | class Output(Link): 1501 | """Output of a Tool. 1502 | 1503 | An Output is any attributes that is a result from a Tool that can be 1504 | connected as input to another Tool. 1505 | 1506 | .. note:: These are the output knobs in the Flow view. 1507 | 1508 | """ 1509 | 1510 | def get_value(self, time=None): 1511 | """Return the value of this Output at the given time. 1512 | 1513 | If time is provided the value is evaluated at that specific time, 1514 | otherwise current time is used. 1515 | 1516 | Args: 1517 | time (float): Time at which to evaluate the Output. 1518 | If None provided current time will be used. 1519 | 1520 | Returns: 1521 | The value of the output at the given time. 1522 | 1523 | """ 1524 | return self.get_value_attrs(time=time)[0] 1525 | 1526 | def get_value_attrs(self, time=None): 1527 | """Returns a tuple of value and attrs for this Output. 1528 | 1529 | `value` may be None, or a variety of different types: 1530 | 1531 | Number - returns a number 1532 | Point - returns a table with X and Y members 1533 | Text - returns a string 1534 | Clip - returns the filename string 1535 | Image - returns an Image object 1536 | 1537 | `attrs` is a dictionary with the following entries: 1538 | 1539 | Valid - table with numeric Start and End entries 1540 | DataType - string ID for the parameter type 1541 | TimeCost - time taken to render this parameter 1542 | 1543 | Args: 1544 | time (float): Time at which to evaluate the Output. 1545 | If None provided current time will be used. 1546 | 1547 | Returns: 1548 | tuple: Value and attributes of this output at the given time. 1549 | 1550 | """ 1551 | if time is None: 1552 | # optimize over going through PyNodes (??) 1553 | time = self._reference.GetTool().Composition.CurrentTime 1554 | # time = self.tool().comp().get_current_time() 1555 | 1556 | return self._reference.GetValue(time) 1557 | 1558 | def get_time_cost(self, time=None): 1559 | """ Return the time taken to render this parameter at the given time. 1560 | 1561 | .. note:: This will evaluate the output and could be computationally 1562 | expensive. 1563 | 1564 | Args: 1565 | time (float): Time at which to evaluate the Output. 1566 | If None provided current time will be used. 1567 | 1568 | Returns: 1569 | float: Time taken to render this Output. 1570 | 1571 | """ 1572 | return self.get_value_attrs(time=time)[1]['TimeCost'] 1573 | 1574 | def disconnect(self, inputs=None): 1575 | """Disconnect Inputs this Output is connected to. 1576 | 1577 | Args: 1578 | inputs (list or None): The inputs to disconnet or all of the 1579 | current connections if None is provided. 1580 | 1581 | """ 1582 | 1583 | if inputs is None: # disconnect all (if any) 1584 | inputs = self.get_connected_inputs() 1585 | else: # disconnect a subset of the connections (if valid) 1586 | # ensure iterable 1587 | if not isinstance(inputs, (list, tuple)): 1588 | inputs = [inputs] 1589 | 1590 | # ensure all are Inputs 1591 | inputs = [Input(input) for input in inputs] 1592 | 1593 | # ensure Inputs are connected to this output 1594 | connected_inputs = set(self._reference.GetConnectedInputs().values()) 1595 | inputs = [input for input in inputs if input._reference in connected_inputs] 1596 | 1597 | for input in inputs: 1598 | input._reference.ConnectTo(None) 1599 | 1600 | def get_connected_inputs(self): 1601 | """ Returns a list of all Inputs that are connected to this Output. 1602 | 1603 | :return: List of Inputs connected to this Output. 1604 | :rtype: list 1605 | """ 1606 | return [Input(x) for x in self._reference.GetConnectedInputs().values()] 1607 | 1608 | def get_dod(self): 1609 | """Returns the Domain of Definition for this output. 1610 | 1611 | Returns: 1612 | [int, int, int, int]: The domain of definition for this output in 1613 | the as list of integers ordered: left, bottom, right, top. 1614 | """ 1615 | return self._reference.GetDoD() 1616 | 1617 | # region connections 1618 | def connect_to(self, input): 1619 | """Connect this Output to another Input. 1620 | 1621 | This connection gains an outgoing connection for this tool. 1622 | 1623 | .. note:: 1624 | This function behaves similarly to right clicking on a 1625 | property, selecting Connect To, and selecting 1626 | the property you wish to connect the input to. In that 1627 | respect, if you try to connect non-similar 1628 | data types (a path's value to a polygon's level, 1629 | for instance) it will not connect the values. 1630 | Such an action will yield NO error message. 1631 | 1632 | Args: 1633 | input (Input): The input to connect to. 1634 | 1635 | """ 1636 | if not isinstance(input, Input): 1637 | input = Input(input) 1638 | 1639 | input.connect_to(self) 1640 | 1641 | def is_connected(self): 1642 | """Return whether the Output has any outgoing connection to any Inputs. 1643 | 1644 | Returns: 1645 | bool: True if connected, otherwise False 1646 | 1647 | """ 1648 | return any(self._reference.GetConnectedInputs().values()) 1649 | 1650 | def data_type(self): 1651 | """Returns the type of Parameter (e.g. Number, Point, Text, Image) 1652 | this Output accepts. 1653 | 1654 | Returns: 1655 | str: Type of parameter. 1656 | 1657 | """ 1658 | return self._reference.GetAttrs()['INPS_DataType'] 1659 | 1660 | # TODO: implement `Output.GetValueMemBlock` Retrieve the Output's value as a MemBlock 1661 | # TODO: implement `Output.EnableDiskCache` Controls disk-based caching 1662 | # TODO: implement `Output.ClearDiskCache` Clears frames from the disk cache 1663 | # TODO: implement `Output.ShowDiskCacheDlg` Displays the Cache-To-Disk dialog for user interaction 1664 | 1665 | 1666 | class Parameter(PyObject): 1667 | """ Base class for all parameter (values) types """ 1668 | pass 1669 | 1670 | 1671 | class Image(Parameter): 1672 | """ An Image parameter object. 1673 | 1674 | For example the Image output from a Tool. 1675 | """ 1676 | def width(self): 1677 | """ Return the width in pixels for the current output, this could be for the current proxy resolution. 1678 | :return: Actual horizontal size, in pixels 1679 | """ 1680 | return self._reference.Width 1681 | 1682 | def height(self): 1683 | """ Return the height in pixels for the current output, this could be for the current proxy resolution. 1684 | :return: Actual horizontal size, in pixels 1685 | """ 1686 | return self._reference.Height 1687 | 1688 | def original_width(self): 1689 | """ 1690 | :return: Unproxied horizontal size, in pixels. 1691 | """ 1692 | return self._reference.OriginalWidth 1693 | 1694 | def original_height(self): 1695 | """ 1696 | :return: Unproxied vertical size, in pixels. 1697 | """ 1698 | return self._reference.OriginalHeight 1699 | 1700 | def depth(self): 1701 | """ Image depth indicator (not in bits) 1702 | :return: Image depth 1703 | """ 1704 | return self._reference.Depth 1705 | 1706 | def x_scale(self): 1707 | """ 1708 | :return: Pixel X Aspect 1709 | """ 1710 | return self._reference.XScale 1711 | 1712 | def y_scale(self): 1713 | """ 1714 | :return: Pixel Y Aspect 1715 | """ 1716 | return self._reference.YScale 1717 | 1718 | def x_offset(self): 1719 | """Returns x-offset in pixels 1720 | 1721 | Returns: 1722 | int: X Offset, in pixels 1723 | """ 1724 | return self._reference.XOffset 1725 | 1726 | def y_offset(self): 1727 | """Returns y-offset in pixels 1728 | 1729 | Returns: 1730 | int: Y Offset, in pixels 1731 | """ 1732 | return self._reference.YOffset 1733 | 1734 | def field(self): 1735 | """Returns field indicator. 1736 | 1737 | Returns: 1738 | int: Field indicator 1739 | """ 1740 | return self._reference.Field 1741 | 1742 | def proxy_scale(self): 1743 | """Returns image proxy scale multiplier. 1744 | 1745 | Returns: 1746 | float: Image proxy scale multiplier. 1747 | """ 1748 | return self._reference.ProxyScale 1749 | 1750 | 1751 | class TransformMatrix(Parameter): 1752 | pass 1753 | 1754 | 1755 | class Fusion(PyObject): 1756 | """The Fusion application. 1757 | 1758 | Contains all functionality to interact with the global Fusion sessions. 1759 | For example this would allow you to retrieve the available compositions 1760 | that are currently open or open a new one. 1761 | """ 1762 | # TODO: Implement Fusion methods: http://www.steakunderwater.com/VFXPedia/96.0.243.189/index5522.html?title=Eyeon:Script/Reference/Applications/Fusion/Classes/Fusion 1763 | 1764 | @staticmethod 1765 | def _default_reference(): 1766 | """Fallback for the default reference""" 1767 | 1768 | # this would be accessible within Fusion as "fusion" in a script 1769 | ref = getattr(sys.modules["__main__"], "fusion", None) 1770 | if ref is not None: 1771 | return ref 1772 | 1773 | # this would be accessible within Fusion's console 1774 | ref = globals().get("fusion", None) 1775 | if ref is not None: 1776 | return ref 1777 | 1778 | def new_comp(self): 1779 | """Creates a new composition and sets it as the currently active one""" 1780 | # TODO: Need fix: During NewComp() Fusion seems to be temporarily unavailable 1781 | self._reference.NewComp() 1782 | comp = self._reference.GetCurrentComp() 1783 | return Comp(comp) 1784 | 1785 | def get_current_comp(self): 1786 | """Return the currently active comp in this Fusion instance""" 1787 | comp = self._reference.GetCurrentComp() 1788 | return Comp(comp) 1789 | 1790 | @property 1791 | def build(self): 1792 | """Returns the build number of the current Fusion instance. 1793 | 1794 | Returns: 1795 | float: Build number 1796 | 1797 | """ 1798 | return self._reference.Build 1799 | 1800 | @property 1801 | def version(self): 1802 | """Returns the version of the current Fusion instance. 1803 | 1804 | Returns: 1805 | float: Version number 1806 | 1807 | """ 1808 | return self._reference.Version 1809 | 1810 | def __repr__(self): 1811 | return '{0}("{1}")'.format(self.__class__.__name__, 1812 | str(self._reference)) 1813 | 1814 | 1815 | class AskUserDialog(object): 1816 | """Dialog to ask users a question through a prompt gui. 1817 | 1818 | Example 1819 | >>> dialog = AskUserDialog("Question") 1820 | >>> dialog.add_message("Simple message") 1821 | >>> dialog.add_position("Center", default=(0.2, 0.8)) 1822 | >>> dialog.add_position("Size") 1823 | >>> result = dialog.show() 1824 | 1825 | """ 1826 | 1827 | # Conversion from nice name attributes to Fusion named attributes 1828 | CONVERSION = { 1829 | "name": "Name", # (str) All controls 1830 | "default": "Default", # (numeric) Checkbox, Dropdown, Multibutton 1831 | "min": "Min", # (numeric) Numeric controls 1832 | "max": "Max", # (numeric) Numeric controls 1833 | "precision": "DisplayedPrecision", # (int) Numeric controls 1834 | "integer": "Integer", # (bool) Numeric controls 1835 | "options": "Options", # (dict) Options table 1836 | "linear": "Linear", # (int) Text 1837 | "wrap": "Wrap", # (bool) Text 1838 | "read_only": "ReadOnly", # (bool) Text 1839 | "font_name": "FontName", # (str) Text 1840 | "font_size": "FontSize", # (float) Text 1841 | "save": "Save", # (bool) FileBrowse, PathBrowse, ClipBrowse 1842 | "low_name": "LowName", # (str) Slider 1843 | "high_name": "HighName", # (str) Slider 1844 | "num_across": "NumAcross" # (int) Checkbox 1845 | } 1846 | 1847 | def __init__(self, title=""): 1848 | self._items = list() 1849 | self.title = title 1850 | 1851 | def set_title(self, title): 1852 | self.title = title 1853 | 1854 | def _add(self, 1855 | type, 1856 | label, 1857 | **kwargs): 1858 | """Utility method for adding any type of control to the dialog""" 1859 | 1860 | item = { 1861 | 1: label, 1862 | 2: type 1863 | } 1864 | 1865 | # Add the optional keys (kwargs) and convert to the original Fusion 1866 | # names for the variables 1867 | for key, value in kwargs.items(): 1868 | if key in self.CONVERSION: 1869 | item[self.CONVERSION[key]] = value 1870 | else: 1871 | raise TypeError("Invalid argument for a Dialog control: " 1872 | "{0}".format(key)) 1873 | 1874 | self._items.append(item) 1875 | 1876 | def add_text(self, label, **kwargs): 1877 | self._add("Text", label, **kwargs) 1878 | 1879 | def add_file_browse(self, label, **kwargs): 1880 | self._add("FileBrowse", label, **kwargs) 1881 | 1882 | def add_path_browse(self, label, **kwargs): 1883 | self._add("PathBrowse", label, **kwargs) 1884 | 1885 | def add_clip_browse(self, label, **kwargs): 1886 | self._add("ClipBrowse", label, **kwargs) 1887 | 1888 | def add_slider(self, label, **kwargs): 1889 | self._add("Slider", label, **kwargs) 1890 | 1891 | def add_checkbox(self, label, **kwargs): 1892 | self._add("Checkbox", label, **kwargs) 1893 | 1894 | def add_position(self, label, **kwargs): 1895 | """Add a X & Y coordinaties control 1896 | 1897 | Displays a pair of edit boxes used to enter X & Y coordinates 1898 | for a center control or other position value. The default value 1899 | of this control is a table with two values, one for the X value and 1900 | one for the Y. The control returns a table of values. 1901 | 1902 | """ 1903 | 1904 | if "default" in kwargs: 1905 | default = kwargs["default"] 1906 | 1907 | # A tuple does not work as a default value for a position control 1908 | # so we convert it to a list to fix it. 1909 | if isinstance(default, tuple): 1910 | kwargs["default"] = list(default) 1911 | 1912 | self._add("Position", label, **kwargs) 1913 | 1914 | def add_screw(self, label, **kwargs): 1915 | """Add the standard Fusion thumbnail or screw control. 1916 | 1917 | This control is almost identical to a slider in almost all respects 1918 | except that its range is infinite, and so it is well suited for 1919 | angle controls and other values without practical limits. 1920 | 1921 | """ 1922 | self._add("Screw", label, **kwargs) 1923 | 1924 | def add_dropdown(self, label, **kwargs): 1925 | """Add a dropdown combobox. 1926 | 1927 | Displays the standard Fusion drop down menu for selecting from a 1928 | list of options. This control exposes and option call Options, 1929 | which takes a table containing the values for the drop down menu. 1930 | Note that the index for the Options table starts at 0, not 1 like is 1931 | common in most FusionScript tables. So, if you wish to set a default 1932 | for the first entry in a list, you would use Default=0, for the 1933 | second Default=1, and so on. 1934 | 1935 | """ 1936 | self._add("Dropdown", label, **kwargs) 1937 | 1938 | def add_multibutton(self, label, **kwargs): 1939 | """Add a multibutton. 1940 | 1941 | Displays a Multibutton, where each option is drawn as a button. 1942 | The same options are used like in a Dropdown. 1943 | 1944 | A set of buttons acting like a combobox (choice). 1945 | 1946 | """ 1947 | self._add("Multibutton", label, **kwargs) 1948 | 1949 | def show(self): 1950 | """Show the dialog 1951 | 1952 | Returns: 1953 | dict: The state of the controls in the UI 1954 | """ 1955 | data = dict((i, value) for i, value in enumerate(self._items)) 1956 | return Comp()._reference.AskUser(self.title, data) 1957 | 1958 | 1959 | class Registry(PyObject): 1960 | """Represents a Registry type of object within Fusion""" 1961 | pass 1962 | -------------------------------------------------------------------------------- /fusionless/standalone.py: -------------------------------------------------------------------------------- 1 | """Initialize a connection with Fusion from a standalone python prompt 2 | 3 | The standalone Python still needs access to the BlackmagicFusion library 4 | to initialize a connection. For pre-Fusion 8 versions you need PeyeonScript. 5 | 6 | Examples: 7 | 8 | # Create a saver in an open fusion in its currently active comp 9 | >>> app = get_fusion() 10 | >>> comp = app.get_current_comp() 11 | >>> saver = comp.create_tool("Saver") 12 | 13 | """ 14 | 15 | try: 16 | # Fusion 8.0+ (bmd) 17 | import BlackmagicFusion as bmd 18 | except ImportError: 19 | # Fusion pre-8.0 (eyeon) 20 | import PeyeonScript as bmd 21 | 22 | 23 | def _get_app(app, ip=None, timeout=0.1, uuid=None): 24 | """Get an application using the `scriptapp` method. 25 | 26 | Args: 27 | app (str): Name of the application, eg. "Fusion" 28 | ip (str): The IP address of the host to connect to. 29 | timeout (float): Timeout for connection in seconds. 30 | uuid (str): The UUID of the application to connect to. 31 | 32 | Returns: 33 | The application's raw pointer. 34 | 35 | """ 36 | 37 | if ip is None: 38 | ip = "127.0.0.1" # localhost 39 | 40 | args = [app, ip, timeout] 41 | if uuid: 42 | args.append(uuid) 43 | 44 | app = bmd.scriptapp(*args) 45 | if not app: 46 | raise RuntimeError("Couldn't connect to application.") 47 | 48 | return app 49 | 50 | 51 | def get_fusion(app="Fusion", ip="127.0.0.1", timeout=0.1, uuid=None): 52 | """Establish connection with an already active Fusion application. 53 | 54 | Args: 55 | app (str): Name of the application. (defaults to: "Fusion") 56 | ip (str): The IP address of the host to connect to. 57 | timeout (float): Timeout for connection in seconds. 58 | uuid (str): The UUID of the application to connect to. 59 | 60 | Returns: 61 | fusionless.core.Fusion: The created Fusion application 62 | 63 | """ 64 | ptr = _get_app(app, ip, timeout, uuid) 65 | 66 | from .core import Fusion 67 | return Fusion(ptr) 68 | 69 | 70 | def get_current_uuid(): 71 | """This should only work from within the application. 72 | 73 | Returns: 74 | The unique id for the currently running application's script process 75 | which can be used elsewhere to connect to exactly this application. 76 | 77 | """ 78 | return bmd.getappuuid() 79 | -------------------------------------------------------------------------------- /fusionless/version.py: -------------------------------------------------------------------------------- 1 | 2 | VERSION_MAJOR = 0 3 | VERSION_MINOR = 1 4 | VERSION_PATCH = 1 5 | 6 | version_info = (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH) 7 | version = '%i.%i.%i' % version_info 8 | __version__ = version 9 | 10 | __all__ = ['version', 'version_info', '__version__'] 11 | -------------------------------------------------------------------------------- /tests/test_tools.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import fusionless as fu 3 | 4 | 5 | class TestTools(unittest.TestCase): 6 | def test_rename(self): 7 | """ Test renaming and clear name functionality """ 8 | c = fu.Comp() 9 | 10 | tool = c.create_tool("Background") 11 | original_name = tool.name() 12 | 13 | tool.rename("foobar_macaroni") 14 | new_name = tool.name() 15 | 16 | tool.clear_name() 17 | cleared_name = tool.name() 18 | 19 | self.assertNotEqual(original_name, new_name) 20 | self.assertEqual(new_name, "foobar_macaroni") 21 | self.assertEqual(cleared_name, original_name) # not sure if this is 'always expected behaviour' to be equal 22 | 23 | tool.delete() 24 | 25 | def test_pos(self): 26 | """ Test setting position and getting position afterwards """ 27 | c = fu.Comp() 28 | 29 | tool = c.create_tool("Merge") 30 | 31 | tmp_pos = [10, 10] 32 | tool.set_pos(tmp_pos) 33 | pos = tool.get_pos() 34 | self.assertEqual(pos, tmp_pos) 35 | 36 | tmp_pos = [123, 321] 37 | tool.set_pos(tmp_pos) 38 | pos = tool.get_pos() 39 | self.assertEqual(pos, tmp_pos) 40 | 41 | tool.delete() 42 | --------------------------------------------------------------------------------