├── .gitignore ├── LICENSE.md ├── README.md ├── audio └── audio_analysis_channel.tox ├── color_palette.tox ├── command_panel.md ├── command_panel.py ├── command_panel.tox ├── common.py ├── component-editor.toe ├── components.tox ├── console_logger.tox ├── control ├── ControlMappingExt.py ├── codev2.tox ├── control_binder.tox ├── control_mapper.tox ├── control_target.md ├── control_target.tox ├── device_control_display.tox ├── device_display.tox └── mftwister.tox ├── corner_rect.tox ├── file_logger.tox ├── hexagonal_grid.tox ├── lib ├── _stubs │ ├── ArcBallExt.py │ ├── CallbacksExt.py │ ├── ListerExt.py │ ├── PopDialogExt.py │ ├── PopMenuExt.py │ ├── TDCallbacksExt.py │ ├── TDCodeGen.py │ ├── TDFunctions.py │ ├── TDJSON.py │ ├── TDStoreTools.py │ ├── TreeListerExt.py │ ├── Updater.py │ └── __init__.py ├── logs.py └── tdcomponents_resolve.py ├── loggly_logger.tox ├── mapper.tox ├── midimap.py ├── mixer ├── MixerSourcesExt.py ├── mixer_sources.tox ├── source_track.tox ├── track_mixer.tox ├── videoSenderCore.tox └── video_sender.tox ├── multi_logger.tox ├── rack.toe ├── rack ├── component_editor │ ├── ComponentEditorExt.py │ └── component_editor.tox ├── component_picker │ ├── ComponentPickerExt.py │ └── component_picker.tox ├── editor │ ├── EditorExt.py │ ├── components │ │ ├── EditorCommon.py │ │ ├── EditorToolsExt.py │ │ ├── EditorViewsExt.py │ │ ├── SettingsExt.py │ │ ├── WorkspaceExt.py │ │ ├── editorTools.tox │ │ ├── editorViews.tox │ │ ├── preview_panel.tox │ │ ├── settings.tox │ │ ├── topMenuCallbacks.py │ │ ├── topMenuDefine.txt │ │ └── workspace.tox │ └── editor.tox ├── rack │ ├── RackExt.py │ └── rack.tox └── rack_tools │ └── rack_tools.tox ├── recorder.py ├── recorder.tox ├── recorder ├── recorderCore.tox └── recorderCoreExt.py ├── sop_merger.tox ├── td-components-tester.toe ├── tools ├── ParamHelperExt.py ├── PathSetupExt.py ├── UIColorEditorExt.py ├── base_save.tox ├── paramAdjuster.tox ├── paramAdjusterExt.py ├── param_helper.tox ├── path_setup.tox ├── saveEXT.py └── ui_color_editor.tox ├── ui ├── slider_value_overlay.tox ├── statusOverlay-readme.txt ├── statusOverlay-thumb.jpg ├── statusOverlay.tox ├── statusOverlayExt.py ├── ui_overrides.txt └── widget_ui_overrides.tox └── utils ├── DataLock.py ├── ParamFilter.py ├── channel_remap.tox ├── chop_lock.tox ├── dat_lock.tox ├── lfo_generator.tox ├── modulated_value.tox ├── param_animator.tox ├── param_filter.tox ├── settings ├── SettingsExt.py └── settings.tox ├── speed_generator.tox ├── taskManager.tox └── taskManagerExt.py /.gitignore: -------------------------------------------------------------------------------- 1 | CrashAutoSave* 2 | *.dmp 3 | .idea 4 | *.pyc 5 | *-log.txt 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | CC0 1.0 Universal 3 | 4 | Statement of Purpose 5 | 6 | The laws of most jurisdictions throughout the world automatically confer 7 | exclusive Copyright and Related Rights (defined below) upon the creator and 8 | subsequent owner(s) (each and all, an "owner") of an original work of 9 | authorship and/or a database (each, a "Work"). 10 | 11 | Certain owners wish to permanently relinquish those rights to a Work for the 12 | purpose of contributing to a commons of creative, cultural and scientific 13 | works ("Commons") that the public can reliably and without fear of later 14 | claims of infringement build upon, modify, incorporate in other works, reuse 15 | and redistribute as freely as possible in any form whatsoever and for any 16 | purposes, including without limitation commercial purposes. These owners may 17 | contribute to the Commons to promote the ideal of a free culture and the 18 | further production of creative, cultural and scientific works, or to gain 19 | reputation or greater distribution for their Work in part through the use and 20 | efforts of others. 21 | 22 | For these and/or other purposes and motivations, and without any expectation 23 | of additional consideration or compensation, the person associating CC0 with a 24 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 25 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 26 | and publicly distribute the Work under its terms, with knowledge of his or her 27 | Copyright and Related Rights in the Work and the meaning and intended legal 28 | effect of CC0 on those rights. 29 | 30 | 1. Copyright and Related Rights. A Work made available under CC0 may be 31 | protected by copyright and related or neighboring rights ("Copyright and 32 | Related Rights"). Copyright and Related Rights include, but are not limited 33 | to, the following: 34 | 35 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 36 | and translate a Work; 37 | 38 | ii. moral rights retained by the original author(s) and/or performer(s); 39 | 40 | iii. publicity and privacy rights pertaining to a person's image or likeness 41 | depicted in a Work; 42 | 43 | iv. rights protecting against unfair competition in regards to a Work, 44 | subject to the limitations in paragraph 4(a), below; 45 | 46 | v. rights protecting the extraction, dissemination, use and reuse of data in 47 | a Work; 48 | 49 | vi. database rights (such as those arising under Directive 96/9/EC of the 50 | European Parliament and of the Council of 11 March 1996 on the legal 51 | protection of databases, and under any national implementation thereof, 52 | including any amended or successor version of such directive); and 53 | 54 | vii. other similar, equivalent or corresponding rights throughout the world 55 | based on applicable law or treaty, and any national implementations thereof. 56 | 57 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 58 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 59 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 60 | and Related Rights and associated claims and causes of action, whether now 61 | known or unknown (including existing as well as future claims and causes of 62 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 63 | duration provided by applicable law or treaty (including future time 64 | extensions), (iii) in any current or future medium and for any number of 65 | copies, and (iv) for any purpose whatsoever, including without limitation 66 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 67 | the Waiver for the benefit of each member of the public at large and to the 68 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 69 | shall not be subject to revocation, rescission, cancellation, termination, or 70 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 71 | by the public as contemplated by Affirmer's express Statement of Purpose. 72 | 73 | 3. Public License Fallback. Should any part of the Waiver for any reason be 74 | judged legally invalid or ineffective under applicable law, then the Waiver 75 | shall be preserved to the maximum extent permitted taking into account 76 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 77 | is so judged Affirmer hereby grants to each affected person a royalty-free, 78 | non transferable, non sublicensable, non exclusive, irrevocable and 79 | unconditional license to exercise Affirmer's Copyright and Related Rights in 80 | the Work (i) in all territories worldwide, (ii) for the maximum duration 81 | provided by applicable law or treaty (including future time extensions), (iii) 82 | in any current or future medium and for any number of copies, and (iv) for any 83 | purpose whatsoever, including without limitation commercial, advertising or 84 | promotional purposes (the "License"). The License shall be deemed effective as 85 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 86 | License for any reason be judged legally invalid or ineffective under 87 | applicable law, such partial invalidity or ineffectiveness shall not 88 | invalidate the remainder of the License, and in such case Affirmer hereby 89 | affirms that he or she will not (i) exercise any of his or her remaining 90 | Copyright and Related Rights in the Work or (ii) assert any associated claims 91 | and causes of action with respect to the Work, in either case contrary to 92 | Affirmer's express Statement of Purpose. 93 | 94 | 4. Limitations and Disclaimers. 95 | 96 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 97 | surrendered, licensed or otherwise affected by this document. 98 | 99 | b. Affirmer offers the Work as-is and makes no representations or warranties 100 | of any kind concerning the Work, express, implied, statutory or otherwise, 101 | including without limitation warranties of title, merchantability, fitness 102 | for a particular purpose, non infringement, or the absence of latent or 103 | other defects, accuracy, or the present or absence of errors, whether or not 104 | discoverable, all to the greatest extent permissible under applicable law. 105 | 106 | c. Affirmer disclaims responsibility for clearing rights of other persons 107 | that may apply to the Work or any use thereof, including without limitation 108 | any person's Copyright and Related Rights in the Work. Further, Affirmer 109 | disclaims responsibility for obtaining any necessary consents, permissions 110 | or other rights required for any use of the Work. 111 | 112 | d. Affirmer understands and acknowledges that Creative Commons is not a 113 | party to this document and has no duty or obligation with respect to this 114 | CC0 or use of the Work. 115 | 116 | For more information, please see 117 | 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | td-components 2 | -------------------------------------------------------------------------------- /audio/audio_analysis_channel.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/audio/audio_analysis_channel.tox -------------------------------------------------------------------------------- /color_palette.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/color_palette.tox -------------------------------------------------------------------------------- /command_panel.md: -------------------------------------------------------------------------------- 1 | # Command Panel 2 | 3 | The command panel is a configurable toolbar for invoking scripts. 4 | 5 | The original use case was for editor shortcuts, like opening panel viewers or output windows, aligning selected OPs, 6 | and so on. But it was designed to be reusable for a variety of purposes. 7 | 8 | The panel loads a list of commands that each have an executable action, and a label and/or icon TOP. 9 | It generates a button for each command with the relevant text/icon and runs the associated script when it is clicked. 10 | 11 | ## Specifying commands 12 | 13 | There are several ways to specify commands for the panel. 14 | 15 | 1. Providing a table DAT via the "Command Table" parameter. 16 | 2. Specifying a list of `dict`s using the "Command Object List" parameter. 17 | 3. Enabling predefined lists of common commands, via the "Include ___ Commands" parameters. 18 | 19 | ### Using a command table 20 | 21 | Each row after the first row is converted to a `dict`, with the columns as keys. See the next section for how those are 22 | handled. 23 | 24 | ### Using `dict`s 25 | 26 | Each `dict` in a list represents a single command. The following settings are available for all (or many) types of 27 | commands: 28 | 29 | * `label` (required) - text to put in the button 30 | * `img` - optional TOP path for an image to show in the button 31 | * `isIcon` - if true, and there's no `img`, `label` is interpreted as an icon character in the Material Design Icons. 32 | * `help` - help text for the button 33 | * `type` - specifies what type of pre-defined action to use (see below) 34 | 35 | #### Command types 36 | 37 | * *(none)* - the `action` setting is interpreted as a reference to an executable Python function 38 | * `open` or `view` - opens an operator viewer or window for the specified `op` 39 | * `unique` - should the viewer be opened as a unique viewer for the op, defaults to true (not available for windows) 40 | * `borders` - should the viewer have window borders, defaults to true 41 | * `toggle` - equivalent to `open`/`view` but if the viewer is already open, it closes it 42 | * `pars` - open a parameter editor for the `op` 43 | * `edit` or `navigate` - opens the `op` in the network editor, reusing the main editor if possible 44 | * `run` - execute the `op` (must be a text DAT) 45 | * `script` - execute a string of python code, from the `script` setting. Note that if you omit the `type` entirely but 46 | have a `script` field, the type is assumed to be `script` 47 | * `reload` - reload the `op` (which can be multiple ops) from their associated files (such as text/table DATs) 48 | * `save` - saves either the selected COMPs or the COMP that the network editor is in as a tox 49 | 50 | 51 | ### Using script text DATs 52 | 53 | The `Cmdscripts` paramter can be used to specify a list of text DATs that each contain a script to run as a command. 54 | If the script has a `def action(...)` in it, the script is treated as a module, and it can specify settings by declaring 55 | them as variables. Otherwise the script is treated as a single block of executable code (using `theScriptDAT.run()`). 56 | 57 | DAT with just a raw chunk of code: 58 | ``` 59 | op('/whatever/foo').par.Stuff = 'things' 60 | print('hello!') 61 | ``` 62 | 63 | DAT with a function and settings: 64 | ``` 65 | label = 'some command' 66 | help = 'do stuff with a script' 67 | def action(): 68 | print('hello!') 69 | ``` 70 | 71 | 72 | ## Command `Context` 73 | 74 | Command functions can optionally accept an argument that's generated by the panel, which provides access to shortcuts 75 | and helper functions based on the editor context (what OPs are selected in the network editor, etc). 76 | -------------------------------------------------------------------------------- /command_panel.py: -------------------------------------------------------------------------------- 1 | from inspect import signature 2 | import os 3 | 4 | if False: 5 | from _stubs import * 6 | 7 | class Context: 8 | def __init__(self, ownerComp): 9 | self.ownerComp = ownerComp 10 | 11 | def resolveOP(self, o): 12 | if isinstance(o, str): 13 | return self.ownerComp.op(o) 14 | return o 15 | 16 | def openUI(self, o, unique=True, borders=True): 17 | o = self.resolveOP(o) 18 | if not o: 19 | return 20 | if o.type == 'window': 21 | o.par.winopen.pulse() 22 | else: 23 | o.openViewer(unique=unique, borders=borders) 24 | 25 | def closeUI(self, o, topMost=False): 26 | o = self.resolveOP(o) 27 | if not o: 28 | return 29 | if o.type == 'window': 30 | o.par.winclose.pulse() 31 | else: 32 | o.closeViewer(topMost=topMost) 33 | 34 | def openOrToggleUI(self, o, borders=True): 35 | o = self.resolveOP(o) 36 | if not o: 37 | return 38 | if o.type == 'window': 39 | self.toggleUI(o, borders=borders) 40 | else: 41 | self.openUI(o, unique=True, borders=borders) 42 | 43 | def toggleUI(self, o, borders=True): 44 | o = self.resolveOP(o) 45 | if not o: 46 | return 47 | if o.type == 'window': 48 | if o.isOpen: 49 | self.openUI(o, unique=True, borders=borders) 50 | else: 51 | self.closeUI(o, topMost=False) 52 | else: 53 | raise NotImplementedError('toggleUI only supported for Window COMP') 54 | 55 | @property 56 | def activeEditor(self): 57 | pane = ui.panes.current 58 | if pane.type == PaneType.NETWORKEDITOR: 59 | return pane 60 | for pane in ui.panes: 61 | if pane.type == PaneType.NETWORKEDITOR: 62 | return pane 63 | 64 | def getSelectedOps(self, predicate=None): 65 | pane = self.activeEditor 66 | if not pane: 67 | return [] 68 | sel = pane.owner.selectedChildren or [pane.owner.currentChild] 69 | if predicate is not None: 70 | sel = list(filter(predicate, sel)) 71 | return sel 72 | 73 | def getSelectedOrContext(self, predicate): 74 | sel = self.getSelectedOps(predicate) 75 | if sel: 76 | return sel[0] 77 | pane = self.activeEditor 78 | if not pane: 79 | return 80 | o = pane.owner 81 | while o: 82 | if predicate(o): 83 | return o 84 | o = o.parent() 85 | 86 | def navigateTo(self, o): 87 | o = self.resolveOP(o) 88 | if not o: 89 | return 90 | pane = self.activeEditor 91 | if pane: 92 | pane.owner = o 93 | 94 | def openNetwork(self, o): 95 | o = self.resolveOP(o) 96 | if not o or not o.isCOMP: 97 | return 98 | pane = ui.panes.createFloating(type=PaneType.NETWORKEDITOR) 99 | pane.owner = o 100 | 101 | @staticmethod 102 | def resolveFile(path): 103 | return path if os.path.exists(path) else '' 104 | 105 | def saveOP(self, o, path=None): 106 | o = self.resolveOP(o) 107 | if not o: 108 | return False 109 | if path is None: 110 | if o.isDAT and getattr(o.par, 'file') and hasattr(o.par, 'writepulse'): 111 | path = o.par.file 112 | o.par.writepulse.pulse() 113 | ui.status = 'saved {} to {}'.format(o.path, path) 114 | return True 115 | if o.isCOMP: 116 | path = o.par.externaltox.eval() 117 | elif hasattr(o.par, 'file'): 118 | path = o.par.file.eval() 119 | if not path: 120 | return False 121 | o.save(path) 122 | ui.status = 'saved {} to {}'.format(o.path, path) 123 | return True 124 | 125 | def reloadOPFile(self, o): 126 | o = self.resolveOP(o) 127 | if not o: 128 | return False 129 | if o.isDAT and hasattr(o.par, 'loadonstartpulse'): 130 | o.par.loadonstartpulse.pulse() 131 | return True 132 | if o.isCOMP: 133 | o.par.reinitnet.pulse() 134 | return True 135 | return False 136 | 137 | def _callWithOptionalParam(fn, param): 138 | if len(signature(fn).parameters) == 0: 139 | return fn() 140 | else: 141 | return fn(param) 142 | 143 | class Command: 144 | def __init__(self, action, **attrs): 145 | self.action = action 146 | self.attrs = attrs or {} 147 | 148 | @property 149 | def label(self): return self.attrs.get('label') or '--' 150 | 151 | @property 152 | def help(self): return self.attrs.get('help') or '' 153 | 154 | @property 155 | def img(self): 156 | i = self.attrs.get('img') 157 | return op(i) if isinstance(i, str) else i 158 | 159 | @property 160 | def isIcon(self): 161 | return self.attrs.get('isIcon') 162 | 163 | def invoke(self, context): 164 | _callWithOptionalParam(self.action, context) 165 | 166 | @classmethod 167 | def forOpenUI(cls, o, unique=True, borders=True, **kwargs): 168 | return cls(lambda context: context.openUI(o, unique=unique, borders=borders), **kwargs) 169 | 170 | @classmethod 171 | def forToggleUI(cls, o, borders=True, **kwargs): 172 | return cls(lambda context: context.openOrToggleUI(o, borders=borders), **kwargs) 173 | 174 | @classmethod 175 | def forEdit(cls, o, **kwargs): 176 | return cls(lambda context: context.navigateTo(o), **kwargs) 177 | 178 | @classmethod 179 | def forParams(cls, oppath, **kwargs): 180 | def _action(context): 181 | o = context.resolveOP(oppath) 182 | if o: 183 | o.openParameters() 184 | return cls(_action, **kwargs) 185 | 186 | @classmethod 187 | def forReload(cls, oppaths, **kwargs): 188 | def _action(context): 189 | targetops = ops(oppaths) 190 | for o in targetops: 191 | context.reloadOPFile(o) 192 | return cls(_action, **kwargs) 193 | 194 | @classmethod 195 | def forSave(cls, oppaths, path, **kwargs): 196 | def _action(context): 197 | targetops = ops(oppaths) 198 | for o in targetops: 199 | context.saveOP(o, path) 200 | return cls(_action, **kwargs) 201 | 202 | @classmethod 203 | def forExec( 204 | cls, oppath, args=None, endFrame=False, fromOP=None, 205 | group=None, delayFrames=0, delayMilliSeconds=0, **kwargs): 206 | def _action(context): 207 | o = context.resolveOP(oppath) 208 | if o and o.isDAT: 209 | o.run( 210 | *args, 211 | endFrame=endFrame, fromOP=fromOP, group=group, 212 | delayFrames=delayFrames, delayMilliSeconds=delayMilliSeconds, 213 | **kwargs) 214 | return cls(_action, **kwargs) 215 | 216 | @classmethod 217 | def forScriptDAT(cls, dat): 218 | if 'def action' not in dat.text: 219 | return cls.forExec(dat.path, label=dat.name) 220 | else: 221 | locs = dat.locals 222 | return cls( 223 | lambda context: _callWithOptionalParam(dat.module.action, context), 224 | label=locs.get('label', None) or dat.name, 225 | **{ 226 | k: locs[k] 227 | for k in ['img', 'help', 'isIcon'] 228 | if k in locs 229 | }) 230 | 231 | @classmethod 232 | def forAction(cls, action, **kwargs): 233 | return cls(action, **kwargs) 234 | 235 | @classmethod 236 | def fromRow(cls, dat, row): 237 | obj = { 238 | dat[0, i].val: dat[row, i].val 239 | for i in range(dat.numCols) 240 | } 241 | return Command.fromObj(obj) 242 | 243 | @classmethod 244 | def fromObj(cls, obj): 245 | if _asbool(obj.get('hidden'), False): 246 | return None 247 | typename = obj.get('type') 248 | attrs = { 249 | 'label': obj.get('label'), 250 | 'help': obj.get('help'), 251 | 'img': obj.get('img'), 252 | 'isIcon': obj.get('isIcon'), 253 | } 254 | o = obj.get('op') 255 | if not typename and 'script' in obj: 256 | typename = 'script' 257 | if not typename: 258 | action = obj.get('action', _noop) 259 | return Command.forAction(action, **attrs) 260 | if typename in ['open', 'view']: 261 | return Command.forOpenUI( 262 | o, 263 | unique=_asbool(obj.get('unique'), True), 264 | borders=_asbool(obj.get('borders'), True), 265 | **attrs) 266 | elif typename in ['toggle']: 267 | return Command.forToggleUI( 268 | o, 269 | borders=_asbool(obj.get('borders'), False), 270 | **attrs) 271 | elif typename in ['pars']: 272 | return Command.forParams(o, **attrs) 273 | elif typename in ['edit', 'navigate']: 274 | return Command.forEdit(o, **attrs) 275 | elif typename in ['run']: 276 | return Command.forExec( 277 | o, 278 | delayFrames=_asint(obj.get('delayFrames'), 0), 279 | **attrs) 280 | elif typename == 'script': 281 | code = obj.get('script') 282 | if not code: 283 | return Command.forAction(_noop, **attrs) 284 | if code.startswith('lambda'): 285 | return Command.forAction(eval(code), **attrs) 286 | else: 287 | return Command.forAction(lambda _: eval(code), **attrs) 288 | elif typename in ['reload']: 289 | return Command.forReload(o, **attrs) 290 | elif typename in ['save']: 291 | return Command.forSave( 292 | o, 293 | path=obj.get('path') or obj.get('file'), 294 | **attrs) 295 | 296 | def _noop(*args, **kwargs): 297 | pass 298 | 299 | def _asbool(val, defval): 300 | if val is None or val == '': 301 | return defval 302 | if val == '1': 303 | return True 304 | if val == '0': 305 | return False 306 | return bool(val) 307 | 308 | def _asint(val, defval): 309 | if val is None or val == '': 310 | return defval 311 | return int(val) 312 | 313 | def _strornull(val): 314 | return str(val) if val is not None else None 315 | 316 | class CommandPanel: 317 | def __init__(self, comp): 318 | self.ownerComp = comp 319 | self.commandlist = [] 320 | 321 | def _AddCommand(self, command): 322 | if command: 323 | self.commandlist.append(command) 324 | 325 | def _AddCommands(self, commands): 326 | if commands: 327 | for command in commands: 328 | self._AddCommand(command) 329 | 330 | def _AddCommandsFromTable(self, dat): 331 | if not dat or dat.numRows < 2: 332 | return 333 | for row in range(1, dat.numRows): 334 | self._AddCommand(Command.fromRow(dat, row)) 335 | 336 | def _AddCommandsFromScriptDATs(self): 337 | scripts = self.ownerComp.par.Cmdscripts.evalOPs() 338 | for script in scripts: 339 | self._AddCommand(Command.forScriptDAT(script)) 340 | 341 | def _AddCommandsFromList(self): 342 | cmdobjs = self.ownerComp.par.Cmdobjs.eval() 343 | if cmdobjs: 344 | if isinstance(cmdobjs, (list, tuple)): 345 | for cmdobj in cmdobjs: 346 | self._AddCommand(Command.fromObj(cmdobj)) 347 | else: 348 | self._AddCommand(Command.fromObj(cmdobjs)) 349 | 350 | def RebuildCommands(self): 351 | self.commandlist.clear() 352 | self._AddCommandsFromTable(self.ownerComp.op('./command_table_in')) 353 | if self.ownerComp.par.Includebasictoolcmds: 354 | self._AddCommands(_basicToolCommands) 355 | if self.ownerComp.par.Includetestcmds: 356 | self._AddCommandsFromTable(self.ownerComp.op('./TEST_commands')) 357 | self._AddCommandsFromList() 358 | self._AddCommandsFromScriptDATs() 359 | 360 | def BuildCommandTable(self, dat): 361 | self.RebuildCommands() 362 | dat.clear() 363 | dat.appendRow(['label', 'help', 'img', 'isIcon']) 364 | for command in self.commandlist: 365 | dat.appendRow([ 366 | command.label, 367 | command.help, 368 | command.img or '', 369 | bool(command.isIcon), 370 | ]) 371 | 372 | def _GetCommandByIndex(self, index): 373 | if index < 0 or index >= len(self.commandlist): 374 | return None 375 | return self.commandlist[index] 376 | 377 | def ExecuteCommandByIndex(self, index): 378 | command = self._GetCommandByIndex(index) 379 | if not command: 380 | return 381 | context = Context(self.ownerComp) 382 | command.invoke(context) 383 | 384 | def InitializeButton(self, button, index): 385 | command = self._GetCommandByIndex(index) # type: Command 386 | button.par.display = True 387 | button.par.Buttonofflabel = command.label 388 | button.par.Buttononlabel = command.label 389 | button.par.Offtoonscript0 = 'iop.commands.ExecuteCommandByIndex({})'.format(index) 390 | img = command.img 391 | imgpath = img.path if img is not None else '' 392 | button.par.Buttonofftop = imgpath 393 | button.par.Buttonontop = imgpath 394 | button.par.Popuphelp = command.help 395 | if command.isIcon and not img: 396 | button.par.Buttonfont = 'Material_Design_Icons' 397 | 398 | def _copyPaths(context: Context): 399 | sel = context.getSelectedOps() 400 | ui.clipboard = ' '.join([o.path for o in sel]) 401 | 402 | def _saveTox(context: Context): 403 | comp = context.getSelectedOrContext(lambda o: o.isCOMP and o.par.externaltox) 404 | if comp: 405 | context.saveOP(comp) 406 | 407 | def _incrementComponentVersion(context: Context): 408 | comp = context.getSelectedOrContext(lambda o: o.isCOMP and hasattr(o.par, 'Compversion')) 409 | if not comp: 410 | comp = context.getSelectedOrContext(lambda o: o.isCOMP) 411 | if not comp: 412 | ui.status = 'Unable to increment component version, no suitable COMP' 413 | return 414 | ui.status = 'Updating component version of {!r}'.format(comp) 415 | if not hasattr(comp.par, 'Compversion'): 416 | page = comp.appendCustomPage(':meta') 417 | par = page.appendInt('Compversion', label=':Version')[0] 418 | par.val = 0 419 | par.default = 0 420 | par.readOnly = True 421 | else: 422 | par = comp.par.Compversion 423 | par.val += 1 424 | par.default = par.val 425 | if par.normMax < par.val: 426 | par.normMax = par.val 427 | par.readOnly = True 428 | 429 | def _makeLastPage(comp, page): 430 | if len(comp.customPages) == 1 and comp.customPages[0] == page: 431 | return 432 | orderedpages = [p.name for p in comp.customPages if p != page] + [page.name] 433 | comp.sortCustomPages(*orderedpages) 434 | 435 | def _addOrUpdatePar(appendmethod, name, label, value=None, expr=None, readonly=None, setdefault=False): 436 | p = appendmethod(name, label=label)[0] 437 | if expr is not None: 438 | p.expr = expr 439 | if setdefault: 440 | p.defaultExpr = expr 441 | elif value is not None: 442 | p.val = value 443 | if setdefault: 444 | p.default = value 445 | if readonly is not None: 446 | p.readOnly = readonly 447 | return p 448 | 449 | def _addOrUpdateMetadataPar(appendmethod, name, label, value): 450 | _addOrUpdatePar(appendmethod, name, label, value, readonly=True, setdefault=True) 451 | 452 | def _setComponentMetadata( 453 | context: Context, 454 | description=None, 455 | version=None, 456 | typeid=None, 457 | website=None, 458 | author=None, 459 | page=':meta'): 460 | comp = context.getSelectedOrContext(lambda o: o.isCOMP) 461 | if not comp: 462 | ui.status = 'Unable to create metadata, no target comp' 463 | return 464 | page = comp.appendCustomPage(page) 465 | if page.name.startswith(':'): 466 | _makeLastPage(comp, page) 467 | if typeid: 468 | _addOrUpdateMetadataPar(page.appendStr, 'Comptypeid', ':Type ID', typeid) 469 | _addOrUpdateMetadataPar(page.appendStr, 'Compdescription', ':Description', description) 470 | _addOrUpdateMetadataPar(page.appendInt, 'Compversion', ':Version', version) 471 | _addOrUpdateMetadataPar(page.appendStr, 'Compwebsite', ':Website', website) 472 | _addOrUpdateMetadataPar(page.appendStr, 'Compauthor', ':Author', author) 473 | page.sort('Comptypeid', 'Compdescription', 'Compversion', 'Compwebsite', 'Compauthor') 474 | 475 | def _createTektTdCompsMetadata(context: Context): 476 | _setComponentMetadata( 477 | context, 478 | website='https://github.com/optexture/td-components', 479 | author='tekt@optexture.com') 480 | 481 | def _destroyCustomPars(context: Context): 482 | for targetOp in context.getSelectedOps(lambda o: hasattr(o, 'destroyCustomPars')): 483 | targetOp.destroyCustomPars() 484 | 485 | _basicToolCommands = [ 486 | Command.forAction( 487 | _copyPaths, 488 | label='copy path', 489 | help='copy paths of selected ops'), 490 | Command.forAction( 491 | _saveTox, 492 | label='save', 493 | help='save selected or active component tox file'), 494 | Command.forAction( 495 | _incrementComponentVersion, 496 | label='version++', 497 | help='increment the component version attribute on selected or active', 498 | ), 499 | Command.forAction( 500 | _destroyCustomPars, 501 | label='kill pars', 502 | help='destroy all custom parameters on the selected OP(s)' 503 | ), 504 | Command.forAction( 505 | _createTektTdCompsMetadata, 506 | label='[t] meta', 507 | help='add tekt TD-components metadata to current component', 508 | ), 509 | ] 510 | -------------------------------------------------------------------------------- /command_panel.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/command_panel.tox -------------------------------------------------------------------------------- /component-editor.toe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/component-editor.toe -------------------------------------------------------------------------------- /components.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/components.tox -------------------------------------------------------------------------------- /console_logger.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/console_logger.tox -------------------------------------------------------------------------------- /control/ControlMappingExt.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | 3 | # noinspection PyUnreachableCode 4 | if False: 5 | # noinspection PyUnresolvedReferences 6 | from _stubs import * 7 | 8 | class ControlTarget: 9 | def __init__(self, ownerComp: 'COMP'): 10 | self.ownerComp = ownerComp 11 | 12 | def HandleActionPulse(self, action): 13 | if action == 'Initmaptable': 14 | self._InitTable(createMissing=True, clearExisting=False) 15 | elif action == 'Clearmaptable': 16 | self._InitTable(createMissing=False, clearExisting=True) 17 | elif action == 'Editmaptable': 18 | self._InitTable(createMissing=True, clearExisting=False) 19 | dat = self.ownerComp.par.Maptable.eval() 20 | if dat: 21 | dat.openViewer() 22 | 23 | def _InitTable(self, createMissing=False, clearExisting=False): 24 | par = self.ownerComp.par.Maptable 25 | dat = par.eval() # type: DAT 26 | if not dat and createMissing: 27 | dat = self.ownerComp.parent().create(tableDAT, 'mappings') 28 | dat.nodeX = self.ownerComp.nodeX + self.ownerComp.nodeWidth + 50 29 | dat.nodeY = self.ownerComp.nodeY 30 | par.val = dat 31 | if not dat: 32 | return 33 | if clearExisting: 34 | dat.clear() 35 | if dat.numRows == 1 and dat.numCols == 1 and dat[0, 0] == '': 36 | dat.clear() 37 | mappingColumns = [ 38 | 'path', 39 | 'param', 40 | 'enable', 41 | 'low', 42 | 'high', 43 | 'control', 44 | 'group', 45 | 'handling', 46 | ] 47 | if not dat.numRows: 48 | dat.appendRow(mappingColumns) 49 | else: 50 | for col in mappingColumns: 51 | if not dat.col(col): 52 | dat.appendCol([col]) 53 | 54 | def AddMappingForParam(self, par: 'Par', control: str = None, enable: bool = False): 55 | self._InitTable(createMissing=True, clearExisting=False) 56 | table = self.ownerComp.par.Maptable.eval() # type: DAT 57 | root = self.ownerComp.par.Targetroot.eval() 58 | if par.owner == root: 59 | path = '' 60 | elif root: 61 | path = root.relativePath(par.owner) 62 | else: 63 | path = par.owner.path 64 | i = table.numRows 65 | table.appendRow([]) 66 | table[i, 'path'] = path 67 | table[i, 'param'] = par.name 68 | table[i, 'enable'] = int(enable) 69 | table[i, 'control'] = control or '' 70 | 71 | def BuildDeviceControls(outDat: 'DAT', definition: 'COMP'): 72 | outDat.clear() 73 | outDat.appendRow([ 74 | 'control', 75 | 'cc', 76 | 'chan', 77 | ]) 78 | if not definition: 79 | return 80 | for ctrlTable in definition.ops('sliders', 'buttons'): 81 | if not ctrlTable or not ctrlTable.valid or ctrlTable.numCols < 2: 82 | continue 83 | for ctrlRow in range(ctrlTable.numRows): 84 | name = ctrlTable[ctrlRow, 0] 85 | if not name: 86 | continue 87 | pattern = ctrlTable[ctrlRow, 1].val 88 | if not pattern: 89 | continue 90 | parts = pattern.split(' ') 91 | if len(parts) != 3: 92 | continue 93 | try: 94 | cc = int(parts[1], 16) 95 | except ValueError: 96 | cc = None 97 | if cc is None: 98 | continue 99 | outDat.appendRow([ 100 | name, 101 | cc, 102 | 'ch1c{}'.format(cc), 103 | ]) 104 | 105 | class ControlMapper: 106 | def __init__(self, ownerComp: 'COMP'): 107 | self.ownerComp = ownerComp 108 | self.statePar = ownerComp.op('iparMapperState').par 109 | 110 | @staticmethod 111 | def BuildDeviceControls(outDat: 'DAT', definition: 'COMP'): 112 | BuildDeviceControls(outDat, definition) 113 | 114 | @staticmethod 115 | def BuildMapTable(outDat: 'DAT', targets: List[str], deviceControls: 'DAT'): 116 | outDat.clear() 117 | outDat.appendRow([ 118 | 'param', 119 | 'path', 120 | 'parName', 121 | 'enable', 122 | 'low', 123 | 'high', 124 | 'targetName', 125 | 'group', 126 | 'parType', 127 | 'control', 128 | 'cc', 129 | 'chan', 130 | 'handling', 131 | ]) 132 | for target in ops(*targets): 133 | mappings = target.op('mappings') 134 | if not mappings or mappings.numRows < 2: 135 | continue 136 | _AddToMapTable( 137 | outDat, 138 | mappings, 139 | targetRoot=op(getattr(target.par, 'Targetroot', None)), 140 | targetName=str(getattr(target.par, 'Targetname', '')), 141 | groups=str(getattr(target.par, 'Group', '')), 142 | deviceControls=deviceControls, 143 | ) 144 | 145 | @staticmethod 146 | def BuildOpPars(outDat: 'DAT', mapDat: 'DAT'): 147 | outDat.clear() 148 | opPars = {} 149 | for i in range(1, mapDat.numRows): 150 | path = mapDat[i, 'path'].val 151 | param = mapDat[i, 'parName'].val 152 | if not path or not param: 153 | continue 154 | if path in opPars: 155 | opPars[path].append(param) 156 | else: 157 | opPars[path] = [param] 158 | for path, params in opPars.items(): 159 | outDat.appendRow([path] + params) 160 | 161 | def HandleDrop(self, dropName, dropExt, baseName, destPath): 162 | print(f'{self.ownerComp}.HandleDrop(dropName: {dropName}, dropExt: {dropExt}, baseName: {baseName}, destPath: {destPath}') 163 | if dropExt != 'parameter': 164 | return 165 | o = op(baseName) 166 | print(f'DROP O {o!r}') 167 | if not o: 168 | return 169 | par = getattr(o.par, dropName, None) 170 | print(f'DROP PAR {par!r}') 171 | if par is None: 172 | return 173 | target = self.statePar.Selectedtarget.eval() # type: ControlTarget 174 | if not target: 175 | return 176 | target.AddMappingForParam(par) 177 | 178 | 179 | 180 | def _AddToMapTable( 181 | outDat: 'DAT', 182 | inDat: 'DAT', 183 | targetRoot: Union[str, 'OP'], 184 | targetName: str, 185 | groups: str, 186 | deviceControls: 'DAT'): 187 | relOp = op(targetRoot) 188 | if relOp and not targetName: 189 | targetName = relOp.path 190 | if groups: 191 | groups = ' ' + groups 192 | if relOp and not targetName: 193 | targetName = relOp.path 194 | for inRow in range(1, inDat.numRows): 195 | enabled = inDat[inRow, 'enable'] in ('True', '1', '') 196 | if not enabled: 197 | continue 198 | control = inDat[inRow, 'control'] 199 | if control and deviceControls.row(control): 200 | cc = deviceControls[control, 'cc'] 201 | chan = deviceControls[control, 'chan'] 202 | else: 203 | cc = '' 204 | chan = '' 205 | relPath = inDat[inRow, 'path'] 206 | if not relPath: 207 | o = relOp 208 | else: 209 | o = relOp.op(relPath) if relOp else op(relOp) 210 | if not o: 211 | continue 212 | par = getattr(o.par, str(inDat[inRow, 'param']), None) if o else None 213 | if par is None: 214 | isPulse = False 215 | low = '' 216 | high = '' 217 | parName = '' 218 | else: 219 | if not (par.isNumber or par.isToggle or par.isMenu or par.isPulse): 220 | continue 221 | parName = par.name 222 | isPulse = par.isPulse or par.isMomentary 223 | low = inDat[inRow, 'low'].val 224 | high = inDat[inRow, 'high'].val 225 | if isPulse: 226 | low = 0 227 | high = 1 228 | else: 229 | if low == '': 230 | if par.isMenu: 231 | low = 0 232 | else: 233 | low = par.normMin 234 | if high == '': 235 | if par.isMenu: 236 | high = len(par.menuNames) - 1 237 | else: 238 | high = par.normMax 239 | row = outDat.numRows 240 | outDat.appendRow([]) 241 | outDat[row, 'param'] = o.path + ':' + parName 242 | outDat[row, 'path'] = o.path 243 | outDat[row, 'parName'] = parName 244 | outDat[row, 'enable'] = int(enabled) 245 | outDat[row, 'low'] = low 246 | outDat[row, 'high'] = high 247 | outDat[row, 'targetName'] = targetName 248 | outDat[row, 'group'] = (inDat[inRow, 'group'].val + groups).strip() 249 | outDat[row, 'parType'] = 'pulse' if isPulse else 'value' 250 | outDat[row, 'control'] = control 251 | outDat[row, 'cc'] = cc 252 | outDat[row, 'chan'] = chan 253 | outDat[row, 'handling'] = inDat[inRow, 'handling'] or 'script' 254 | 255 | class DeviceDisplay: 256 | def __init__(self, ownerComp): 257 | self.ownerComp = ownerComp 258 | 259 | @staticmethod 260 | def BuildDeviceControls(outDat: 'DAT', definition: 'COMP'): 261 | BuildDeviceControls(outDat, definition) 262 | 263 | @staticmethod 264 | def BuildLayout(outDat: 'DAT', layout: 'DAT', mappings: 'DAT', controls: 'DAT'): 265 | outDat.clear() 266 | outDat.appendRow([ 267 | 'label', 268 | 'page', 'row', 'col', 269 | 'slider', 'button', 270 | 'sliderChan', 'buttonChan', 271 | 'mappingLabel', 'mappingHelp', 272 | ]) 273 | if not layout: 274 | return 275 | for i in range(1, layout.numRows): 276 | slider = _cellOrDefault(layout, i, 'slider', '') 277 | button = _cellOrDefault(layout, i, 'button', '') 278 | controlLabelParts = [] 279 | mapLabel = '' 280 | mapHelp = '' 281 | if slider: 282 | controlLabelParts.append(slider) 283 | sliderParam = mappings[slider, 'param'] if mappings else None 284 | if sliderParam: 285 | mapLabel = _getParamLabel(sliderParam) 286 | mapHelp = sliderParam.val 287 | if button: 288 | controlLabelParts.append(button) 289 | buttonParam = mappings[button, 'param'] if mappings else None 290 | if buttonParam: 291 | if mapLabel: 292 | mapLabel = 's: {}'.format(mapLabel) 293 | mapLabel += '\\nb: ' + _getParamLabel(buttonParam) 294 | if mapHelp: 295 | mapHelp = 's: {}: '.format(mapHelp) 296 | mapHelp += '\\nb: ' + buttonParam.val 297 | 298 | outDat.appendRow([ 299 | ' | '.join(controlLabelParts), 300 | _cellOrDefault(layout, i, 'page', 0), 301 | _cellOrDefault(layout, i, 'row', 0), 302 | _cellOrDefault(layout, i, 'col', 0), 303 | slider, 304 | button, 305 | _cellOrDefault(controls, _cellOrDefault(layout, i, 'slider', None), 'chan', ''), 306 | _cellOrDefault(controls, _cellOrDefault(layout, i, 'button', None), 'chan', ''), 307 | mapLabel, 308 | mapHelp, 309 | ]) 310 | 311 | @staticmethod 312 | def BuildControlMappings(outDat: 'DAT', controls: 'DAT', mappings: 'DAT'): 313 | outDat.clear() 314 | outDat.appendRow(['control', 'param', 'paramLabel']) 315 | for control in controls.col('control')[1:]: 316 | if not mappings or not control or not mappings.row(control): 317 | param = None 318 | else: 319 | param = mappings[control, 'param'] 320 | outDat.appendRow([ 321 | control, 322 | param or '', 323 | '\\n'.join(param.val.split(':')) if param else '', 324 | ]) 325 | 326 | @staticmethod 327 | def BuildPages(outDat: 'DAT', definition: 'COMP', layout: 'DAT'): 328 | outDat.clear() 329 | outDat.appendRow(['page', 'label', 'triggerChan', 'triggerCc', 'triggerChanName']) 330 | pages = definition.op('pages') if definition else None 331 | pageNames = set() 332 | if pages: 333 | for i in range(1, pages.numRows): 334 | name = pages[i, 'page'] 335 | if not name: 336 | continue 337 | pageNames.add(name.val) 338 | chan = _cellOrDefault(pages, i, 'triggerChan', '') 339 | cc = _cellOrDefault(pages, i, 'triggerCc', '') 340 | outDat.appendRow([ 341 | name, 342 | _cellOrDefault(pages, i, 'label', name), 343 | chan, 344 | cc, 345 | 'ch{}c{}'.format(chan, cc), 346 | ]) 347 | for i in range(1, layout.numRows): 348 | page = layout[i, 'page'].val 349 | if page not in pageNames: 350 | pageNames.add(page) 351 | outDat.appendRow([page, page, '', '']) 352 | 353 | def _getParamLabel(param): 354 | if not param: 355 | return '' 356 | param = str(param) 357 | if ':' not in param: 358 | return param 359 | path, parName = param.rsplit(':', 1) 360 | o = op(path) 361 | par = getattr(o.par, parName, None) if o else None 362 | if par is None: 363 | return parName 364 | return par.label 365 | 366 | def _cellOrDefault(dat: 'DAT', r, c, default): 367 | if None in (dat, r, c): 368 | return default 369 | cell = dat[r, c] 370 | return cell.val if cell not in (None, '') else default 371 | -------------------------------------------------------------------------------- /control/codev2.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/control/codev2.tox -------------------------------------------------------------------------------- /control/control_binder.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/control/control_binder.tox -------------------------------------------------------------------------------- /control/control_mapper.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/control/control_mapper.tox -------------------------------------------------------------------------------- /control/control_target.md: -------------------------------------------------------------------------------- 1 | `control_target` is a collection of mappings for a 2 | target component or group of components. 3 | 4 | When there is a single target component, that would 5 | be used for the `Target Root` parameter. 6 | When there are multiple target components, that would 7 | be a parent that contains all of those components, or 8 | the root (which is a parent of every component). 9 | 10 | Mappings use paths that are relative to the root. 11 | That can be used for being able to move/copy targets 12 | without redoing all of the mappings. 13 | -------------------------------------------------------------------------------- /control/control_target.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/control/control_target.tox -------------------------------------------------------------------------------- /control/device_control_display.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/control/device_control_display.tox -------------------------------------------------------------------------------- /control/device_display.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/control/device_display.tox -------------------------------------------------------------------------------- /control/mftwister.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/control/mftwister.tox -------------------------------------------------------------------------------- /corner_rect.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/corner_rect.tox -------------------------------------------------------------------------------- /file_logger.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/file_logger.tox -------------------------------------------------------------------------------- /hexagonal_grid.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/hexagonal_grid.tox -------------------------------------------------------------------------------- /lib/_stubs/ArcBallExt.py: -------------------------------------------------------------------------------- 1 | class ArcBallExt(): 2 | """ 3 | The ArcBallExt Class helps with interactively controlling a Camera COMP 4 | given a Container COMP with the Mouse UV Buttons Parameters for left, 5 | middle and right enabled. 6 | 7 | Attributes 8 | ---------- 9 | ownerComp : OP 10 | Reference to the COMP this Class is initiated by. 11 | 12 | Methods 13 | ------- 14 | StartTransform(btn=None, u=0, v=0) 15 | Will begin a transform depending on the mouse button pressed. 16 | Transform(btn=None, u=0, v=0, scaler=1) 17 | Applies a transform to the ArcBall depending on the mouse button pressed. 18 | Reset() 19 | Resets the ArcBall. 20 | LoadTransform(dat=None,matrix=None) 21 | Given a tableDAT or a tdu.Matrix() it can be used to recall a saved transformation. 22 | SaveTransform(dat=op('newMat')) 23 | Will save out the current ArcBall's transformation matrix to a tableDAT. If no 24 | TableDAT is given, the internal newMat TableDAT is being used. 25 | fillMat() 26 | Utility function used by the ArcBall. 27 | """ 28 | def __init__(self, ownerComp : OP): 29 | #The component to which this extension is attached 30 | self.ownerComp = ownerComp 31 | self.arcInst = tdu.ArcBall(forCamera=True) 32 | self.matrix = tdu.Matrix() 33 | 34 | # initialize ArcBall to saved out transformation matrix 35 | matrixDAT = op('newMat') 36 | for i in range(4): 37 | for j in range(4): 38 | self.matrix[i,j] = float(matrixDAT[i,j]) 39 | 40 | matCopy = tdu.Matrix(self.matrix) 41 | self.arcInst.setTransform(matCopy) 42 | 43 | def StartTransform(self, btn : str = None, u : float = 0, v : float = 0) -> None: 44 | """ 45 | 46 | Parameters 47 | ---------- 48 | btn : str 49 | The mouse btn pressed. Can be one of 'lselect', 'rselect' or 'mselect' corrseponding to 'rotate', 'pan' and 'zoom'. 50 | u : float 51 | The horizontal mouse position on the control panel. 52 | v : float 53 | The vertical mouse position on the control panel. 54 | """ 55 | 56 | if btn == 'lselect': 57 | #if lselect ==> rotate 58 | self.arcInst.beginRotate(u,v) 59 | elif btn == 'rselect': 60 | #if rselect ==> pan 61 | self.arcInst.beginPan(u,v) 62 | elif btn == 'mselect': 63 | #if mselect ==> zoom 64 | self.arcInst.beginDolly(u,v) 65 | 66 | return 67 | 68 | def Transform(self, btn : str = None, u : float = 0, v : float = 0, scaler : float = 1) -> None: 69 | """ 70 | 71 | Parameters 72 | ---------- 73 | btn : str 74 | The mouse btn pressed. Can be one of 'lselect', 'rselect' or 'mselect' corrseponding to 'rotate', 'pan' and 'zoom'. 75 | u : float 76 | The horizontal mouse position on the control panel. 77 | v : float 78 | The vertical mouse position on the control panel. 79 | scaler : float 80 | A multiplier to increase or decrease the transformation. 81 | """ 82 | 83 | if btn == 'lselect': 84 | #if lselect ==> rotate 85 | self.arcInst.rotateTo(u,v,scale=scaler) 86 | elif btn == 'rselect': 87 | #if rselect ==> pan 88 | self.arcInst.panTo(u,v,scale=scaler) 89 | elif btn == 'mselect': 90 | #if mselect ==> zoom 91 | self.arcInst.dollyTo(u,v,scale=scaler) 92 | 93 | self.fillMat() 94 | return 95 | 96 | def Reset(self) -> None: 97 | op('autoRotate/hold1').par.pulse.pulse() 98 | op('autoRotate/hold2').par.pulse.pulse() 99 | op('autoRotate/hold3').par.pulse.pulse() 100 | self.arcInst.identity() 101 | self.fillMat() 102 | return 103 | 104 | def LoadTransform(self, dat : tableDAT = None, matrix : tdu.Matrix = None) -> None: 105 | """ 106 | Parameters 107 | ---------- 108 | dat : tableDAT 109 | An tableDAT operator reference to the tableDAT that holds the matrix to be loaded. 110 | matrix : tdu.MAtrix 111 | A tdu.Matrix object that holds the matrix to be loaded. 112 | """ 113 | 114 | if dat and dat.numRows == 4 and dat.numCols == 4: 115 | matrix = tdu.Matrix() 116 | for i in range(4): 117 | for j in range(4): 118 | matrix[i,j] = float(dat[i,j]) 119 | 120 | self.arcInst.setTransform(matrix) 121 | self.fillMat() 122 | return 123 | 124 | def SaveTransform(self, dat : tableDAT = op('newMat')) -> None: 125 | """ 126 | Parameters 127 | ---------- 128 | dat : tableDAT 129 | A tableDAT operator reference to the tableDAT where to write the current transform matrix into. 130 | """ 131 | 132 | if dat.OPType == 'tableDAT': 133 | self.matrix.fillTable(dat) 134 | 135 | def fillMat(self): 136 | newMat = self.arcInst.transform() 137 | self.matrix = newMat 138 | self.ownerComp.cook(force=True) 139 | newMat.fillTable(op('newMat')) 140 | return -------------------------------------------------------------------------------- /lib/_stubs/CallbacksExt.py: -------------------------------------------------------------------------------- 1 | # This file and all related intellectual property rights are 2 | # owned by Derivative Inc. ("Derivative"). The use and modification 3 | # of this file is governed by, and only permitted under, the terms 4 | # of the Derivative [End-User License Agreement] 5 | # [https://www.derivative.ca/Agreements/UsageAgreementTouchDesigner.asp] 6 | # (the "License Agreement"). Among other terms, this file can only 7 | # be used, and/or modified for use, with Derivative's TouchDesigner 8 | # software, and only by employees of the organization that has licensed 9 | # Derivative's TouchDesigner software by [accepting] the License Agreement. 10 | # Any redistribution or sharing of this file, with or without modification, 11 | # to or with any other person is strictly prohibited [(except as expressly 12 | # permitted by the License Agreement)]. 13 | # 14 | # Version: 099.2017.30440.28Sep 15 | # 16 | # _END_HEADER_ 17 | 18 | # callbacks extension 19 | 20 | import traceback 21 | import reprlib 22 | 23 | #short form repr for print callbacks 24 | shortRepr = reprlib.Repr() 25 | shortRepr.maxlevel = 3 26 | shortRepr.maxlist = 100 27 | shortRepr.maxdict = 100 28 | shortRepr.maxtuple = 100 29 | shortRepr.maxset = 100 30 | shortRepr.maxfrozenset = 100 31 | shortRepr.maxdeque = 3 32 | shortRepr.maxlong = 20 33 | shortRepr.maxstring = 200 34 | shortRepr.maxother = 200 35 | 36 | class CallbacksExt: 37 | """ 38 | Component extension providing a python callback system. Just call 39 | DoCallback( callbackname, callbackInfo dictionary). See DoCallback method 40 | for details. 41 | 42 | Assigned callbacks can be created for easier, more specific use. See 43 | SetAssignedCallback for details. 44 | 45 | This extension looks for two parameters on its component: A toggle named 46 | Printcallbacks, and a DAT named Callbackdat. If these aren't present, the 47 | (non-promoted!) members callbackDat and printCallbacks can be set. 48 | 49 | If you want to set up a chain of callback targets, use PassCallbacksTo to 50 | set the next target. This target can be a dat or a function. 51 | Unfound callbacks will then be sent to the appropriate callback in the dat 52 | or to the function. Also, from within a callback, you can call 53 | info['ownerComp'].PassOnCallback(info) 54 | to pass it on to the target. If you need to return a value, use: 55 | info['returnValue'] = 56 | return info['ownerComp'].PassOnCallback(info) 57 | NOTE: Returning values in chained callbacks is tricky and requires special 58 | attention both to what is being ultimately returned and how the 59 | receiver is looking at it. 60 | """ 61 | 62 | def __init__(self, ownerComp): 63 | #The component to which this extension is attached 64 | self.ownerComp = ownerComp 65 | self.AssignedCallbacks = {} 66 | self.PassTarget = None 67 | self._printCallbacks = False 68 | if hasattr(ownerComp.par, 'Callbackdat'): 69 | self.callbackDat = ownerComp.par.Callbackdat.eval() 70 | if self.callbackDat: 71 | try: 72 | if self.callbackDat.text.strip(): 73 | self.callbackDat.module 74 | except: 75 | print(traceback.format_exc()) 76 | self.ownerComp.addScriptError("Error in Callback DAT. " 77 | "See textport for details.") 78 | raise 79 | 80 | else: 81 | self.callbackDat = None 82 | 83 | # repr function for short prints 84 | self.shortRepr = shortRepr 85 | 86 | def SetAssignedCallback(self, callbackName, callback): 87 | """ 88 | An assigned callback is a callback system made to call specified python 89 | methods rather than searching a callback DAT. 90 | 91 | callbackName is the name to be passed to the DoAssignedCallback method 92 | callback is a python function that takes a single callbackInfo argument, 93 | just like standard callbacks. If callback is None, callbackName is 94 | removed from the assigned callback system. 95 | details is extra info to be passed in the ['details'] key of infoDict 96 | when callback is called. callbackName will also be added to infoDict 97 | """ 98 | if callback is not None: 99 | if callable(callback): 100 | self.AssignedCallbacks[callbackName] = callback 101 | else: 102 | raise TypeError("SetAssignedCallback" + callbackName + 103 | "attempted to assign non-callable object: " 104 | + callback) 105 | else: 106 | try: 107 | del self.AssignedCallbacks[callbackName] 108 | except: 109 | pass 110 | 111 | # def DoAssignedCallback(self, callbackName, callbackInfo=None): 112 | # """ 113 | # Perform the assigned callback with callbackName. See DoCallback for 114 | # full details. 115 | # """ 116 | # try: 117 | # callback, details = self.AssignedCallbacks[callbackName] 118 | # except: 119 | # if self.PrintCallbacks: 120 | # debug(callbackInfo) 121 | # self.DoCallback(callbackName + " (assigned callback)", 122 | # callbackInfo, None) 123 | # return 124 | # if callbackInfo is None: 125 | # callbackInfo = {'callbackName': callbackName} 126 | # if details is not None: 127 | # callbackInfo['details'] = details 128 | # self.DoCallback(callbackName, callbackInfo, callback) 129 | # 130 | def DoCallback(self, callbackName, callbackInfo=None, callbackOrDat=None): 131 | """ 132 | If it exists, call the named callback in ownerComp.par.Callbackdat. 133 | Pass any data inside callbackInfo. callbackInfo['ownerComp'] is set to 134 | self.ownerComp. If callback needs special instructions, such as looking 135 | for return data, put them in an callbackInfo['about'] 136 | 137 | callbackOrDat is used for redirection to a DAT or specific function 138 | 139 | If callback is provided, the mod search is skipped and it will be 140 | called instead. 141 | 142 | If a user callback was found, returns callbackInfo with the callback 143 | return value in callbackInfo['returnValue']. If no callback found, 144 | returns None. 145 | 146 | If ownerComp has a parameter called Printcallbacks, and that parameter 147 | is True, callbacks will be printed when called. 148 | """ 149 | if callable(callbackOrDat): 150 | callback = callbackOrDat 151 | else: 152 | callback = self.AssignedCallbacks.get(callbackName) 153 | if not callback: 154 | if callbackOrDat: 155 | moduleDat = callbackOrDat 156 | else: 157 | try: 158 | self.callbackDat = moduleDat = \ 159 | self.ownerComp.par.Callbackdat.eval() 160 | except: 161 | self.callbackDat = moduleDat = None 162 | try: 163 | try: 164 | callbackMod = self.callbackDat.module 165 | callback = getattr(callbackMod, callbackName, None) 166 | except: 167 | pass 168 | except: 169 | if moduleDat: 170 | print(self.ownerComp, "Invalid callback DAT:", 171 | moduleDat.path) 172 | raise 173 | else: 174 | if not self.PrintCallbacks: 175 | # callback dat is blank and no print, just forget it. 176 | return 177 | callback = None 178 | if callbackInfo is None: 179 | callbackInfo = {} 180 | callbackInfo.setdefault('ownerComp', self.ownerComp) 181 | callbackInfo['callbackName'] = callbackName 182 | # do callback if found 183 | if callback: 184 | # the next line is the actual function call 185 | # put returnValue into the callbackInfo dict 186 | callbackInfo['returnValue'] = callback(callbackInfo) 187 | retvalue = callbackInfo 188 | printCallback = self.PrintCallbacks 189 | # pass callback on if pass target 190 | elif self.PassTarget and self.PassTarget != callbackOrDat: 191 | callbackInfo['returnValue'] = self.PassOnCallback(callbackInfo) 192 | retvalue = callbackInfo 193 | printCallback = False 194 | # no callback 195 | else: 196 | retvalue = None 197 | printCallback = self.PrintCallbacks 198 | # print callback 199 | if printCallback: 200 | if retvalue is None or callback and self.PassTarget: 201 | notfound = 'NOT FOUND -' 202 | else: 203 | notfound = '-' 204 | # print(callbackName, notfound,'callbackInfo: ', 205 | # self.shortRepr.repr(callbackInfo), '\n') 206 | debug(callbackName, notfound,'callbackInfo: ', 207 | callbackInfo, '\n') 208 | return retvalue 209 | 210 | def PassCallbacksTo(self, passTarget): 211 | """ 212 | Set a target DAT or function for passing on unfound callbacks to. 213 | """ 214 | if callable(passTarget): 215 | self.PassTarget = passTarget 216 | elif isinstance(passTarget, DAT): 217 | try: 218 | passTarget.module 219 | except: 220 | self.ownerComp.error = traceback.format_exc() + \ 221 | "\nError in Callback DAT. See textport for details." 222 | raise 223 | self.PassTarget = passTarget 224 | else: 225 | raise TypeError('PassCallbacksTo target must be callable or DAT. ' 226 | 'Got ' + str(passTarget) + '.') 227 | 228 | def PassOnCallback(self, info): 229 | """ 230 | Pass this callback to ContextExt's PassTarget. Use PassCallbacksTo to 231 | set target 232 | """ 233 | callbackName = info['callbackName'] 234 | firstReturnValue = info.get('returnValue',None) 235 | returnDict = self.DoCallback(callbackName, info, self.PassTarget) 236 | if returnDict: 237 | if returnDict['returnValue'] is None: 238 | return firstReturnValue 239 | else: 240 | return returnDict['returnValue'] 241 | else: 242 | return firstReturnValue 243 | 244 | @property 245 | def CallbackDat(self): 246 | return self.callbackDat 247 | @CallbackDat.setter 248 | def CallbackDat(self, val): 249 | self.callbackDat = val 250 | if hasattr(ownerComp.par, 'Callbackdat'): 251 | ownerComp.par.Callbackdat.val = self.callbackDat 252 | 253 | @property 254 | def PrintCallbacks(self): 255 | if hasattr(self.ownerComp.par, 'Printcallbacks'): 256 | return self.ownerComp.par.Printcallbacks.eval() 257 | else: 258 | return self._printCallbacks 259 | @PrintCallbacks.setter 260 | def PrintCallbacks(self, val): 261 | if hasattr(self.ownerComp.par, 'Printcallbacks'): 262 | self.ownerComp.par.Printcallbacks = val 263 | else: 264 | self._printCallbacks = val 265 | -------------------------------------------------------------------------------- /lib/_stubs/PopDialogExt.py: -------------------------------------------------------------------------------- 1 | # This file and all related intellectual property rights are 2 | # owned by Derivative Inc. ("Derivative"). The use and modification 3 | # of this file is governed by, and only permitted under, the terms 4 | # of the Derivative [End-User License Agreement] 5 | # [https://www.derivative.ca/Agreements/UsageAgreementTouchDesigner.asp] 6 | # (the "License Agreement"). Among other terms, this file can only 7 | # be used, and/or modified for use, with Derivative's TouchDesigner 8 | # software, and only by employees of the organization that has licensed 9 | # Derivative's TouchDesigner software by [accepting] the License Agreement. 10 | # Any redistribution or sharing of this file, with or without modification, 11 | # to or with any other person is strictly prohibited [(except as expressly 12 | # permitted by the License Agreement)]. 13 | # 14 | # Version: 099.2017.30440.28Sep 15 | # 16 | # _END_HEADER_ 17 | 18 | from TDStoreTools import StorageManager 19 | import TDFunctions as TDF 20 | class PopDialogExt: 21 | 22 | def __init__(self, ownerComp): 23 | """ 24 | Popup dialog extension. Just call the DoPopup method to create a popup. 25 | Provide info in that method. This component can be used over and over, 26 | no need for a different component for each dialog, unless you want to 27 | change the insides. 28 | """ 29 | self.ownerComp = ownerComp 30 | self.windowComp = ownerComp.op('popDialogWindow') 31 | self.details = None 32 | 33 | # upgrade version 34 | h = self.ownerComp.par.h 35 | if h.mode == ParMode.EXPRESSION and h.expr == "op('./dialog').par.h": 36 | h.expr = "me.DialogHeight" 37 | h.readOnly = True 38 | 39 | TDF.createProperty(self, 'EnteredText', value='') 40 | TDF.createProperty(self, 'TextHeight', value=0) 41 | run("args[0].UpdateTextHeight()", self, delayFrames=1, 42 | delayRef=op.TDResources) 43 | 44 | def OpenDefault(self, text='', title='', buttons=('OK',), callback=None, 45 | details=None, textEntry=False, escButton=1, 46 | escOnClickAway=True, enterButton=1): 47 | self.Open(text, title, buttons, callback, details, textEntry, escButton, 48 | escOnClickAway, enterButton) 49 | 50 | def Open(self, text=None, title=None, buttons=None, callback=None, 51 | details=None, textEntry=None, escButton=None, 52 | escOnClickAway=None, enterButton=None): 53 | """ 54 | Open a popup dialog. 55 | text goes in the center of the dialog. Default None, use pars. 56 | title goes on top of the dialog. Blank means no title bar. Default None, 57 | use pars 58 | buttons is a list of strings. The number of buttons is equal to the 59 | number of buttons, up to 4. Default is ['OK'] 60 | callback: a method that will be called when a selection is made, see the 61 | SetCallback method. This is in addition to all internal callbacks. 62 | If not provided, Callback DAT will be searched. 63 | details: will be passed to callback in addition to item chosen. 64 | Default is None. 65 | If textEntry is a string, display textEntry field and use the string 66 | as a default. If textEntry is False, no entry field. Default is 67 | None, use pars 68 | escButton is a number from 1-4 indicating which button is simulated when 69 | esc is pressed or False for no button simulation. Default is None, 70 | use pars. First button is 1 not 0!!! 71 | enterButton is a number from 1-4 indicating which button is simulated 72 | when enter is pressed or False for no button simulation. Default is 73 | None, use pars. First button is 1 not 0!!! 74 | escOnClickAway is a boolean indicating whether esc is simulated when user 75 | clicks somewhere besides the dialog. Default is None, use pars 76 | """ 77 | self.Close() 78 | # text and title 79 | if text is not None: 80 | self.ownerComp.par.Text = text 81 | if title is not None: 82 | self.ownerComp.par.Title = title 83 | # buttons 84 | if buttons is not None: 85 | if not isinstance(buttons, list): 86 | buttons = ['OK'] 87 | self.ownerComp.par.Buttons = len(buttons) 88 | for i, label in enumerate(buttons[:4]): 89 | getattr(self.ownerComp.par, 90 | 'Buttonlabel' + str(i + 1)).val = label 91 | # callbacks 92 | if callback: 93 | ext.CallbacksExt.SetAssignedCallback('onSelect', callback) 94 | else: 95 | ext.CallbacksExt.SetAssignedCallback('onSelect', None) 96 | # textEntry 97 | if textEntry is not None: 98 | if isinstance(textEntry, str): 99 | self.ownerComp.par.Textentryarea = True 100 | self.ownerComp.par.Textentrydefault = str(textEntry) 101 | elif textEntry: 102 | self.ownerComp.par.Textentryarea = True 103 | self.ownerComp.par.Textentrydefault = '' 104 | else: 105 | self.ownerComp.par.Textentryarea = False 106 | self.EnteredText = self.ownerComp.par.Textentrydefault.eval() 107 | self.details = details 108 | self.ownerComp.op('entry/inputText').par.text = self.EnteredText 109 | self.ownerComp.op('entry').cook(force=True) 110 | if escButton is not None: 111 | if escButton is False or not (1 <= escButton <= 4): 112 | self.ownerComp.par.Escbutton = 'None' 113 | else: 114 | self.ownerComp.par.Escbutton = str(escButton) 115 | if escOnClickAway is not None: 116 | self.ownerComp.par.Esconclickaway = escOnClickAway 117 | if enterButton is not None: 118 | if enterButton is False or not (1 <= enterButton <= 4): 119 | self.ownerComp.par.Enterbutton = 'None' 120 | else: 121 | self.ownerComp.par.Enterbutton = str(enterButton) 122 | self.UpdateTextHeight() 123 | # HACK shouldn't be necessary - problem with clones/replicating 124 | self.ownerComp.op('replicator1').par.recreateall.pulse() 125 | run("op('" + self.ownerComp.path + "').ext.PopDialogExt.actualOpen()", 126 | delayFrames=1, delayRef=op.TDResources) 127 | 128 | def actualOpen(self): 129 | # needs to be deferred so that sizes can update properly 130 | self.windowComp.par.winopen.pulse() 131 | ext.CallbacksExt.DoCallback('onOpen') 132 | if self.ownerComp.op('entry').par.display.eval(): 133 | # self.ownerComp.setFocus() 134 | # hack shouldn't have to wait a frame 135 | run('op("' + self.ownerComp.path + '").op("entry/inputText").' 136 | 'setKeyboardFocus(selectAll=True)', 137 | delayFrames=1, delayRef=op.TDResources) 138 | else: 139 | self.ownerComp.setFocus() 140 | 141 | def Close(self): 142 | """ 143 | Close the dialog 144 | """ 145 | ext.CallbacksExt.SetAssignedCallback('onSelect', None) 146 | ext.CallbacksExt.DoCallback('onClose') 147 | self.windowComp.par.winclose.pulse() 148 | self.ownerComp.op('entry/inputText').par.text = self.EnteredText 149 | 150 | def OnButtonClicked(self, buttonNum): 151 | """ 152 | Callback from buttons 153 | """ 154 | infoDict = {'buttonNum': buttonNum, 155 | 'button': getattr(self.ownerComp.par, 156 | 'Buttonlabel' + str(buttonNum)).eval(), 157 | 'details': self.details} 158 | if self.ownerComp.par.Textentryarea.eval(): 159 | infoDict['enteredText'] = self.EnteredText 160 | try: 161 | ext.CallbacksExt.DoCallback('onSelect', infoDict) 162 | finally: 163 | self.Close() 164 | 165 | def OnKeyPressed(self, key): 166 | """ 167 | Callback for esc or enterpressed. 168 | """ 169 | if key == 'esc' and self.ownerComp.par.Escbutton.eval() != 'None': 170 | button = int(self.ownerComp.par.Escbutton.eval()) 171 | if button <= self.ownerComp.par.Buttons: 172 | self.OnButtonClicked(button) 173 | if key == 'enter' and self.ownerComp.par.Enterbutton.eval() != 'None': 174 | button = int(self.ownerComp.par.Enterbutton.eval()) 175 | if button <= self.ownerComp.par.Buttons: 176 | self.OnButtonClicked(button) 177 | 178 | def OnClickAway(self): 179 | """ 180 | Callback for esc pressed. Only happens when Escbutton is not None 181 | """ 182 | if self.ownerComp.par.Esconclickaway.eval(): 183 | self.OnKeyPressed('esc') 184 | 185 | def OnParValueChange(self, par, val, prev): 186 | """ 187 | Callback for when parameters change 188 | """ 189 | if par.name == "Textentryarea": 190 | self.ownerComp.par.Textentrydefault.enable = par.eval() 191 | if par.name == "Escbutton": 192 | self.ownerComp.par.Esconclickaway.enable = par.eval() != "None" 193 | 194 | def OnParPulse(self, par): 195 | if par.name == "Open": 196 | self.Open() 197 | elif par.name == "Close": 198 | self.Close() 199 | elif par.name == 'Editcallbacks': 200 | dat = self.ownerComp.par.Callbackdat.eval() 201 | if dat: 202 | dat.par.edit.pulse() 203 | else: 204 | print("No callback dat for", self.ownerComp.path) 205 | elif par.name == 'Helppage': 206 | ui.viewFile('https://docs.derivative.ca/' 207 | 'index.php?title=Palette:popDialog') 208 | 209 | def UpdateTextHeight(self): 210 | self.TextHeight = self.ownerComp.op('text/text').evalTextSize( 211 | self.ownerComp.par.Text)[1] 212 | 213 | @property 214 | def DialogHeight(self): 215 | return 65 + self.TextHeight + \ 216 | (20 if self.ownerComp.par.Title else 0) + \ 217 | (37 if self.ownerComp.par.Textentryarea else 0) -------------------------------------------------------------------------------- /lib/_stubs/TDCallbacksExt.py: -------------------------------------------------------------------------------- 1 | # This file and all related intellectual property rights are 2 | # owned by Derivative Inc. ("Derivative"). The use and modification 3 | # of this file is governed by, and only permitted under, the terms 4 | # of the Derivative [End-User License Agreement] 5 | # [https://www.derivative.ca/Agreements/UsageAgreementTouchDesigner.asp] 6 | # (the "License Agreement"). Among other terms, this file can only 7 | # be used, and/or modified for use, with Derivative's TouchDesigner 8 | # software, and only by employees of the organization that has licensed 9 | # Derivative's TouchDesigner software by [accepting] the License Agreement. 10 | # Any redistribution or sharing of this file, with or without modification, 11 | # to or with any other person is strictly prohibited [(except as expressly 12 | # permitted by the License Agreement)]. 13 | # 14 | # Version: 099.2017.30440.28Sep 15 | # 16 | # _END_HEADER_ 17 | 18 | # callbacks extension 19 | 20 | import traceback 21 | import reprlib 22 | 23 | #short form repr for print callbacks 24 | shortRepr = reprlib.Repr() 25 | shortRepr.maxlevel = 3 26 | shortRepr.maxlist = 100 27 | shortRepr.maxdict = 100 28 | shortRepr.maxtuple = 100 29 | shortRepr.maxset = 100 30 | shortRepr.maxfrozenset = 100 31 | shortRepr.maxdeque = 3 32 | shortRepr.maxlong = 20 33 | shortRepr.maxstring = 200 34 | shortRepr.maxother = 200 35 | 36 | class CallbacksExt: 37 | """ 38 | Component extension providing a python callback system. Just call 39 | DoCallback( callbackname, callbackInfo dictionary). See DoCallback method 40 | for details. 41 | 42 | Assigned callbacks can be created for easier, more specific use. See 43 | SetAssignedCallback for details. 44 | 45 | This extension looks for two parameters on its component: A toggle named 46 | Printcallbacks, and a DAT named Callbackdat. If these aren't present, the 47 | (non-promoted!) members callbackDat and printCallbacks can be set. 48 | 49 | If you want to set up a chain of callback targets, use PassCallbacksTo to 50 | set the next target. This target can be a dat or a function. 51 | Unfound callbacks will then be sent to the appropriate callback in the dat 52 | or to the function. Also, from within a callback, you can call 53 | info['ownerComp'].PassOnCallback(info) 54 | to pass it on to the target. If you need to return a value, use: 55 | info['returnValue'] = 56 | return info['ownerComp'].PassOnCallback(info) 57 | NOTE: Returning values in chained callbacks is tricky and requires special 58 | attention both to what is being ultimately returned and how the 59 | receiver is looking at it. 60 | """ 61 | 62 | def __init__(self, ownerComp): 63 | #The component to which this extension is attached 64 | self.ownerComp = ownerComp 65 | self.AssignedCallbacks = {} 66 | self.PassTarget = None 67 | self._printCallbacks = False 68 | if hasattr(ownerComp.par, 'Callbackdat'): 69 | self.callbackDat = ownerComp.par.Callbackdat.eval() 70 | if self.callbackDat: 71 | try: 72 | if self.callbackDat.text.strip(): 73 | self.callbackDat.module 74 | except: 75 | print(traceback.format_exc()) 76 | self.ownerComp.addScriptError("Error in Callback DAT. " 77 | "See textport for details.") 78 | raise 79 | 80 | else: 81 | self.callbackDat = None 82 | 83 | # repr function for short prints 84 | self.shortRepr = shortRepr 85 | 86 | def SetAssignedCallback(self, callbackName, callback): 87 | """ 88 | An assigned callback is a callback system made to call specified python 89 | methods rather than searching a callback DAT. 90 | 91 | callbackName is the name to be passed to the DoAssignedCallback method 92 | callback is a python function that takes a single callbackInfo argument, 93 | just like standard callbacks. If callback is None, callbackName is 94 | removed from the assigned callback system. 95 | details is extra info to be passed in the ['details'] key of infoDict 96 | when callback is called. callbackName will also be added to infoDict 97 | """ 98 | if callback is not None: 99 | if callable(callback): 100 | self.AssignedCallbacks[callbackName] = callback 101 | else: 102 | raise TypeError("SetAssignedCallback" + callbackName + 103 | "attempted to assign non-callable object: " 104 | + callback) 105 | else: 106 | try: 107 | del self.AssignedCallbacks[callbackName] 108 | except: 109 | pass 110 | 111 | # def DoAssignedCallback(self, callbackName, callbackInfo=None): 112 | # """ 113 | # Perform the assigned callback with callbackName. See DoCallback for 114 | # full details. 115 | # """ 116 | # try: 117 | # callback, details = self.AssignedCallbacks[callbackName] 118 | # except: 119 | # if self.PrintCallbacks: 120 | # debug(callbackInfo) 121 | # self.DoCallback(callbackName + " (assigned callback)", 122 | # callbackInfo, None) 123 | # return 124 | # if callbackInfo is None: 125 | # callbackInfo = {'callbackName': callbackName} 126 | # if details is not None: 127 | # callbackInfo['details'] = details 128 | # self.DoCallback(callbackName, callbackInfo, callback) 129 | # 130 | def DoCallback(self, callbackName, callbackInfo=None, callbackOrDat=None): 131 | """ 132 | If it exists, call the named callback in ownerComp.par.Callbackdat. 133 | Pass any data inside callbackInfo. callbackInfo['ownerComp'] is set to 134 | self.ownerComp. If callback needs special instructions, such as looking 135 | for return data, put them in an callbackInfo['about'] 136 | 137 | callbackOrDat is used for redirection to a DAT or specific function 138 | 139 | If callback is provided, the mod search is skipped and it will be 140 | called instead. 141 | 142 | If a user callback was found, returns callbackInfo with the callback 143 | return value in callbackInfo['returnValue']. If no callback found, 144 | returns None. 145 | 146 | If ownerComp has a parameter called Printcallbacks, and that parameter 147 | is True, callbacks will be printed when called. 148 | """ 149 | if callable(callbackOrDat): 150 | callback = callbackOrDat 151 | else: 152 | callback = self.AssignedCallbacks.get(callbackName) 153 | if not callback: 154 | if callbackOrDat: 155 | moduleDat = callbackOrDat 156 | else: 157 | try: 158 | self.callbackDat = moduleDat = \ 159 | self.ownerComp.par.Callbackdat.eval() 160 | except: 161 | self.callbackDat = moduleDat = None 162 | try: 163 | try: 164 | callbackMod = self.callbackDat.module 165 | callback = getattr(callbackMod, callbackName, None) 166 | except: 167 | pass 168 | except: 169 | if moduleDat: 170 | print(self.ownerComp, "Invalid callback DAT:", 171 | moduleDat.path) 172 | raise 173 | else: 174 | if not self.PrintCallbacks: 175 | # callback dat is blank and no print, just forget it. 176 | return 177 | callback = None 178 | if callbackInfo is None: 179 | callbackInfo = {} 180 | callbackInfo.setdefault('ownerComp', self.ownerComp) 181 | callbackInfo['callbackName'] = callbackName 182 | # do callback if found 183 | if callback: 184 | # the next line is the actual function call 185 | # put returnValue into the callbackInfo dict 186 | callbackInfo['returnValue'] = callback(callbackInfo) 187 | retvalue = callbackInfo 188 | printCallback = self.PrintCallbacks 189 | # pass callback on if pass target 190 | elif self.PassTarget and self.PassTarget != callbackOrDat: 191 | callbackInfo['returnValue'] = self.PassOnCallback(callbackInfo) 192 | retvalue = callbackInfo 193 | printCallback = False 194 | # no callback 195 | else: 196 | retvalue = None 197 | printCallback = self.PrintCallbacks 198 | # print callback 199 | if printCallback: 200 | if retvalue is None or callback and self.PassTarget: 201 | notfound = 'NOT FOUND -' 202 | else: 203 | notfound = '-' 204 | # print(callbackName, notfound,'callbackInfo: ', 205 | # self.shortRepr.repr(callbackInfo), '\n') 206 | debug(callbackName, notfound,'callbackInfo: ', 207 | callbackInfo, '\n') 208 | return retvalue 209 | 210 | def PassCallbacksTo(self, passTarget): 211 | """ 212 | Set a target DAT or function for passing on unfound callbacks to. 213 | """ 214 | if callable(passTarget): 215 | self.PassTarget = passTarget 216 | elif isinstance(passTarget, DAT): 217 | try: 218 | passTarget.module 219 | except: 220 | self.ownerComp.error = traceback.format_exc() + \ 221 | "\nError in Callback DAT. See textport for details." 222 | raise 223 | self.PassTarget = passTarget 224 | else: 225 | raise TypeError('PassCallbacksTo target must be callable or DAT. ' 226 | 'Got ' + str(passTarget) + '.') 227 | 228 | def PassOnCallback(self, info): 229 | """ 230 | Pass this callback to ContextExt's PassTarget. Use PassCallbacksTo to 231 | set target 232 | """ 233 | callbackName = info['callbackName'] 234 | firstReturnValue = info.get('returnValue',None) 235 | returnDict = self.DoCallback(callbackName, info, self.PassTarget) 236 | if returnDict: 237 | if returnDict['returnValue'] is None: 238 | return firstReturnValue 239 | else: 240 | return returnDict['returnValue'] 241 | else: 242 | return firstReturnValue 243 | 244 | @property 245 | def CallbackDat(self): 246 | return self.callbackDat 247 | @CallbackDat.setter 248 | def CallbackDat(self, val): 249 | self.callbackDat = val 250 | if hasattr(ownerComp.par, 'Callbackdat'): 251 | ownerComp.par.Callbackdat.val = self.callbackDat 252 | 253 | @property 254 | def PrintCallbacks(self): 255 | if hasattr(self.ownerComp.par, 'Printcallbacks'): 256 | return self.ownerComp.par.Printcallbacks.eval() 257 | else: 258 | return self._printCallbacks 259 | @PrintCallbacks.setter 260 | def PrintCallbacks(self, val): 261 | if hasattr(self.ownerComp.par, 'Printcallbacks'): 262 | self.ownerComp.par.Printcallbacks = val 263 | else: 264 | self._printCallbacks = val 265 | -------------------------------------------------------------------------------- /lib/_stubs/TDCodeGen.py: -------------------------------------------------------------------------------- 1 | # This file and all related intellectual property rights are 2 | # owned by Derivative Inc. ("Derivative"). The use and modification 3 | # of this file is governed by, and only permitted under, the terms 4 | # of the Derivative [End-User License Agreement] 5 | # [https://www.derivative.ca/Agreements/UsageAgreementTouchDesigner.asp] 6 | # (the "License Agreement"). Among other terms, this file can only 7 | # be used, and/or modified for use, with Derivative's TouchDesigner 8 | # software, and only by employees of the organization that has licensed 9 | # Derivative's TouchDesigner software by [accepting] the License Agreement. 10 | # Any redistribution or sharing of this file, with or without modification, 11 | # to or with any other person is strictly prohibited [(except as expressly 12 | # permitted by the License Agreement)]. 13 | # 14 | # Version: 099.2017.30440.28Sep 15 | # 16 | # _END_HEADER_ 17 | 18 | """TDCodeGen 19 | 20 | Helpers for automatically generated Python code. Heavily based on the ast 21 | Python library 22 | """ 23 | 24 | import ast 25 | script = op('script1_callbacks') 26 | 27 | def datFunctionInfo(dat): 28 | """ 29 | Get info about all the top level functions in a DAT 30 | 31 | Args: 32 | dat: the dat to analyze 33 | 34 | Returns: 35 | {'functionName': {'docString': docString, 'lines': line range}, ...} 36 | """ 37 | ast = datAst(dat) 38 | functions = nodeFunctions(ast) 39 | return {functionName: 40 | {'docString': nodeDocString(node), 41 | 'lines': nodeLines(node)} 42 | for functionName, node in functions.items()} 43 | 44 | def datRemoveFunction(dat, functionName): 45 | """ 46 | Remove a function from a dat 47 | 48 | Args: 49 | dat: the DAT 50 | functionName: name of function to remove 51 | """ 52 | info = datFunctionInfo(dat) 53 | if functionName in info: 54 | functionInfo = info[functionName] 55 | lines = functionInfo['lines'] 56 | lines[0] -= 1 # 1 based 57 | lines[1] -= 1 # 1 based 58 | datLines = dat.text.splitlines() 59 | # remove a blank line if surrounded on both sides 60 | if len(datLines) > lines[1] and not datLines[lines[0] - 1].strip() \ 61 | and not datLines[lines[1]].strip(): 62 | lines[1] += 1 63 | newLines = datLines[:lines[0]] + datLines[lines[1]:] 64 | dat.text = '\n'.join(newLines) 65 | 66 | def datInsertText(dat, text, insertLine=None): 67 | """ 68 | Insert text into a dat. 69 | 70 | Args: 71 | dat: the DAT 72 | text: the text to be inserted. 73 | insertLine: the line at which to insert text. If None (default), insert 74 | text after last non-pblank line in file. 75 | """ 76 | insertLines = text.splitlines() 77 | datLines = dat.text.splitlines() 78 | if insertLine is None: 79 | insertLine = len(datLines) 80 | while not datLines[insertLine-1].strip() and insertLine > 1: 81 | insertLine -= 1 82 | else: 83 | insertLine -= 1 # 1 based 84 | newText = datLines[:insertLine] + insertLines + datLines[insertLine:] 85 | dat.text = '\n'.join(newText) 86 | 87 | def datAst(dat): 88 | """ 89 | Get the ast node of a DAT holding Python code 90 | 91 | Args: 92 | dat: the dat to analyze 93 | 94 | Returns: 95 | ast node of the DAT's text. Will be ast.Module at top 96 | """ 97 | return ast.parse(dat.text) 98 | 99 | def nodeFunctions(node): 100 | """ 101 | Get info about functions at the top level of node 102 | 103 | Args: 104 | node: the ast node to search 105 | 106 | Returns: 107 | {: ast node of function} 108 | """ 109 | functionDict = {} 110 | for item in node.body: 111 | if isinstance(item, ast.FunctionDef): 112 | functionDict[item.name] = item 113 | return functionDict 114 | 115 | def nodeDocString(node): 116 | """ 117 | Get the docstring of a node 118 | 119 | Args: 120 | node: ast node 121 | 122 | Returns: 123 | docstring or none 124 | """ 125 | try: 126 | item = node.body[0] 127 | if isinstance(item, ast.Expr) and isinstance(item.value, ast.Str): 128 | return item.value.s 129 | except: 130 | return None 131 | 132 | def nodeLines(node): 133 | """ 134 | Get the line number range of a node 135 | 136 | Args: 137 | node: an ast node 138 | 139 | Returns: 140 | (start line #, end line # + 1) 141 | """ 142 | 143 | min_lineno = node.lineno 144 | max_lineno = node.lineno 145 | for node in ast.walk(node): 146 | if hasattr(node, "lineno"): 147 | min_lineno = min(min_lineno, node.lineno) 148 | max_lineno = max(max_lineno, node.lineno) 149 | return [min_lineno, max_lineno + 1] -------------------------------------------------------------------------------- /lib/_stubs/TDStoreTools.py: -------------------------------------------------------------------------------- 1 | 2 | # This file and all related intellectual property rights are 3 | # owned by Derivative Inc. ("Derivative"). The use and modification 4 | # of this file is governed by, and only permitted under, the terms 5 | # of the Derivative [End-User License Agreement] 6 | # [https://www.derivative.ca/Agreements/UsageAgreementTouchDesigner.asp] 7 | # (the "License Agreement"). Among other terms, this file can only 8 | # be used, and/or modified for use, with Derivative's TouchDesigner 9 | # software, and only by employees of the organization that has licensed 10 | # Derivative's TouchDesigner software by [accepting] the License Agreement. 11 | # Any redistribution or sharing of this file, with or without modification, 12 | # to or with any other person is strictly prohibited [(except as expressly 13 | # permitted by the License Agreement)]. 14 | # 15 | # Version: 099.2017.30440.28Sep 16 | # 17 | # _END_HEADER_ 18 | 19 | # TDStoreTools 20 | 21 | from collections.abc import MutableSet, MutableMapping, MutableSequence 22 | from abc import ABCMeta, abstractmethod 23 | from numbers import Number 24 | 25 | 26 | class StorageManager(MutableMapping): 27 | def __init__(self, extension, ownerComp, storedItems=None, 28 | restoreAllDefaults=False, sync=True, dictName=None, 29 | locked=True): 30 | """ 31 | StorageManager manages a TDStoreTools.dependDict for ease of use in 32 | extensions. 33 | extension: the extension using this StorageManager 34 | ownerComp: the comp to manage storage for 35 | storedItems: a list of dictionaries in the form: 36 | {'name': itemName, 'default': defaultValue, 'readOnly': T/F, 37 | 'property': T/F, 'dependable':T/F} 38 | 39 | if unspecified, defaultValue will be None. 40 | property defaults to True and decides whether to add item as a 41 | property of extension 42 | readOnly defaults to False and defines whether the created property 43 | is readOnly 44 | dependable defaults to False and determines if a container will be 45 | wrapped in a dependency. These are slower but will cause nodes 46 | that observe them to cook properly. 47 | capitalized properties can be promoted in the extension. 48 | restoreAllDefaults: if True, force all values to their default 49 | sync: if True, clear old items that are no longer defined and set to 50 | default value if they are new 51 | dictName: all stored data will go in this dict in storage. defaults to 52 | ownerComp.__class__.__name__ + 'Store' 53 | locked: if True, raise an exception if an attempt is made to add a value 54 | not defined in storedItems 55 | """ 56 | self._items = {} # will be set up in setItems... 57 | # {'attrName': provided dict} 58 | # this is an internal list of item info and should 59 | # not be messed with lightly 60 | 61 | self.locked = False 62 | if isinstance(ownerComp, COMP): 63 | self.ownerComp = ownerComp 64 | else: 65 | raise TypeError('Invalid owner for StorageManager', ownerComp) 66 | self.extension = extension 67 | if dictName is None: 68 | dictName = extension.__class__.__name__ + 'Stored' 69 | if dictName in ownerComp.storage: 70 | self.storageDict = ownerComp.storage[dictName] 71 | else: 72 | self.storageDict = ownerComp.store(dictName, DependDict()) 73 | setattr(extension, '_storageDict', self.storageDict) 74 | if storedItems is None: 75 | storedItems = [] 76 | self._setItems(storedItems, sync=sync) 77 | if restoreAllDefaults: 78 | self.restoreAllDefaults() 79 | self.locked = locked 80 | 81 | def restoreAllDefaults(self): 82 | """ 83 | Restore all storage items to the default value defined during init. 84 | If no default value was defined, item will be set to None 85 | """ 86 | for item, info in self._items.items(): 87 | self.storageDict[item] = info['default'] 88 | 89 | def restoreDefault(self, storageItem): 90 | if storageItem in self._items: 91 | self.storageDict[storageItem] = self._items[storageItem]['default'] 92 | 93 | def _sync(self, deleteOld=True): 94 | """ 95 | Create items in storage, make properties, and set to default if 96 | necessary. If deleteOld is True, delete stored items that aren't in item 97 | list. 98 | Should really only be done during initialization. 99 | """ 100 | if deleteOld: 101 | oldKeys = [] 102 | for key in self.storageDict: 103 | if key not in self._items: 104 | oldKeys.append(key) 105 | for key in [key for key in self.storageDict 106 | if key not in self._items]: 107 | del self.storageDict[key] 108 | for key, info in self._items.items(): 109 | if key not in self.storageDict: 110 | try: 111 | self[key] = info['default'] 112 | except: 113 | import traceback; 114 | traceback.print_exc() 115 | print('Unable to create ' + info['name'], 116 | 'on', self.ownerComp, info['default']) 117 | raise 118 | else: 119 | # check to make sure 'dependable' flag hasn't changed: 120 | if info['dependable'] and not isinstance(self[key], 121 | DependMixin): 122 | # re-setting causes dependability to be updated 123 | self.storageDict[key] = self.storageDict[key] 124 | elif not info['dependable'] and isinstance(self[key], 125 | DependMixin): 126 | try: 127 | self[key] = self[key].getRaw() # attempt to update 128 | except: 129 | print('Unable to update ' + info['name'], 130 | 'on', self.ownerComp, self[key]) 131 | raise 132 | if info['property']: 133 | self._makeProperty(key, info['readOnly']) 134 | 135 | def _makeProperty(self, key, readOnly=False): 136 | """ 137 | Creates a property on ownerComp. Really should only be done during 138 | initialization. 139 | """ 140 | if not isinstance(key, str) or not key.isidentifier(): 141 | raise ValueError('Invalid identifier in stored items', key, 142 | self.ownerComp) 143 | # def getter(s): 144 | # return s.storage[self.dictName] 145 | # debug(id(self.storageDict), 146 | # id(self.ownerComp.storage[self.dictName])) 147 | 148 | if readOnly: 149 | def setter(s, val): 150 | raise AttributeError("Can't set attribute", key, val, 151 | self.ownerComp) 152 | else: 153 | def setter(s, val): 154 | s._storageDict[key] = val 155 | prop = property(lambda s: s._storageDict[key], setter) 156 | try: 157 | setattr(self.extension.__class__, key, prop) 158 | except: 159 | print('Unable to create', key, 'property on', self.extension) 160 | raise 161 | 162 | def _setItems(self, storedItems, sync=True): 163 | """ 164 | Create values in dictionary and set up item values if necessary. 165 | items is a list of lists or dicts just like in __init__ 166 | If sync is True, perform a sync as well. 167 | """ 168 | oldItems = self._items.copy() 169 | self._items = {} 170 | for item in storedItems: 171 | self._addItem(item) 172 | for name, info in oldItems.items(): 173 | if name not in self._items and info['property'] is False: 174 | # Watch out for this! Changing the ownerComp's class! 175 | delattr(self.ownerComp.__class__, name) 176 | if sync: 177 | self._sync() 178 | 179 | def _addItem(self, storageItem): 180 | """ 181 | Add an item to StorageManager. WARNING: all instances of an extension 182 | class will share the properties of that class! 183 | """ 184 | if isinstance(storageItem, dict) and storageItem.get('name', None): 185 | storageItem.setdefault('default', None) 186 | storageItem.setdefault('readOnly', False) 187 | storageItem.setdefault('property', True) 188 | storageItem.setdefault('dependable', False) 189 | self._items[storageItem['name']] = storageItem 190 | else: 191 | raise ValueError("StorageItems must be a list of dictionaries. See " 192 | "StorageManager docs.", self.ownerComp) 193 | 194 | def _removeItem(self, itemName): 195 | """ 196 | Remove an item from StorageManager. WARNING: all instances of an 197 | extension class will share the properties of that class! 198 | """ 199 | if itemName in self._items: 200 | propertyType = self._items[itemName][1] 201 | if propertyType: 202 | delattr(self.ownerComp.__class__, itemName) 203 | del self._items[itemName] 204 | 205 | # Fake operation as dictionary 206 | 207 | def __getitem__(self, key): 208 | return self.storageDict[key] 209 | 210 | def __setitem__(self, key, val): 211 | if self.locked and key not in self.storageDict: 212 | raise KeyError("Can't create key in locked storage dictionary", 213 | key, self.ownerComp) 214 | if self._items[key]['dependable']: 215 | self.storageDict[key] = val 216 | else: 217 | # allow dict, list, set to go in raw 218 | self.storageDict.setItem(key, val, 219 | raw=isinstance(val, (dict, list, set))) 220 | 221 | def __delitem__(self, key): 222 | del self.storageDict[key] 223 | 224 | def __iter__(self): 225 | return iter(self.storageDict) 226 | 227 | def __len__(self): 228 | return len(self.storageDict) 229 | 230 | 231 | # some basic functionalities in all our Depend collections 232 | class DependMixin: 233 | __metaclass__ = ABCMeta 234 | 235 | @abstractmethod 236 | def __init__(self): 237 | self.myMainDep = tdu.Dependency() 238 | self.parentDep = None 239 | 240 | @abstractmethod 241 | def getRaw(self): 242 | """ 243 | returns dependable with dependency wrappers removed 244 | """ 245 | self.myMainDep.val # this has to be in your method definition 246 | 247 | def getDependencies(self): 248 | """ 249 | returns object with dependency wrappers intact 250 | """ 251 | return self.myItems 252 | 253 | def __len__(self): 254 | # try: 255 | # debug(self.myItems) 256 | # except: 257 | # pass 258 | self.myMainDep.val # dummy for dependency 259 | return len(self.myItems) 260 | 261 | def __str__(self): 262 | self.myMainDep.val # dummy for dependency 263 | # return str(self.myItems) 264 | return str(self.getRaw()) 265 | 266 | def __repr__(self): 267 | self.myMainDep.val # dummy for dependency 268 | return 'type: ' + self.__class__.__name__ + ' val: ' + str(self.myItems) 269 | 270 | def __iter__(self): 271 | self.myMainDep.val # dummy for dependency 272 | return iter(self.myItems) 273 | 274 | def __contains__(self, item): 275 | self.myMainDep.val # dummy for dependency 276 | return item in self.myItems 277 | 278 | def __del__(self): 279 | self.myMainDep.modified() 280 | 281 | def copy(self): 282 | return self.__class__(self.myItems) 283 | 284 | 285 | class DependDict(DependMixin, MutableMapping): 286 | def __init__(self, *args, **kwargs): 287 | DependMixin.__init__(self) 288 | self.myItems = dict() 289 | self.update(dict(*args, **kwargs)) # use the free update to set keys 290 | 291 | @property 292 | def val(self): 293 | return self 294 | 295 | @val.setter 296 | def val(self, value): 297 | try: 298 | self.clear() 299 | self.update(value) 300 | self.myMainDep.modified() 301 | except: 302 | print("DependDict.val can only be set to a dict") 303 | 304 | def getRaw(self, key=None): 305 | self.myMainDep.val 306 | if key is None: 307 | return {key: item.val.getRaw() 308 | if isinstance(item.val, DependMixin) else item.val 309 | for key, item in self.myItems.items()} 310 | if isinstance(self.myItems[key], DependMixin): 311 | return self.myItems[key].getRaw() 312 | else: 313 | return self.myItems[key].val 314 | 315 | def __getitem__(self, key): 316 | try: 317 | item = self.myItems[key] 318 | return item.val 319 | except: 320 | self.myMainDep.val # dummy for dependency 321 | raise 322 | 323 | def getDependency(self, key): 324 | return self.myItems[key] 325 | 326 | def __setitem__(self, key, item): 327 | self.setItem(key, item) 328 | 329 | def setItem(self, key, item, raw=False): 330 | if key in self.myItems: 331 | if raw or isImmutable(item): 332 | self.myItems[key].val = item 333 | return 334 | else: 335 | self.myItems[key].modified() 336 | newv = makeDependable(self, item, raw) 337 | self.myItems[key] = newv 338 | self.myMainDep.modified() 339 | 340 | def clear(self): 341 | try: 342 | for i in self.myItems: 343 | i.modified() 344 | except: 345 | pass 346 | self.myItems.clear() 347 | self.myMainDep.modified() 348 | 349 | # def update(self, *args, **kwargs): 350 | # self.myItems.update(*args, **kwargs) 351 | 352 | def __delitem__(self, key): 353 | self.myItems[key].modified() 354 | del self.myItems[key] 355 | self.myMainDep.modified() 356 | 357 | 358 | 359 | class DependList(DependMixin, MutableSequence): 360 | def __init__(self, arg=None): 361 | if arg is None: 362 | arg = [] 363 | DependMixin.__init__(self) 364 | self.myItems = [] 365 | for i in arg: 366 | self.append(i) 367 | 368 | @property 369 | def val(self): 370 | return self 371 | 372 | @val.setter 373 | def val(self, value): 374 | if isinstance(value, list): 375 | self.clear() 376 | for i in value: 377 | self.append(i) 378 | self.myMainDep.modified() 379 | else: 380 | raise TypeError("DependDict.val can only be set to a dict") 381 | 382 | def getRaw(self, index=None): 383 | self.myMainDep.val 384 | if index is None: 385 | return [item.val.getRaw() if isinstance(item.val, DependMixin) 386 | else item.val for item in self.myItems] 387 | if isinstance(self.myItems[index].val, DependMixin): 388 | return self.myItems[index].val.getRaw() 389 | else: 390 | return self.myItems[index].val 391 | 392 | def append(self, value, raw=False): 393 | self.insert(len(self.myItems), value, raw) 394 | 395 | def insert(self, index, item, raw=False): 396 | newitem = makeDependable(self, item, raw) 397 | self.myItems.insert(index, newitem) 398 | for i in range(index, len(self.myItems)): 399 | self.myItems[i].modified() 400 | self.myMainDep.modified() 401 | 402 | def __getitem__(self, index): 403 | try: 404 | item = self.myItems[index] 405 | return item.val 406 | except: 407 | self.myMainDep.val # dummy for dependency 408 | raise 409 | 410 | def __setitem__(self, index, item): 411 | self.setItem(index, item) 412 | 413 | def setItem(self, index, item, raw=False): 414 | newv = makeDependable(self, item, raw) 415 | self.myItems[index] = newv 416 | if 0 <= index < len(self.myItems): 417 | if raw or isImmutable(item): 418 | self.myItems[index].val = item 419 | return 420 | else: 421 | self.myItems[index].modified() 422 | self.myMainDep.modified() 423 | 424 | def getDependency(self, index): 425 | return self.myItems[index] 426 | 427 | def __iter__(self): 428 | self.myMainDep.val # dummy for dependency 429 | return iter([i.val for i in self.myItems]) 430 | 431 | def clear(self): 432 | self.myItems.clear() 433 | self.myMainDep.modified() 434 | 435 | def __delitem__(self, index): 436 | del self.myItems[index] 437 | for i in range(index, len(self.myItems)): 438 | self.myItems[i].modified() 439 | self.myMainDep.modified() 440 | 441 | # def pop(self, *args, **kwargs): 442 | # self.myMainDep.modified() 443 | # return self.myItems.pop(*args, **kwargs) 444 | 445 | class DependSet(DependMixin, MutableSet): 446 | """ 447 | DependSet is a bit different in that we don't need to convert items inside 448 | to dependencies. 449 | """ 450 | 451 | def __init__(self, *args, **kwargs): 452 | DependMixin.__init__(self) 453 | self.myItems = set(*args, **kwargs) 454 | 455 | @property 456 | def val(self): 457 | return self 458 | 459 | @val.setter 460 | def val(self, value): 461 | try: 462 | self.clear() 463 | self.myItems = value 464 | self.myMainDep.modified() 465 | except: 466 | print("DependSet.val set to bad value") 467 | 468 | def getRaw(self): 469 | self.myMainDep.val 470 | return self.myItems 471 | 472 | def add(self, item): 473 | if item in self.myItems: 474 | return 475 | self.myItems.add(item) 476 | self.myMainDep.modified() 477 | 478 | def discard(self, item): 479 | if item not in self.myItems: 480 | return 481 | self.myItems.discard(item) 482 | self.myMainDep.modified() 483 | 484 | def update(self, *args, **kwargs): 485 | self.myItems.update(*args, **kwargs) 486 | self.myMainDep.modified() 487 | 488 | def clear(self): 489 | self.myItems.clear() 490 | self.myMainDep.modified() 491 | 492 | def union(self, *args): 493 | return self.myItems.union(*args) 494 | 495 | def difference(self, *args): 496 | return self.myItems.difference(*args) 497 | 498 | def intersection(self, *args): 499 | return self.myItems.intersection(*args) 500 | 501 | def symmetric_difference(self, *args): 502 | return self.myItems.symmetric_difference(*args) 503 | 504 | def issuperset(self, *args): 505 | return self.myItems.issuperset(*args) 506 | 507 | def isImmutable(item): 508 | if item is None: 509 | return True 510 | if isinstance(item, Number): 511 | return True 512 | if type(item) in (str, tuple, frozenset): 513 | return True 514 | return False 515 | 516 | 517 | def makeDependable(parentDep, value, raw=False): 518 | if raw and isinstance(value, (dict, list, set)): 519 | newv = value 520 | elif isinstance(value, dict): 521 | newv = DependDict(value) 522 | newv.parentDep = parentDep 523 | elif isinstance(value, list): 524 | newv = DependList(value) 525 | newv.parentDep = parentDep 526 | elif isinstance(value, set): 527 | newv = DependSet(value) 528 | newv.parentDep = parentDep 529 | elif isinstance(value, DependMixin): 530 | value.parentDep = parentDep 531 | newv = value 532 | elif type(value).__name__ in ['DependDict', 'DependList', 'DependSet']: 533 | value.parentDep = parentDep 534 | newv = value 535 | elif isinstance(value, tdu.Dependency): 536 | if isinstance(value.val, DependMixin): 537 | value.val.parentDep = parentDep 538 | elif type(value.val).__name__ in \ 539 | ['DependDict', 'DependList', 'DependSet']: 540 | value.val.parentDep = parentDep 541 | return value 542 | else: 543 | newv = value 544 | if hasattr(newv, '_TDParentDep'): 545 | newv._TDParentDep = parentDep 546 | return tdu.Dependency(newv) 547 | 548 | 549 | def isImmutable(item): 550 | if item is None: 551 | return True 552 | if isinstance(item, Number): 553 | return True 554 | if type(item) in (str, tuple, frozenset): 555 | return True 556 | return False 557 | -------------------------------------------------------------------------------- /lib/_stubs/Updater.py: -------------------------------------------------------------------------------- 1 | """ 2 | Extension classes enhance TouchDesigner components with python. An 3 | extension is accessed via ext.ExtensionClassName from any operator 4 | within the extended component. If the extension is promoted via its 5 | Promote Extension parameter, all its attributes with capitalized names 6 | can be accessed externally, e.g. op('yourComp').PromotedFunction(). 7 | 8 | Help: search "Extensions" in wiki 9 | """ 10 | 11 | from distutils.version import LooseVersion 12 | 13 | from _stubs import * 14 | from .TDStoreTools import StorageManager 15 | TDF = op.TDModules.mod.TDFunctions 16 | popDialog = op.TDResources.op('popDialog') 17 | 18 | class Updater: 19 | """ 20 | Updater description 21 | """ 22 | def __init__(self, ownerComp): 23 | # The component to which this extension is attached 24 | self.ownerComp = ownerComp 25 | self.progressDialog = ownerComp.op('progressDialog') 26 | self.updateInfo = {} 27 | TDF.createProperty(self, 'UpdatesRemaining', value=0, dependable=True) 28 | TDF.createProperty(self, 'UpdatesQueued', value=0, dependable=True) 29 | TDF.createProperty(self, 'CurrentUpdateComp', value='', dependable=True) 30 | 31 | def onUpdateCompParValueChange(self, par, prev, updater): 32 | pass 33 | 34 | def onUpdateCompParPulse(self, par, updater): 35 | if par.name == 'Update': 36 | self.Update(par.owner, updater) 37 | 38 | def AbortUpdates(self): 39 | self.updateInfo = {} 40 | self.UpdatesRemaining = self.UpdatesQueued = 0 41 | print("*** Update Aborted ***") 42 | 43 | def Update(self, comps, updater=None, versionCheck='dialog', 44 | preUpdateMethod=None, postUpdateMethod=None, 45 | updateMethod=None): 46 | """ 47 | Update a component or components. Updating will pulse clone and set the 48 | component's version to the version on the clone source. 49 | 50 | :param comps: a single component or a list of components 51 | :param updater: the TDUpdateSystem component 52 | :param versionCheck: defines versioning behavior. 'dialog' = opens 53 | warning dialog if clone source version is lower, True = 54 | does not update if clone source version is lower, False = ignore 55 | versions 56 | :param preUpdateMethod: will be called before update with a single 57 | argument containing a dictionary of info 58 | :param postUpdateMethod: will be called after update with a single 59 | argument containing a dictionary of info 60 | :param updateMethod: will be called instead of standard update with a 61 | single argument containing a dictionary of info 62 | :return: 63 | """ 64 | if versionCheck not in ['dialog', True, False]: 65 | raise ValueError('versionCheck must be True, False, or "dialog"') 66 | info = self.updateInfo 67 | if isinstance(comps, COMP): 68 | comps = [comps] 69 | self.UpdatesRemaining += len(comps) 70 | self.UpdatesQueued += len(comps) 71 | if self.UpdatesQueued > 1: 72 | self.progressDialog.Open() 73 | updateInfo = {'queuedComps': comps, 74 | 'versionCheck': versionCheck, 75 | 'preUpdateMethod': preUpdateMethod, 76 | 'postUpdateMethod': postUpdateMethod, 77 | 'updateMethod': updateMethod, 78 | 'updater': updater} 79 | if info and info['queuedComps']: 80 | info['queuedInfo'].append(updateInfo) 81 | else: 82 | self.updateInfo = {'queuedComps': comps, 83 | 'versionCheck': versionCheck, 84 | 'preUpdateMethod': preUpdateMethod, 85 | 'postUpdateMethod': postUpdateMethod, 86 | 'updateMethod': updateMethod, 87 | 'updater': updater, 88 | 'queuedInfo': []} 89 | self.doNextUpdate() 90 | 91 | def doNextUpdate(self): 92 | info = self.updateInfo 93 | if not info: 94 | self.endUpdates() 95 | return 96 | comp = info['queuedComps'][0] 97 | self.CurrentUpdateComp = comp.path 98 | info['comp'] = comp 99 | if not isinstance(comp, COMP): 100 | raise TypeError("Invalid object sent to Update: " + str(comp)) 101 | elif not comp.par.Update.enable: 102 | print('Skip update for', comp.path + '. Update disabled.\n') 103 | self.doNextUpdate() 104 | return 105 | else: 106 | if info['versionCheck'] is not False: 107 | source = comp.par.clone.eval() 108 | if source == comp: 109 | print("Can't update", comp.path + ". Component is it's own " 110 | "clone source.") 111 | while comp in info['queuedComps']: 112 | info['queuedComps'].remove(comp) 113 | if not info['queuedComps']: 114 | self.endUpdates() 115 | return 116 | self.doNextUpdate() 117 | return 118 | if not source: 119 | sourceMessage = \ 120 | info['updater'].par.Invalidsourcemessage.eval()\ 121 | if info['updater'] else '' 122 | errorMessage = "Invalid clone source for " + comp.path +'.' 123 | if sourceMessage: 124 | errorMessage += ' ' + sourceMessage 125 | print(errorMessage) 126 | self.OpenErrorDialog() 127 | self.doNextUpdate() 128 | return 129 | oldVersion = comp.par.Version.eval() 130 | newVersion = source.par.Version.eval() 131 | if LooseVersion(oldVersion) > LooseVersion(newVersion): 132 | if info['versionCheck'] is True: 133 | self.versionSkip() 134 | return 135 | else: 136 | op.TDResources.op('popDialog').Open( 137 | text='Clone source for ' + comp.path + 138 | ' has lower version. Update anyway?', 139 | title='Lower Version', 140 | buttons=['Yes', 'Yes All', 'No', 'No All'], 141 | callback=self.onVersionDialog, 142 | textEntry=False, 143 | escButton=3, 144 | enterButton=3, 145 | escOnClickAway=True 146 | ) 147 | return 148 | self.startUpdate() 149 | 150 | def OpenErrorDialog(self): 151 | popDialog.OpenDefault( 152 | "There were errors running Update. See textport for details.", 153 | "Update Error", 154 | ["OK"]) 155 | 156 | def versionSkip(self): 157 | print('Skip update for', 158 | self.updateInfo['comp'].path + '. Newer than source.\n') 159 | self.doNextUpdate() 160 | 161 | def onVersionDialog(self, info): 162 | if info['button'] == 'Yes': 163 | self.startUpdate() 164 | elif info['button'] == 'Yes All': 165 | self.updateInfo['versionCheck'] = False 166 | self.startUpdate() 167 | elif info['button'] == 'No': 168 | self.versionSkip() 169 | elif info['button'] == 'No All': 170 | self.updateInfo['versionCheck'] = True 171 | self.versionSkip() 172 | 173 | def startUpdate(self): 174 | info = self.updateInfo 175 | comp = info['comp'] 176 | print('Updating', comp.path) 177 | if info['preUpdateMethod']: 178 | info['preUpdateMethod'](info) 179 | if info['updateMethod']: 180 | info['updateMethod'](info) 181 | else: 182 | self.updateMethod() 183 | if info['postUpdateMethod']: 184 | info['postUpdateMethod'](info) 185 | run('op(' + str(self.ownerComp.id) + 186 | ').ext.Updater.updateComplete()', delayFrames=1, 187 | delayRef=op.TDResources) 188 | 189 | def updateMethod(self): 190 | info = self.updateInfo 191 | comp = info['comp'] 192 | info['preUpdateAllowCooking'] = comp.allowCooking 193 | comp.allowCooking = False 194 | comp.par.enablecloningpulse.pulse() 195 | if comp.pars('Version') and comp.par.clone.eval().pars('Version'): 196 | comp.par.Version = comp.par.clone.eval().par.Version.eval() 197 | 198 | def endUpdates(self): 199 | self.progressDialog.Close() 200 | self.UpdatesRemaining = 0 201 | self.UpdatesQueued = 0 202 | self.updateInfo = [] 203 | 204 | def updateComplete(self): 205 | if not self.updateInfo or not self.UpdatesRemaining: 206 | self.endUpdates() 207 | return 208 | self.UpdatesRemaining -= 1 209 | 210 | info = self.updateInfo 211 | if 'preUpdateAllowCooking' in info: 212 | info['comp'].allowCooking = info['preUpdateAllowCooking'] 213 | print(' Update complete.\n') 214 | while info['comp'] in info['queuedComps']: 215 | info['queuedComps'].remove(info['comp']) 216 | if info['queuedInfo']: 217 | newInfo = info['queuedInfo'].pop(0) 218 | info.update(newInfo) 219 | if info['queuedComps']: 220 | run('op(' + str(self.ownerComp.id) + 221 | ').ext.Updater.doNextUpdate()', delayFrames=1, 222 | delayRef=op.TDResources) 223 | else: 224 | self.endUpdates() 225 | 226 | 227 | def onUpdateSystemParValueChange(self, par, prev): 228 | system = par.owner 229 | comp = system.par.Comptoupdate.eval() 230 | if par.name == 'Enableupdatesystem': 231 | if comp.pars('Update'): 232 | comp.par.Update.enable = par.eval() 233 | 234 | def onUpdateSystemParPulse(self, par): 235 | system = par.owner 236 | comp = system.par.Comptoupdate.eval() 237 | if par.name == 'Setupparameters': 238 | self.SetupUpdateParameters(comp) 239 | elif par.name == 'Update': 240 | self.Update(comp) 241 | 242 | def SetupUpdateParameters(self, comp): 243 | """ 244 | Add Version and Update parameters to comp, if they aren't there already. 245 | They will be added to "Update" page, which will also be created, if 246 | necessary. 247 | 248 | :param comp: Component to add parameters to. 249 | :return: 250 | """ 251 | if isinstance(comp, COMP): 252 | if comp.pars('Version'): 253 | if not comp.par.Version.isString: 254 | raise TypeError('"Version" parameter not a string') 255 | else: 256 | if 'Update' not in comp.customPages: 257 | page = comp.appendCustomPage('Update') 258 | else: 259 | page = next((p for p in comp.customPages 260 | if p.name == 'Update')) 261 | page.appendStr('Version') 262 | comp.par.Version = '0.1' 263 | comp.par.Version.readOnly = True 264 | if comp.pars('Update'): 265 | if not comp.par.Update.isPulse: 266 | raise TypeError('"Update" parameter not a pulse') 267 | else: 268 | if 'Update' not in comp.customPages: 269 | page = comp.appendCustomPage('Update') 270 | else: 271 | page = next((p for p in comp.customPages 272 | if p.name == 'Update')) 273 | page.appendPulse('Update') 274 | print('Added Update parameters to', comp) 275 | else: 276 | raise TypeError('Can only setup parameters on COMP. Passed: ', 277 | str(comp)) 278 | -------------------------------------------------------------------------------- /lib/tdcomponents_resolve.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | def file(path): 4 | return path if os.path.exists(path) else '' 5 | 6 | def modpath(*oppaths, checkprefix=None): 7 | for o in ops(*oppaths): 8 | if o.isDAT and o.isText and o.text and (not checkprefix or o.text.startswith(checkprefix)): 9 | return o.path 10 | return '' 11 | -------------------------------------------------------------------------------- /loggly_logger.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/loggly_logger.tox -------------------------------------------------------------------------------- /mapper.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/mapper.tox -------------------------------------------------------------------------------- /midimap.py: -------------------------------------------------------------------------------- 1 | import typing as T 2 | 3 | print('midimap.py loading...') 4 | 5 | if False: 6 | from _stubs import * 7 | 8 | class MidiMapper: 9 | def __init__(self, ownerComp): 10 | self.ownerComp = ownerComp 11 | self.mappings = [] # type: T.List[Mapping] 12 | self.LoadMapTable() 13 | 14 | @property 15 | def _ExternalMapTable(self): 16 | return self.ownerComp.par.Maptable.eval() 17 | 18 | @property 19 | def _InnerMapTable(self): 20 | return self.ownerComp.op('set_map_table') 21 | 22 | def LoadMapTable(self): 23 | self.mappings.clear() 24 | dat = self._ExternalMapTable 25 | if dat and dat.numRows: 26 | for i in range(1, dat.numRows): 27 | m = Mapping() 28 | m.ReadRow(dat, i) 29 | self.mappings.append(m) 30 | self._WriteMapTable(self._InnerMapTable) 31 | self._BuildActiveMappingTables() 32 | 33 | def SaveMapTable(self): 34 | dat = self._ExternalMapTable 35 | if not dat: 36 | raise Exception('No map table to write to!') 37 | self._WriteMapTable(dat) 38 | 39 | def _WriteMapTable(self, dat): 40 | dat.clear() 41 | dat.appendRow(Mapping.colnames) 42 | for m in self.mappings: 43 | m.WriteRow(dat) 44 | 45 | def _BuildActiveMappingTables(self): 46 | dat = self.ownerComp.op('set_active_mappings') 47 | dat.clear() 48 | dat.appendRow(['param', 'inchan', 'outchan', 'low', 'high', 'path', 'parname']) 49 | mappingsbyop = {} # type: T.Dict[str, T.List[Mapping]] 50 | for m in self.mappings: 51 | if not m.enable or not m.path or not m.param or m.cc in ('', None): 52 | continue 53 | o = op(m.path) 54 | if not o: 55 | continue 56 | p = getattr(o.par, m.param, None) 57 | if p is None: 58 | continue 59 | if not (p.isNumber or p.isToggle or p.isMenu): 60 | continue 61 | dat.appendRow([ 62 | '{}:{}'.format(o.path, m.param), 63 | 'cc{}'.format(m.cc), 64 | 'ch1c{}'.format(m.cc), 65 | _format(m.rangelow if m.rangelow is not None else p.normMin), 66 | _format(m.rangehigh if m.rangehigh is not None else p.normMax), 67 | o.path, 68 | m.param, 69 | ]) 70 | if o.path not in mappingsbyop: 71 | mappingsbyop[o.path] = [] 72 | mappingsbyop[o.path].append(m) 73 | opsdat = self.ownerComp.op('set_mapped_ops') 74 | opsdat.clear() 75 | opsdat.appendRow(['path', 'params', 'fullparams', 'inchans', 'outchans']) 76 | for path in sorted(mappingsbyop.keys()): 77 | omaps = mappingsbyop[path] 78 | opsdat.appendRow([ 79 | path, 80 | ' '.join([m.param for m in omaps]), 81 | ' '.join(['{}:{}'.format(path, m.param) for m in omaps]), 82 | ' '.join(['cc{}'.format(m.cc) for m in omaps]), 83 | ' '.join(['ch1c{}'.format(m.cc) for m in omaps]), 84 | ]) 85 | 86 | def HandleDrop(self, args): 87 | pass 88 | 89 | def _HandleDrop(self, args): 90 | pass 91 | 92 | @staticmethod 93 | def PrepareDevices(dat): 94 | dat.appendCol(['label']) 95 | for i in range(1, dat.numRows): 96 | indev = dat[i, 'indevice'] 97 | outdev = dat[i, 'outdevice'] 98 | if indev == outdev: 99 | dat[i, 'label'] = indev 100 | else: 101 | dat[i, 'label'] = '{} / {}'.format(indev, outdev) 102 | for i in range(dat.numRows, 17): 103 | dat.appendRow([i]) 104 | name = '({})'.format(i) 105 | dat[i, 'indevice'] = name 106 | dat[i, 'outdevice'] = name 107 | dat[i, 'label'] = name + ' (unknown)' 108 | dat[i, 'channel'] = 1 109 | 110 | class Mapping: 111 | def __init__( 112 | self, 113 | path=None, 114 | param=None, 115 | enable=True, 116 | rangelow=None, 117 | rangehigh=None, 118 | cc=None): 119 | self.path = path 120 | self.param = param 121 | self.enable = enable 122 | self.rangelow = rangelow 123 | self.rangehigh = rangehigh 124 | self.cc = cc 125 | 126 | colnames = ['path', 'param', 'enable', 'low', 'high', 'cc'] 127 | 128 | def ReadRow(self, dat, i): 129 | self.path = _parse(dat[i, 'path'], str) 130 | self.param = _parse(dat[i, 'param'], str) 131 | self.enable = _parse(dat[i, 'enable'], bool, False) 132 | self.rangelow = _parse(dat[i, 'low'], float) 133 | self.rangehigh = _parse(dat[i, 'high'], float) 134 | self.cc = _parse(dat[i, 'cc'], int) 135 | 136 | def WriteRow(self, dat): 137 | i = dat.numRows 138 | dat.appendRow([]) 139 | for c in Mapping.colnames: 140 | if dat.col(c) is None: 141 | dat.appendCol([c]) 142 | dat[i, 'path'] = _format(self.path) 143 | dat[i, 'param'] = _format(self.param) 144 | dat[i, 'enable'] = _format(self.enable) 145 | dat[i, 'low'] = _format(self.rangelow) 146 | dat[i, 'high'] = _format(self.rangehigh) 147 | dat[i, 'cc'] = _format(self.cc) 148 | 149 | def _parse(val, t, default=None): 150 | if val in ('', None): 151 | return default 152 | try: 153 | return t(val) 154 | except ValueError: 155 | return default 156 | 157 | def _format(val): 158 | if val is None: 159 | return '' 160 | if isinstance(val, bool): 161 | return int(val) 162 | if isinstance(val, (int, float)): 163 | if val == int(val): 164 | return int(val) 165 | return val 166 | -------------------------------------------------------------------------------- /mixer/MixerSourcesExt.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import dataclasses 3 | from typing import Any, Dict, Iterable, List, Optional, Union 4 | 5 | # noinspection PyUnreachableCode 6 | if False: 7 | # noinspection PyUnresolvedReferences 8 | from _stubs import * 9 | 10 | class SourceTrack: 11 | def __init__(self, ownerComp): 12 | self.ownerComp = ownerComp 13 | 14 | @property 15 | def SourcesTable(self): 16 | o = self.ownerComp.par.Sources.eval() 17 | if not o: 18 | return None 19 | if o.isDAT: 20 | return o 21 | if not o.isCOMP: 22 | return None 23 | if hasattr(o.par, 'Sourcetable'): 24 | src = o.par.Sourcetable.eval() 25 | if src and src.isDAT: 26 | return src 27 | src = o.op('sources') 28 | if src and src.isDAT: 29 | return src 30 | return None 31 | 32 | def PrepareOpSources(self, dat: 'DAT', paths: Iterable[Union[str, 'OP']]): 33 | _prepareOpSources(dat, paths, removeComp=self.ownerComp) 34 | 35 | def GetSourceButtonLabels(self, wrapcount=None): 36 | sources = self.ownerComp.op('sources') # type: DAT 37 | labels = [ 38 | str(sources[i, 'label'] or sources[i, 'shortName'] or sources[i, 'legalName'] or '-') 39 | for i in range(1, sources.numRows) 40 | ] 41 | if wrapcount is None or wrapcount <= 0: 42 | return labels 43 | return [_wrapString(s, wrapcount) for s in labels] 44 | 45 | def _wrapString(s, wraplen): 46 | strlen = len(s) 47 | if strlen <= wraplen: 48 | return s 49 | return r'\n'.join([s[i:i + wraplen] for i in range(0, strlen, wraplen)]) 50 | 51 | class MixerSources: 52 | def __init__(self, ownerComp): 53 | self.ownerComp = ownerComp 54 | 55 | @staticmethod 56 | def PrepareOpSources(dat: 'DAT', paths: Iterable[Union[str, 'OP']]): 57 | _prepareOpSources(dat, paths) 58 | 59 | def _prepareOpSources(dat: 'DAT', paths: Iterable[Union[str, 'OP']], removeComp: Optional['OP'] = None): 60 | dat.clear() 61 | dat.appendRow(_sourceColumns) 62 | if not paths: 63 | return 64 | srcops = ops(*paths) 65 | for o in srcops: 66 | src = _Source( 67 | path=o.path, 68 | legalName=o.name, 69 | type='OP', 70 | ) 71 | src.label = _firstStringPar(o, 'Label', 'Uilabel', 'Name') or o.name 72 | src.shortName = _firstStringPar(o, 'Shortlabel', 'Shortname') or src.label 73 | if o.isTOP: 74 | src.videoPath = o.path 75 | src.compPath = o.parent().path 76 | elif o.isCOMP: 77 | vidop = _getCompVideoSource(o) 78 | src.videoPath = vidop.path if vidop else None 79 | src.compPath = o.path 80 | else: 81 | continue 82 | if removeComp and src.compPath == removeComp.path: 83 | src.compPath = '' 84 | src.appendToRow(dat) 85 | 86 | def _getCompVideoSource(o): 87 | if o and o.isCOMP: 88 | for pattern in [ 89 | 'out1', 90 | 'video_out', 91 | '*_out', 92 | 'out*', 93 | '*out', 94 | ]: 95 | for out in o.ops(pattern): 96 | if out.isTOP: 97 | return out 98 | pass 99 | return None 100 | 101 | def _firstStringPar(o, *names): 102 | if not o: 103 | return None 104 | for par in o.pars(*names): 105 | if par: 106 | return par.eval() 107 | 108 | @dataclass 109 | class _Source: 110 | path: str = None 111 | legalName: str = None 112 | sourceName: str = None 113 | shortName: str = None 114 | label: str = None 115 | compPath: str = None 116 | videoPath: str = None 117 | type: str = None 118 | deviceName: str = None 119 | active: bool = None 120 | streaming: bool = None 121 | fps: int = None 122 | url: str = None 123 | extraFields: Dict[str, Any] = None 124 | 125 | def appendToRow(self, dat: 'DAT'): 126 | data = dict(dataclasses.asdict(self)) 127 | if 'extraFields' in data: 128 | del data['extraFields'] 129 | if self.extraFields: 130 | data.update(self.extraFields) 131 | addDictRow(dat, data) 132 | 133 | _sourceColumns = [f.name for f in dataclasses.fields(_Source) if f.name != 'extraFields'] 134 | 135 | 136 | NULL_PLACEHOLDER = '_' 137 | 138 | def formatValue(val, nonevalue=NULL_PLACEHOLDER): 139 | if isinstance(val, str): 140 | return val 141 | if val is None: 142 | return nonevalue 143 | if isinstance(val, bool): 144 | return str(int(val)) 145 | if isinstance(val, float) and int(val) == val: 146 | return str(int(val)) 147 | return str(val) 148 | 149 | def addDictRow(dat, obj: Dict[str, Any]): 150 | r = dat.numRows 151 | dat.appendRow([]) 152 | setDictRow(dat, r, obj) 153 | 154 | def setDictRow(dat, rowkey: Union[str, int], obj: Dict[str, Any], clearmissing=False): 155 | for key, val in obj.items(): 156 | dat[rowkey, key] = formatValue(val, nonevalue='') 157 | if clearmissing: 158 | for col in dat.row(0): 159 | if col.val not in obj: 160 | col.val = '' 161 | -------------------------------------------------------------------------------- /mixer/mixer_sources.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/mixer/mixer_sources.tox -------------------------------------------------------------------------------- /mixer/source_track.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/mixer/source_track.tox -------------------------------------------------------------------------------- /mixer/track_mixer.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/mixer/track_mixer.tox -------------------------------------------------------------------------------- /mixer/videoSenderCore.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/mixer/videoSenderCore.tox -------------------------------------------------------------------------------- /mixer/video_sender.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/mixer/video_sender.tox -------------------------------------------------------------------------------- /multi_logger.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/multi_logger.tox -------------------------------------------------------------------------------- /rack.toe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack.toe -------------------------------------------------------------------------------- /rack/component_editor/ComponentEditorExt.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | # noinspection PyUnreachableCode 4 | if False: 5 | # noinspection PyUnresolvedReferences 6 | from _stubs import * 7 | from typing import Any 8 | iop.hostedComp = COMP() 9 | ipar.compEditor = Any() 10 | ipar.workspace = Any() 11 | 12 | class ComponentEditor: 13 | def __init__(self, ownerComp): 14 | self.ownerComp = ownerComp 15 | 16 | def LoadComponent(self): 17 | comp = iop.hostedComp 18 | tox = ipar.compEditor.Toxfile.eval() 19 | if not tox: 20 | comp.par.externaltox = '' 21 | comp.destroyCustomPars() 22 | for child in list(comp.children): 23 | if child.valid: 24 | child.destroy() 25 | else: 26 | msg = f'Loading component {tdu.expandPath(tox)}' 27 | print(msg) 28 | ui.status = msg 29 | # comp = comp.loadTox(tox, unwired=True) 30 | comp.par.externaltox = tox 31 | comp.par.reinitnet.pulse() 32 | comp = iop.hostedComp 33 | if comp.isPanel: 34 | panel = self.ownerComp.op('component_ui_panel') # type: PanelCOMP 35 | if comp not in panel.panelChildren: 36 | panel.outputCOMPConnectors[0].connect(comp) 37 | 38 | def SaveComponent(self): 39 | comp = iop.hostedComp 40 | tox = ipar.compEditor.Toxfile.eval() 41 | if not tox: 42 | # TODO: prompt for a file 43 | ui.status = 'WARNING: UNABLE TO SAVE COMPONENT, NO TOX FILE' 44 | return 45 | tox = tdu.expandPath(tox) 46 | comp.save(tox, createFolders=False) 47 | msg = f'Saved component to {tox}' 48 | print(msg) 49 | ui.status = msg 50 | if ipar.workspace.Savethumbnails: 51 | img = ipar.compEditor.Thumbfile.eval() 52 | if not img: 53 | img = re.sub(r'\.tox$', '.png', tox) 54 | self.ownerComp.op('video_output').save(img) 55 | 56 | @staticmethod 57 | def CustomizeComponent(): 58 | comp = iop.hostedComp 59 | ui.openCOMPEditor(comp) 60 | 61 | @staticmethod 62 | def ShowNetwork(useActive=True): 63 | comp = iop.hostedComp 64 | pane = None 65 | if useActive: 66 | pane = _GetActiveEditor() 67 | if not pane: 68 | pane = _GetPaneByName('compeditor') 69 | if not pane: 70 | pane = ui.panes.createFloating(type=PaneType.NETWORKEDITOR, name='compeditor') 71 | pane.owner = comp 72 | 73 | @staticmethod 74 | def FindVideoOutput(): 75 | comp = iop.hostedComp 76 | o = comp.op('video_out') or comp.op('out1') 77 | if o and o.isTOP: 78 | return o 79 | for o in comp.findChildren(type=outTOP, depth=1): 80 | return o 81 | 82 | @staticmethod 83 | def FindAudioOutput(): 84 | comp = iop.hostedComp 85 | for name in ['audio_out', 'out1', 'out2']: 86 | o = comp.op(name) 87 | if o and o.isCHOP: 88 | return o 89 | for o in comp.findChildren(type=outCHOP, depth=1): 90 | return o 91 | 92 | def Savecomponent(self, par): 93 | self.SaveComponent() 94 | 95 | def Loadcomponent(self, par): 96 | self.LoadComponent() 97 | 98 | def _GetActiveEditor(): 99 | pane = ui.panes.current 100 | if pane.type == PaneType.NETWORKEDITOR: 101 | return pane 102 | for pane in ui.panes: 103 | if pane.type == PaneType.NETWORKEDITOR: 104 | return pane 105 | 106 | def _GetPaneByName(name): 107 | for pane in ui.panes: 108 | if pane.name == name: 109 | return pane 110 | -------------------------------------------------------------------------------- /rack/component_editor/component_editor.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/component_editor/component_editor.tox -------------------------------------------------------------------------------- /rack/component_picker/ComponentPickerExt.py: -------------------------------------------------------------------------------- 1 | import re 2 | from datetime import datetime 3 | 4 | # noinspection PyUnreachableCode 5 | if False: 6 | # noinspection PyUnresolvedReferences 7 | from _stubs import * 8 | 9 | class ComponentPicker: 10 | def __init__(self, ownerComp): 11 | self.ownerComp = ownerComp 12 | self.statePar = ownerComp.op('iparpicker').par 13 | 14 | @staticmethod 15 | def BuildComponentTable(dat: 'DAT', files: 'DAT'): 16 | dat.clear() 17 | dat.appendRow(['relpath', 'name', 'modified', 'timestamp', 'tox', 'thumb', 'folder']) 18 | for i in range(1, files.numRows): 19 | if files[i, 'extension'] != 'tox': 20 | continue 21 | relPath = files[i, 'relpath'].val 22 | thumbPath = _findThumbPath(files, relPath) 23 | timestamp = int(files[i, 'datemodified'] or 0) 24 | if not timestamp: 25 | modified = '' 26 | else: 27 | modified = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M') 28 | folder = files[i, 'folder'].val 29 | dat.appendRow([ 30 | relPath, 31 | re.sub(r'([\w_ ]+)/\1\.tox$', r'\1', relPath), 32 | modified, 33 | timestamp, 34 | tdu.collapsePath(files[i, 'path'].val), 35 | tdu.collapsePath(thumbPath) if thumbPath else '', 36 | tdu.collapsePath(folder) if folder else '', 37 | ]) 38 | 39 | def UpdateListFromPar(self, par: 'Par' = None): 40 | if par is None: 41 | par = self.ownerComp.par.Selectedcomp 42 | listWidget = self.ownerComp.op('component_list') 43 | if not par: 44 | index = None 45 | else: 46 | cell = self.ownerComp.op('component_table')[par.eval(), 'relpath'] 47 | index = cell.row if cell is not None else None 48 | listWidget.par.Selectedrows = index if index is not None else '' 49 | 50 | def Refreshpulse(self, par): 51 | self.ownerComp.op('folder').par.refreshpulse.pulse() 52 | 53 | def Selectedcomp(self, par, val, prev): 54 | self.UpdateListFromPar(par) 55 | 56 | def OnSelectRow(self, info: dict): 57 | rowData = info.get('rowData') or {} 58 | rowObject = rowData.get('rowObject') or {} 59 | self.ownerComp.par.Selectedcomp = rowObject.get('relpath') or '' 60 | self.ownerComp.par.Onitemclick.pulse() 61 | 62 | def OnListRollover(self, info: dict): 63 | self.statePar.Hoverrow = info.get('row', -1) 64 | 65 | def _findThumbPath(files: 'DAT', toxPath: str): 66 | toxPathBase = re.sub('.tox$', '', toxPath) 67 | for path in files.col('relpath')[1:]: 68 | extension = files[path, 'extension'].val 69 | if extension not in tdu.fileTypes['image']: 70 | continue 71 | basePath = re.sub('.' + extension, '', path.val) 72 | if basePath == toxPathBase: 73 | return files[path.row, 'path'].val 74 | if basePath == toxPathBase.rsplit('/', maxsplit=1)[0] + '/thumb': 75 | return files[path.row, 'path'].val 76 | 77 | def OnStart(): 78 | run(f'op({parent.picker.path!r}).UpdateListFromPar()', delayFrames=2) 79 | 80 | def OnCreate(): 81 | OnStart() 82 | -------------------------------------------------------------------------------- /rack/component_picker/component_picker.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/component_picker/component_picker.tox -------------------------------------------------------------------------------- /rack/editor/components/EditorCommon.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | from dataclasses import dataclass 3 | from typing import Any, Callable, Dict, List, Optional, Union 4 | 5 | # noinspection PyUnreachableCode 6 | if False: 7 | # noinspection PyUnresolvedReferences 8 | from _stubs import * 9 | 10 | ValueOrExpr = Union[Any, Dict[str, str]] 11 | 12 | @dataclass 13 | class LibrarySpec: 14 | shortcut: str 15 | path: str 16 | allPaths: str 17 | 18 | @dataclass 19 | class ComponentSpec: 20 | name: Optional[str] = None 21 | label: Optional[str] = None 22 | tox: Optional[str] = None 23 | copyOf: Optional[ValueOrExpr] = None 24 | pars: Optional[Dict[str, ValueOrExpr]] = None 25 | 26 | @classmethod 27 | def fromDict(cls, d: dict): 28 | return cls(**d) 29 | 30 | def toDict(self): 31 | return dataclasses.asdict(self) 32 | 33 | def createComp(self, destination: COMP) -> COMP: 34 | if self.copyOf: 35 | if isinstance(self.copyOf, Dict): 36 | master = destination.evalExpression(self.copyOf['$']) 37 | else: 38 | master = destination.op(self.copyOf) 39 | if not master: 40 | raise Exception(f'Invalid component spec {self!r}') 41 | comp = destination.copy(master, name=self.name) 42 | elif self.tox: 43 | comp = destination.loadTox(self.tox) 44 | else: 45 | raise Exception(f'Invalid component spec {self!r}') 46 | if self.name: 47 | comp.name = self.name 48 | # in case the name need to change for uniqueness, this will store the actual name 49 | self.name = comp.name 50 | self.applyParams(comp) 51 | return comp 52 | 53 | def applyParams(self, comp: COMP): 54 | if not self.pars: 55 | return 56 | for name, val in self.pars.items(): 57 | par = getattr(comp.par, name, None) 58 | if par is None: 59 | print(f'Param {name} not found in {comp}') 60 | continue 61 | if isinstance(val, dict) and '$' in val: 62 | par.expr = val['$'] 63 | else: 64 | par.val = val 65 | 66 | @dataclass 67 | class UITab: 68 | name: str 69 | label: str 70 | visible: bool = True 71 | icon: str = None 72 | componentName: str = None 73 | attrs: dict = None 74 | 75 | @classmethod 76 | def noneTab(cls): 77 | return cls(name='none', label='None', icon=chr(0xF156)) 78 | 79 | @dataclass 80 | class UITabSet: 81 | tabs: List[UITab] = dataclasses.field(default_factory=list) 82 | hasNone: bool = False 83 | attrNames: List[str] = None 84 | 85 | def buildTable(self, dat: 'DAT'): 86 | dat.clear() 87 | dat.appendRow(['name', 'label', 'icon', 'componentName'] + (self.attrNames or [])) 88 | for tab in self.tabs: 89 | if not tab.visible: 90 | continue 91 | vals = [tab.name or '', tab.label or '', tab.icon or '', tab.componentName or ''] 92 | if self.attrNames: 93 | for name in self.attrNames: 94 | val = tab.attrs.get(name) if tab.attrs else None 95 | vals.append(val if val is not None else '') 96 | dat.appendRow(vals) 97 | 98 | def updateParMenu(self, par: 'Par'): 99 | names = [] 100 | labels = [] 101 | for tab in self.tabs: 102 | if tab.visible: 103 | names.append(tab.name) 104 | labels.append(tab.label) 105 | par.menuNames = names 106 | par.menuLabels = labels 107 | -------------------------------------------------------------------------------- /rack/editor/components/EditorToolsExt.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, is_dataclass, asdict 2 | from typing import Callable, List, Union 3 | 4 | # noinspection PyUnreachableCode 5 | if False: 6 | # noinspection PyUnresolvedReferences 7 | from _stubs import * 8 | from ..EditorExt import Editor 9 | ext.editor = Editor(None) 10 | iop.hostedComp = COMP() 11 | 12 | @dataclass 13 | class ToolContext: 14 | toolName: str 15 | component: 'COMP' 16 | editorPane: 'NetworkEditor' 17 | 18 | @dataclass 19 | class _ToolDefinition: 20 | name: str 21 | action: Callable[[ToolContext], None] 22 | label: str = None 23 | icon: str = None 24 | 25 | @classmethod 26 | def parse(cls, spec: Union['_ToolDefinition', list, tuple, dict]) -> '_ToolDefinition': 27 | if isinstance(spec, _ToolDefinition): 28 | return spec 29 | if is_dataclass(spec): 30 | spec = asdict(spec) 31 | if isinstance(spec, dict): 32 | return cls(**spec) 33 | return cls(*spec) 34 | 35 | class EditorTools: 36 | def __init__(self, ownerComp: 'COMP'): 37 | self.ownerComp = ownerComp 38 | self.builtInTools = [] # type: List[_ToolDefinition] 39 | self.customTools = [] # type: List[_ToolDefinition] 40 | 41 | self.customToolsScript = self.ownerComp.op('customTools') # type: DAT 42 | self.toolTable = self.ownerComp.op('set_tool_table') # type: DAT 43 | 44 | self.initializeBuiltInTools() 45 | self.updateToolTable() 46 | 47 | def initializeBuiltInTools(self): 48 | self.builtInTools = [ 49 | _ToolDefinition('saveComponent', lambda ctx: ext.editor.SaveComponent(), icon=chr(0xF193)) 50 | ] 51 | 52 | def updateToolTable(self): 53 | self.toolTable.clear() 54 | self.toolTable.appendRow(['name', 'label', 'icon', 'category']) 55 | for tool in self.builtInTools: 56 | self.toolTable.appendRow([ 57 | tool.name, 58 | tool.label or tool.name, 59 | tool.icon or '', 60 | 'builtIn' 61 | ]) 62 | for tool in self.customTools: 63 | self.toolTable.appendRow([ 64 | tool.name, 65 | tool.label or tool.name, 66 | tool.icon or '', 67 | 'custom' 68 | ]) 69 | 70 | def ClearCustomTools(self): 71 | self.customToolsScript.clear() 72 | self.ownerComp.par.Customtoolscriptfile = '' 73 | self.ownerComp.par.Customtoolscript = '' 74 | self.updateToolTable() 75 | 76 | def LoadCustomTools(self): 77 | self.customTools.clear() 78 | self.customToolsScript.clear() 79 | file = self.ownerComp.par.Customtoolscriptfile.eval() 80 | if file: 81 | self.customToolsScript.par.file = file 82 | self.customToolsScript.par.loadonstartpulse.pulse() 83 | self.addCustomToolsFromScript(self.customToolsScript) 84 | self.addCustomToolsFromScript(self.ownerComp.par.Customtoolscript.eval()) 85 | self.updateToolTable() 86 | 87 | def addCustomToolsFromScript(self, script: 'DAT'): 88 | if not script or not script.text: 89 | return 90 | try: 91 | m = script.module 92 | except Exception as e: 93 | print(self.ownerComp, f'ERROR loading custom tools script: {e}') 94 | return 95 | if not hasattr(m, 'getEditorTools'): 96 | print(self.ownerComp, 'ERROR: Custom tools script does not have `getEditorTools` function') 97 | return 98 | try: 99 | specs = m.getEditorTools() 100 | except Exception as e: 101 | print(self.ownerComp, f'ERROR loading custom tools script: {e}') 102 | return 103 | if not specs: 104 | return 105 | for spec in specs: 106 | try: 107 | tool = _ToolDefinition.parse(spec) 108 | except Exception as e: 109 | print(self.ownerComp, f'ERROR parsing custom tool spec: {spec!r}\n{e}') 110 | continue 111 | self.customTools.append(tool) 112 | 113 | def findTool(self, name: str): 114 | # custom tools take precedence over built-in tools 115 | for tool in self.customTools: 116 | if tool.name == name: 117 | return tool 118 | for tool in self.builtInTools: 119 | if tool.name == name: 120 | return tool 121 | 122 | def ExecuteTool(self, name: str): 123 | tool = self.findTool(name) 124 | if not tool: 125 | raise Exception(f'Editor tool not found: {name}') 126 | context = ToolContext( 127 | name, 128 | iop.hostedComp, 129 | ext.editor.GetActiveNetworkEditor()) 130 | tool.action(context) 131 | 132 | def OnWorkspaceLoad(self): 133 | self.LoadCustomTools() 134 | 135 | def OnWorkspaceUnload(self): 136 | self.ClearCustomTools() 137 | -------------------------------------------------------------------------------- /rack/editor/components/EditorViewsExt.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from .EditorCommon import ComponentSpec 3 | 4 | # noinspection PyUnreachableCode 5 | if False: 6 | # noinspection PyUnresolvedReferences 7 | from _stubs import * 8 | 9 | class EditorViews: 10 | def __init__(self, ownerComp): 11 | self.ownerComp = ownerComp # type: COMP 12 | self.customHolder = ownerComp.op('customViews') # type: COMP 13 | 14 | def LoadCustomViews(self): 15 | return 16 | self.ClearCustomViews() 17 | viewDicts = self.ownerComp.par.Customviews.eval() 18 | if not viewDicts: 19 | return 20 | viewSpecs = [ComponentSpec.fromDict(v) for v in viewDicts] 21 | # for i, viewSpec in enumerate(viewSpecs): 22 | # comp = viewSpec.createComp(self.customHolder) 23 | # comp.nodeY = 600 - (i * 150) 24 | # self.initializeCustomView(comp, viewSpec) 25 | self.updateCustomViewTable(viewSpecs) 26 | 27 | def initializeCustomView(self, comp: 'COMP', viewSpec: ComponentSpec): 28 | if not comp.isPanel: 29 | return 30 | try: 31 | comp.par.hmode = 'fill' 32 | comp.par.vmode = 'fill' 33 | comp.par.display.expr = f'parent.editorViews.par.Selectedview == {viewSpec.name!r}' 34 | except Exception as e: 35 | print(self.ownerComp, 'Error initializing custom view', comp, '\n', viewSpec, '\n', e) 36 | 37 | def ClearCustomViews(self): 38 | for o in self.customHolder.children: 39 | if not o or not o.valid: 40 | continue 41 | # try: 42 | o.destroy() 43 | # except: 44 | # pass 45 | self.updateCustomViewTable([]) 46 | 47 | def OnWorkspaceUnload(self): 48 | self.ownerComp.par.Customviews = None 49 | self.ClearCustomViews() 50 | 51 | def OnWorkspaceLoad(self): 52 | self.LoadCustomViews() 53 | 54 | def updateCustomViewTable(self, viewSpecs: List[ComponentSpec]): 55 | dat = self.ownerComp.op('set_custom_view_table') 56 | dat.clear() 57 | dat.appendRow(['name', 'label']) 58 | if not viewSpecs: 59 | return 60 | for viewSpec in viewSpecs: 61 | dat.appendRow([viewSpec.name, viewSpec.label or viewSpec.name]) 62 | -------------------------------------------------------------------------------- /rack/editor/components/SettingsExt.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from dataclasses import dataclass 3 | import json 4 | from pathlib import Path 5 | from typing import Any, List, Optional 6 | 7 | # noinspection PyUnreachableCode 8 | if False: 9 | # noinspection PyUnresolvedReferences 10 | from _stubs import * 11 | 12 | try: 13 | from TDStoreTools import DependList, DependDict 14 | except ImportError: 15 | from _stubs.TDStoreTools import DependList, DependDict 16 | 17 | class SettingsExtBase(ABC): 18 | @abstractmethod 19 | def getSettingsOps(self) -> List['SettingsOp']: 20 | pass 21 | 22 | def loadSettingsFile(self, filePath: Path): 23 | if filePath and filePath.exists(): 24 | with filePath.open('r') as f: 25 | text = f.read() 26 | settings = json.loads(text or '{}') 27 | else: 28 | settings = {} 29 | self.applySettings(settings) 30 | 31 | def saveSettingsFile(self, filePath: Path): 32 | settings = self.buildSettings() 33 | with filePath.open('w') as f: 34 | json.dump(settings, f, indent=' ', cls=_CustomEncoder) 35 | return settings 36 | 37 | def applySettings(self, settings: dict): 38 | settingsOps = self.getSettingsOps() 39 | for settingsOp in settingsOps: 40 | settingsOp.applySettings(settings.get(settingsOp.name)) 41 | 42 | def buildSettings(self): 43 | settingsOps = self.getSettingsOps() 44 | settings = {} 45 | for settingsOp in settingsOps: 46 | settings[settingsOp.name] = settingsOp.buildSettings() 47 | return settings 48 | 49 | @dataclass 50 | class SettingsOp: 51 | name: str 52 | op: Optional['OP'] = None 53 | # note that this is an OR combination, meaning that it is all the params that 54 | # are listed in `paramNames` plus all the params in all the pages listed in `paramPages` 55 | params: Optional[List[str]] = None 56 | pages: Optional[List[str]] = None 57 | 58 | @classmethod 59 | def fromDatRow(cls, dat: 'DAT', row): 60 | return cls( 61 | name=str(dat[row, 'name']), 62 | params=str(dat[row, 'params'] or '').split(' '), 63 | pages=str(dat[row, 'paramPages'] or '').split(' '), 64 | ) 65 | 66 | def _getParams(self): 67 | if self.op: 68 | o = self.op 69 | else: 70 | o = getattr(iop, self.name, None) 71 | if not o: 72 | return [] 73 | pars = [] 74 | if self.params: 75 | for par in o.pars(*self.params): 76 | if self._isEligible(par): 77 | pars.append(par) 78 | if self.pages: 79 | for page in o.customPages: 80 | if page.name in self.pages: 81 | for par in page.pars: 82 | if par not in pars and self._isEligible(par): 83 | pars.append(par) 84 | return pars 85 | 86 | @staticmethod 87 | def _isEligible(par: 'Par'): 88 | if not par.enable or par.readOnly or not par.isCustom: 89 | return False 90 | if par.label.startswith('-'): 91 | return False 92 | if par.isPulse: 93 | return False 94 | return True 95 | 96 | def buildSettings(self): 97 | settings = {} 98 | for par in self._getParams(): 99 | if par.mode == ParMode.CONSTANT: 100 | settings[par.name] = par.eval() 101 | elif par.mode == ParMode.EXPRESSION: 102 | settings[par.name] = {'$': par.expr} 103 | return settings 104 | 105 | def applySettings(self, settings: dict): 106 | for par in self._getParams(): 107 | if settings and par.name in settings: 108 | val = settings[par.name] 109 | if isinstance(val, dict) and '$' in val: 110 | par.expr = val['$'] 111 | else: 112 | par.val = val 113 | else: 114 | par.val = par.default 115 | 116 | class UserSettings(SettingsExtBase): 117 | def __init__(self, ownerComp): 118 | self.ownerComp = ownerComp # type: COMP 119 | 120 | def getSettingsOps(self) -> List['SettingsOp']: 121 | return [SettingsOp('settings', op=self.ownerComp, pages=['Settings'])] 122 | 123 | def buildSettings(self): 124 | settings = super().buildSettings() 125 | settings['recorderPresets'] = self.getRecorderPresets() 126 | return settings 127 | 128 | def getRecorderPresets(self): 129 | recorderPresets = self.getRecorderPresetsComp() 130 | if not recorderPresets: 131 | return {} 132 | return { 133 | 'Banks': recorderPresets.Banks 134 | } 135 | 136 | def setRecorderPresets(self, settings): 137 | recorderPresets = self.getRecorderPresetsComp() 138 | if not recorderPresets: 139 | return 140 | recorderPresets.Banks = (settings and settings.get('Banks')) or [] 141 | 142 | def applySettings(self, settings: dict): 143 | super().applySettings(settings) 144 | self.setRecorderPresets(settings.get('recorderPresets')) 145 | 146 | def getRecorderPresetsComp(self) -> 'Any': 147 | return self.ownerComp.par.Recorderpresetscomp.eval() 148 | 149 | def getSettingsPath(self): 150 | return Path(self.ownerComp.par.Usersettingsfile.eval()) 151 | 152 | def LoadSettings(self): 153 | self.loadSettingsFile(self.getSettingsPath()) 154 | 155 | def Loadsettings(self, par): 156 | self.LoadSettings() 157 | 158 | def SaveSettings(self): 159 | filePath = self.getSettingsPath() 160 | self.saveSettingsFile(filePath) 161 | print(f'Settings saved to {filePath}') 162 | 163 | def Savesettings(self, par): 164 | self.SaveSettings() 165 | 166 | def RecentWorkspaces(self) -> List[str]: 167 | return [ 168 | path 169 | for path in self.ownerComp.par.Recentworkspaces.eval() or [] 170 | if path and path != '.' 171 | ] 172 | 173 | def AddRecentWorkspace(self, workspaceSettingsFile: str): 174 | workspaceSettingsFile = tdu.collapsePath(str(Path(workspaceSettingsFile).as_posix())) 175 | workspaces = self.RecentWorkspaces() 176 | if workspaceSettingsFile in workspaces: 177 | workspaces.remove(workspaceSettingsFile) 178 | workspaces.insert(0, workspaceSettingsFile) 179 | self.ownerComp.par.Recentworkspaces.val = workspaces 180 | self.SaveSettings() 181 | 182 | class _CustomEncoder(json.JSONEncoder): 183 | def default(self, o): 184 | if isinstance(o, (DependList, DependDict)): 185 | return o.getRaw() 186 | return o 187 | -------------------------------------------------------------------------------- /rack/editor/components/WorkspaceExt.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import List, Optional 3 | from .SettingsExt import SettingsOp, SettingsExtBase 4 | 5 | # noinspection PyUnreachableCode 6 | if False: 7 | # noinspection PyUnresolvedReferences 8 | from _stubs import * 9 | from typing import Any 10 | iop.workspaceState = COMP() 11 | ipar.workspaceState = Any() 12 | 13 | class Workspace(SettingsExtBase): 14 | def __init__(self, ownerComp): 15 | self.ownerComp = ownerComp # type: COMP 16 | 17 | def getSettingsOps(self) -> List['SettingsOp']: 18 | settingsOps = [ 19 | self.workspaceSettingsOp() 20 | ] 21 | opTable = self.ownerComp.par.Settingsoptable.eval() # type: DAT 22 | if opTable: 23 | for i in range(1, opTable.numRows): 24 | settingsOps.append(SettingsOp.fromDatRow(opTable, i)) 25 | return settingsOps 26 | 27 | def PromptLoadWorkspaceFile(self): 28 | path = ui.chooseFile(load=True, fileTypes=['json'], title='Open Workspace File') 29 | if path: 30 | self.LoadWorkspaceFile(path) 31 | 32 | def PromptLoadWorkspaceFolder(self): 33 | path = ui.chooseFolder(title='Open Workspace Folder') 34 | if path: 35 | self.LoadWorkspaceFolder(path) 36 | 37 | def LoadWorkspaceFile(self, file: str): 38 | filePath = Path(file) 39 | self._LoadWorkspace( 40 | filePath, 41 | filePath.parent) 42 | 43 | def LoadWorkspaceFolder(self, path: str): 44 | folderPath = Path(path) 45 | self._LoadWorkspace( 46 | folderPath / 'workspace.json', 47 | folderPath) 48 | 49 | def OpenWorkspace(self, fileOrFolder: str): 50 | path = Path(fileOrFolder) 51 | if path.is_dir(): 52 | self.LoadWorkspaceFolder(path) 53 | else: 54 | self.LoadWorkspaceFile(path) 55 | 56 | def workspaceSettingsOp(self): 57 | return SettingsOp('workspace', op=self.ownerComp, pages=['Settings']) 58 | 59 | def _LoadWorkspace(self, settingsPath: Optional[Path], folderPath: Optional[Path]): 60 | ipar.workspaceState.Rootfolder = folderPath or '' 61 | self.ownerComp.par.Settingsfile = settingsPath or '' 62 | if folderPath is not None: 63 | folderPath.mkdir(parents=True, exist_ok=True) 64 | self.loadSettingsFile(settingsPath) 65 | self.ownerComp.par.Name = self.ownerComp.par.Name or (folderPath.name if folderPath is not None else '') 66 | self.ownerComp.par.Onworkspaceload.pulse() 67 | 68 | def applySettings(self, settings: dict): 69 | super().applySettings(settings) 70 | self.ownerComp.par.Settings = settings 71 | 72 | def UnloadWorkspace(self): 73 | self._LoadWorkspace(None, None) 74 | self.ownerComp.par.Onworkspaceunload.pulse() 75 | 76 | def SaveSettings(self): 77 | if not ipar.workspaceState.Rootfolder or not self.ownerComp.par.Settingsfile: 78 | raise Exception('No workspace selected') 79 | folderPath = Path(ipar.workspaceState.Rootfolder.eval()) 80 | folderPath.mkdir(parents=True, exist_ok=True) 81 | settingsPath = Path(self.ownerComp.par.Settingsfile.eval()) 82 | settings = self.saveSettingsFile(settingsPath) 83 | self.ownerComp.par.Settings = settings 84 | print(f'Saved workspace settings to {settingsPath}') 85 | -------------------------------------------------------------------------------- /rack/editor/components/editorTools.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/editor/components/editorTools.tox -------------------------------------------------------------------------------- /rack/editor/components/editorViews.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/editor/components/editorViews.tox -------------------------------------------------------------------------------- /rack/editor/components/preview_panel.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/editor/components/preview_panel.tox -------------------------------------------------------------------------------- /rack/editor/components/settings.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/editor/components/settings.tox -------------------------------------------------------------------------------- /rack/editor/components/topMenuCallbacks.py: -------------------------------------------------------------------------------- 1 | """ 2 | TopMenu callbacks 3 | 4 | Callbacks always take a single argument, which is a dictionary 5 | of values relevant to the callback. Print this dictionary to see what is 6 | being passed. The keys explain what each item is. 7 | 8 | TopMenu info keys: 9 | 'widget': the TopMenu widget 10 | 'item': the item label in the menu list 11 | 'index': either menu index or -1 for none 12 | 'indexPath': list of parent menu indexes leading to this item 13 | 'define': TopMenu define DAT definition info for this menu item 14 | 'menu': the popMenu component inside topMenu 15 | """ 16 | 17 | 18 | 19 | def getMenuItems(info): 20 | items = ext.editor.GetMenuItems(**info) 21 | # print(f'getMenuItems -> {items}') 22 | return items 23 | 24 | 25 | def onItemTrigger(info): 26 | ext.editor.OnMenuTrigger(**info) 27 | 28 | # standard menu callbacks 29 | 30 | def onSelect(info): 31 | """ 32 | User selects a menu option 33 | """ 34 | # debug(info) 35 | pass 36 | 37 | def onRollover(info): 38 | """ 39 | Mouse rolled over an item 40 | """ 41 | 42 | def onOpen(info): 43 | """ 44 | Menu opened 45 | """ 46 | 47 | def onClose(info): 48 | """ 49 | Menu closed 50 | """ 51 | 52 | def onMouseDown(info): 53 | """ 54 | Item pressed 55 | """ 56 | 57 | def onMouseUp(info): 58 | """ 59 | Item released 60 | """ 61 | 62 | def onClick(info): 63 | """ 64 | Item pressed and released 65 | """ 66 | 67 | def onLostFocus(info): 68 | """ 69 | Menu lost focus 70 | """ -------------------------------------------------------------------------------- /rack/editor/components/topMenuDefine.txt: -------------------------------------------------------------------------------- 1 | button item1 item2 callback dividerAfter disable checked highlight shortcut rowCallback specialId actionOp actionMethod statePar itemValue itemDepth 2 | Workspace 3 | Open Workspace File onItemTrigger workspace PromptLoadWorkspaceFile 4 | Open Workspace Folder onItemTrigger workspace PromptLoadWorkspaceFolder 5 | Save Workspace onItemTrigger not ipar.workspace.Rootfolder workspace SaveSettings 6 | Close Workspace onItemTrigger not ipar.workspace.Rootfolder workspace UnloadWorkspace 7 | Recent True 8 | getMenuItems recentWorkspaces 2 9 | Component 10 | Save onItemTrigger not ipar.editorState.Hascomponent SaveComponent 11 | Save As onItemTrigger not ipar.editorState.Hascomponent PromptComponentSaveAs 12 | Save New Version onItemTrigger not ipar.editorState.Hascomponent SaveComponentNewVersion 13 | Close onItemTrigger True not ipar.editorState.Hascomponent UnloadComponent 14 | Show Component Network onItemTrigger not ipar.editorState.Hascomponent ShowNetwork 15 | Show Component Network (Popup) onItemTrigger not ipar.editorState.Hascomponent ShowNetworkPopup 16 | Customize Component onItemTrigger not ipar.editorState.Hascomponent CustomizeComponent 17 | View 18 | Left Panel True 19 | getMenuItems Selectedleftpanel 2 20 | Main View True 21 | getMenuItems Selectedview 2 22 | Right Panel True 23 | getMenuItems Selectedrightpanel 2 24 | -------------------------------------------------------------------------------- /rack/editor/components/workspace.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/editor/components/workspace.tox -------------------------------------------------------------------------------- /rack/editor/editor.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/editor/editor.tox -------------------------------------------------------------------------------- /rack/rack/RackExt.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Tuple 2 | 3 | # noinspection PyUnreachableCode 4 | if False: 5 | # noinspection PyUnresolvedReferences 6 | from _stubs import * 7 | 8 | class Rack: 9 | def __init__(self, ownerComp): 10 | self.ownerComp = ownerComp 11 | 12 | @property 13 | def RackTools(self): return self.ownerComp.op('rack_tools') 14 | 15 | @property 16 | def RackToolsPane(self) -> Optional['Pane']: 17 | tools = self.RackTools 18 | for pane in ui.panes: 19 | if pane.owner == tools and pane.type == PaneType.PANEL: 20 | return pane 21 | 22 | @property 23 | def RackToolsPaneSize(self) -> Tuple[int, int]: 24 | pane = self.RackToolsPane 25 | if not pane: 26 | return 0, 0 27 | w = pane.topRight.x - pane.bottomLeft.x 28 | h = pane.topRight.y - pane.bottomLeft.y 29 | return w, h 30 | -------------------------------------------------------------------------------- /rack/rack/rack.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/rack/rack.tox -------------------------------------------------------------------------------- /rack/rack_tools/rack_tools.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/rack_tools/rack_tools.tox -------------------------------------------------------------------------------- /recorder.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import pathlib 4 | import shutil 5 | 6 | from common import ExtensionBase, loggedmethod, formatValue, parseValue 7 | 8 | # noinspection PyUnreachableCode 9 | if False: 10 | # noinspection PyUnresolvedReferences 11 | from _stubs import * 12 | 13 | _inputResMultipliers = { 14 | 'quarter': 0.25, 15 | 'half': 0.5, 16 | 'original': 1, 17 | 'double': 2, 18 | 'quadruple': 4, 19 | } 20 | 21 | class Recorder(ExtensionBase): 22 | def __init__(self, ownerComp): 23 | super().__init__(ownerComp) 24 | self.DiskSpace = tdu.Dependency(None) 25 | if self.IsRecordingVideo: 26 | self.EndVideoCapture() 27 | 28 | def BuildImageFileName(self): 29 | folder = self._OutputFolderPath 30 | return self._BuildFileName(folder, isvideo=False, isimagesequence=False) 31 | 32 | def BuildImageSequenceFileName(self): 33 | folder = self._OutputFolderPath 34 | return self._BuildFileName(folder, isvideo=False, isimagesequence=True) 35 | 36 | def BuildVideoFileName(self): 37 | folder = self._OutputFolderPath 38 | return self._BuildFileName(folder, isvideo=True, isimagesequence=False) 39 | 40 | def _GetFileBaseName(self): 41 | name = self.ownerComp.par.Basename.eval() 42 | if not name: 43 | name = os.path.splitext(project.name)[0] + '-output' 44 | if name.endswith('.toe'): 45 | name = name[:-4] 46 | if self.ownerComp.par.Includedate: 47 | name += '-' + str(datetime.date.today()) 48 | return name + '-' 49 | 50 | @property 51 | def Resolution(self): 52 | if self.ownerComp.par.Useinputres: 53 | video = self.ownerComp.op('./video') 54 | multipliername = self.ownerComp.par.Inputresmult.eval() 55 | w, h = video.width, video.height 56 | mult = _inputResMultipliers.get(multipliername) or 1 57 | w = round(w * mult) 58 | h = round(h * mult) 59 | else: 60 | w, h = int(self.ownerComp.par.Resolution1), int(self.ownerComp.par.Resolution2) 61 | return w, h 62 | 63 | @property 64 | def FormattedResolution(self): 65 | w, h = self.Resolution 66 | return '{}x{}'.format(w, h) 67 | 68 | @property 69 | def _VideoCodecSuffix(self): 70 | vcodec = self.ownerComp.par.Videocodec.eval() 71 | suffix = self.ownerComp.op('./video_codecs')[vcodec, 'suffix'] 72 | return suffix.val if suffix and suffix.val else vcodec 73 | 74 | @property 75 | def _ImageExtension(self): 76 | imgtype = self.ownerComp.par.Imagefiletype.eval() 77 | ext = self.ownerComp.op('./image_ext_overrides')[imgtype, 1] 78 | return ext.val if ext else imgtype 79 | 80 | @property 81 | def _HasAudio(self): 82 | return self.ownerComp.op('./have_audio')[0] 83 | 84 | def _GetSuffix(self, isvideo, isimagesequence): 85 | suffix = self.ownerComp.par.Suffix.eval() 86 | suffixparts = [suffix] if suffix else [] 87 | if self.ownerComp.par.Addresolutionsuffix: 88 | suffixparts.append(self.FormattedResolution) 89 | if isvideo or isimagesequence: 90 | if self.ownerComp.par.Addvcodecsuffix: 91 | suffixparts.append(self._VideoCodecSuffix) 92 | if self.ownerComp.par.Addacodecsuffix and self._HasAudio: 93 | suffixparts.append(self.ownerComp.par.Audiocodec.eval()) 94 | if self.ownerComp.par.Addfpssuffix: 95 | suffixparts.append(str(self.ownerComp.par.Fps) + 'fps') 96 | if isvideo: 97 | ext = '.mov' 98 | else: 99 | ext = '.' + self._ImageExtension 100 | if not suffixparts: 101 | return ext 102 | return '-'.join([''] + suffixparts) + ext 103 | 104 | @property 105 | def _OutputFolderPath(self): 106 | folder = self.ownerComp.par.Folder.eval() 107 | if folder: 108 | folder = mod.tdu.expandPath(folder) 109 | return pathlib.Path(folder or project.folder) 110 | 111 | def _BuildFileName(self, folder: pathlib.Path, isvideo, isimagesequence): 112 | basename = self._GetFileBaseName() 113 | i = 1 114 | if folder.exists(): 115 | while any(folder.glob(basename + str(i) + '[.-]*')): 116 | i += 1 117 | suffix = self._GetSuffix(isvideo, isimagesequence) 118 | return basename + str(i) + suffix 119 | 120 | @staticmethod 121 | def _CreateOutputFolder(folder): 122 | if folder.exists(): 123 | ui.status = 'Output folder exists: {}'.format(folder) 124 | else: 125 | folder.mkdir(parents=True, exist_ok=True) 126 | ui.status = 'Created output folder: {}'.format(folder) 127 | 128 | @property 129 | def _MovieOut(self): 130 | return self.ownerComp.op('moviefileout') 131 | 132 | @property 133 | def IsRecordingVideo(self): 134 | return self._MovieOut.par.record.eval() 135 | 136 | @loggedmethod 137 | def StartVideoCapture(self): 138 | folder = self._OutputFolderPath 139 | self._CreateOutputFolder(folder) 140 | if self.ownerComp.par.Videotype == 'imagesequence': 141 | filename = self.BuildImageSequenceFileName() 142 | else: 143 | filename = self.BuildVideoFileName() 144 | filepath = folder.joinpath(filename) 145 | ui.status = 'Start video capture ' + str(filepath) 146 | fileout = self._MovieOut 147 | fileout.par.file = filepath 148 | fileout.par.record = True 149 | 150 | @loggedmethod 151 | def EndVideoCapture(self): 152 | fileout = self._MovieOut 153 | fileout.par.record = False 154 | ui.status = 'Wrote video to ' + fileout.par.file.eval() 155 | 156 | @loggedmethod 157 | def CaptureImage(self): 158 | folder = self._OutputFolderPath 159 | self._CreateOutputFolder(folder) 160 | filepath = folder.joinpath(self.BuildImageFileName()) 161 | fileout = self.ownerComp.op('video') 162 | fileout.save(filepath) 163 | ui.status = 'Wrote image to ' + str(filepath) 164 | 165 | def UpdateDiskSpace(self): 166 | folder = self._OutputFolderPath 167 | if not folder.exists(): 168 | self.DiskSpace.val = None 169 | else: 170 | space = shutil.disk_usage(str(folder)) 171 | self.DiskSpace.val = space.free 172 | 173 | @property 174 | def FormattedDiskSpace(self): 175 | val = self.DiskSpace.val 176 | return '' if val is None else _formatBytes(val) 177 | 178 | @loggedmethod 179 | def EmergencyShutOff(self): 180 | if not self.IsRecordingVideo: 181 | self._LogEvent('Not recording, nothing to stop') 182 | return 183 | msg = 'WARNING: halting recording due to insufficient disk space' 184 | self._LogEvent(msg) 185 | ui.status = 'WARNING: halting recording due to insufficient disk space' 186 | self.EndVideoCapture() 187 | 188 | def UpdatePanelHeight(self): 189 | height = _panelsHeight(self.ownerComp.op('root_panel').panelChildren) 190 | height += _panelsHeight(self.ownerComp.op('settings_panel').panelChildren) 191 | maxheight = self.ownerComp.par.Maxheight.eval() 192 | if 0 < maxheight < height: 193 | height = maxheight 194 | self.ownerComp.par.h = height 195 | 196 | def GetSettingsDict(self): 197 | pars = [] 198 | for page in self.ownerComp.customPages: 199 | if page.name in ['Output', 'Format', 'Advanced']: 200 | pars += page.pars 201 | return { 202 | par.name: formatValue(par.eval()) 203 | for par in pars 204 | } 205 | 206 | def SetSettingsDict(self, settings: dict): 207 | for key, val in settings.items(): 208 | par = self.ownerComp.par[key] 209 | if par is not None: 210 | par.val = parseValue(val) 211 | 212 | def _panelsHeight(panels): 213 | return sum([ 214 | c.height 215 | for c in panels 216 | if c.isPanel and c.par.display and c.par.vmode == 'fixed' 217 | ]) 218 | 219 | _sizes = ["B", "KB", "MB", "GB", "TB"] 220 | def _formatBytes(bytes_num): 221 | 222 | i = 0 223 | dblbyte = bytes_num 224 | 225 | while i < len(_sizes) and bytes_num >= 1024: 226 | dblbyte = bytes_num / 1024.0 227 | i = i + 1 228 | bytes_num = bytes_num / 1024 229 | 230 | return str(round(dblbyte, 2)) + " " + _sizes[i] 231 | 232 | -------------------------------------------------------------------------------- /recorder.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/recorder.tox -------------------------------------------------------------------------------- /recorder/recorderCore.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/recorder/recorderCore.tox -------------------------------------------------------------------------------- /recorder/recorderCoreExt.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import os 3 | import datetime 4 | 5 | # noinspection PyUnreachableCode 6 | if False: 7 | # noinspection PyUnresolvedReferences 8 | from _stubs import * 9 | 10 | 11 | class RecorderCore: 12 | def __init__(self, ownerComp): 13 | self.ownerComp = ownerComp # type: COMP 14 | self.processedVideo = ownerComp.op('video') 15 | self.movieOut = ownerComp.op('moviefileout') 16 | self.imageOut = ownerComp.op('imagefileout') 17 | self.videoRes = ownerComp.op('video_res') 18 | self.audioState = ownerComp.op('audio_state') 19 | 20 | @property 21 | def formattedResolution(self): 22 | w = int(self.videoRes['width']) 23 | h = int(self.videoRes['height']) 24 | return f'{int(w)}x{int(h)}' 25 | 26 | @property 27 | def videoCodecSuffix(self): 28 | vcodec = self.ownerComp.par.Videocodec.eval() 29 | suffix = self.ownerComp.op('./video_codecs')[vcodec, 'suffix'] 30 | if suffix is None: 31 | return vcodec 32 | return suffix.val 33 | 34 | @property 35 | def imageExtension(self): 36 | imgtype = self.ownerComp.par.Imagefiletype.eval() 37 | extension = self.ownerComp.op('./image_ext_overrides')[imgtype, 1] 38 | return extension.val if extension else imgtype 39 | 40 | @property 41 | def haveAudio(self): 42 | return self.audioState['haveaudio'] > 0 43 | 44 | def getSuffix(self, fileType): 45 | suffix = self.ownerComp.par.Suffix.eval() 46 | suffixParts = [suffix] if suffix else [] 47 | if self.ownerComp.par.Addresolutionsuffix: 48 | suffixParts.append(self.formattedResolution) 49 | if fileType in ['movie', 'imagesequence']: 50 | if self.ownerComp.par.Addvcodecsuffix: 51 | vcSuffix = self.videoCodecSuffix 52 | if vcSuffix: 53 | suffixParts.append(vcSuffix) 54 | if self.ownerComp.par.Audioenabled and self.ownerComp.par.Addacodecsuffix and self.haveAudio: 55 | suffixParts.append(self.ownerComp.par.Audiocodec.eval()) 56 | if self.ownerComp.par.Addfpssuffix: 57 | suffixParts.append(str(self.ownerComp.par.Fps) + 'fps') 58 | if fileType == 'movie': 59 | extension = '.mov' 60 | else: 61 | extension = '.' + self.imageExtension 62 | if not suffixParts: 63 | return extension 64 | return '-'.join([''] + suffixParts) + extension 65 | 66 | @property 67 | def outputFolderPath(self): 68 | folder = self.ownerComp.par.Folder.eval() 69 | if folder: 70 | folder = mod.tdu.expandPath(folder) 71 | return Path(folder or project.folder) 72 | 73 | @property 74 | def fileBaseName(self): 75 | name = self.ownerComp.par.Basename.eval() 76 | if not name: 77 | name = os.path.splitext(project.name)[0] + '-output' 78 | if name.endswith('.toe'): 79 | name = name[:-4] 80 | if self.ownerComp.par.Includedate: 81 | dateMode = self.ownerComp.par.Datetype.eval() 82 | if dateMode == 'custom': 83 | dateValue = self.ownerComp.par.Customdate.eval() 84 | else: 85 | dateValue = str(datetime.date.today()) 86 | if dateValue: 87 | name += '-' + dateValue 88 | return name + '-' 89 | 90 | def buildFileName(self, fileType): 91 | baseName = self.fileBaseName 92 | i = 1 93 | folder = self.outputFolderPath 94 | if folder.exists(): 95 | existingFiles = folder.glob(baseName + '[0-9]*') 96 | maxIndex = None 97 | for file in existingFiles: 98 | try: 99 | fileIndex = int(file.stem[len(baseName):].split('-', 1)[0]) 100 | except ValueError: 101 | continue 102 | if maxIndex is None or fileIndex > maxIndex: 103 | maxIndex = fileIndex 104 | if maxIndex is not None: 105 | i = maxIndex + 1 106 | suffix = self.getSuffix(fileType) 107 | return baseName + str(i) + suffix 108 | 109 | def CreateOutputFolder(self): 110 | folder = self.outputFolderPath 111 | if folder.exists(): 112 | # ui.status = 'Output folder exists: {}'.format(folder) 113 | pass 114 | else: 115 | folder.mkdir(parents=True, exist_ok=True) 116 | ui.status = 'Created output folder: ' + str(folder) 117 | return folder 118 | 119 | def BuildImageFileName(self): 120 | return self.buildFileName('image') 121 | 122 | def BuildVideoFileName(self): 123 | return self.buildFileName('movie') 124 | 125 | def BuildImageSequenceFileName(self): 126 | return self.buildFileName('imagesequence') 127 | 128 | def StartVideoCapture(self): 129 | folder = self.CreateOutputFolder() 130 | if self.ownerComp.par.Videotype == 'imagesequence': 131 | fileName = self.BuildImageSequenceFileName() 132 | else: 133 | fileName = self.BuildVideoFileName() 134 | filePath = folder.joinpath(fileName) 135 | ui.status = 'Start video capture ' + str(filePath) 136 | self.movieOut.par.file = filePath 137 | self.movieOut.par.record = True 138 | 139 | def EndVideoCapture(self): 140 | if not self.movieOut.par.record: 141 | return 142 | self.movieOut.par.record = False 143 | ui.status = 'Wrote video to ' + str(self.movieOut.par.file) 144 | self.movieOut.par.file = '' 145 | 146 | def CaptureImage(self): 147 | folder = self.CreateOutputFolder() 148 | filePath = folder.joinpath(self.BuildImageFileName()) 149 | self.imageOut.par.file = filePath 150 | self.imageOut.par.record.pulse(1) 151 | ui.status = 'Wrote image to ' + str(filePath) 152 | self.imageOut.par.file = '' 153 | 154 | 155 | -------------------------------------------------------------------------------- /sop_merger.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/sop_merger.tox -------------------------------------------------------------------------------- /td-components-tester.toe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/td-components-tester.toe -------------------------------------------------------------------------------- /tools/ParamHelperExt.py: -------------------------------------------------------------------------------- 1 | # noinspection PyUnresolvedReferences 2 | import tdu 3 | 4 | # noinspection PyUnreachableCode 5 | if False: 6 | from _stubs import * 7 | 8 | class ParamHelper: 9 | def __init__(self, ownerComp): 10 | self.ownerComp = ownerComp 11 | 12 | @property 13 | def DestinationPageNames(self): 14 | d = self._Destination 15 | return [p.name for p in d.customPages] if d else [] 16 | 17 | def HandleDrop(self, dropName, xPos, yPos, index, totalDragged, dropExt, baseName, destPath): 18 | print('ParamHelper HandleDrop {!r}'.format(locals())) 19 | if dropExt == 'parameter': 20 | o = op(baseName) 21 | par = getattr(o.par, dropName) if o else None 22 | if par is not None: 23 | self._ApplyChangesToParameter(par) 24 | elif dropExt in ('DAT', 'TOP', 'CHOP', 'SOP', 'MAT'): 25 | parentOp = op(baseName) 26 | o = parentOp.op(dropName) if parentOp else None 27 | if o is not None: 28 | self._HandleDropOp(o) 29 | pass 30 | 31 | def _HandleDropOp(self, o: 'OP'): 32 | pass 33 | 34 | @property 35 | def _Destination(self) -> 'COMP': 36 | return self.ownerComp.par.Destination.eval() 37 | 38 | def _UpdateStatus(self, message: str): 39 | ui.status = message 40 | statuses = self.ownerComp.op('set_statuses') 41 | statuses.appendRow([message]) 42 | 43 | def _ApplyChangesToParameter(self, par: 'Par'): 44 | dest = self._Destination 45 | if not dest: 46 | self._UpdateStatus('No destination OP') 47 | return 48 | if not dest.isCOMP: 49 | self._UpdateStatus('Destination is not a COMP!') 50 | return 51 | namePrefix = self.ownerComp.par.Nameprefix.eval() 52 | if self.ownerComp.par.Labelusecustomprefix: 53 | labelPrefix = self.ownerComp.par.Labelprefix.eval() 54 | elif not namePrefix: 55 | labelPrefix = '' 56 | else: 57 | labelPrefix = namePrefix 58 | if labelPrefix and not labelPrefix.endswith(' '): 59 | labelPrefix += ' ' 60 | namePrefix = tdu.legalName(namePrefix) 61 | newName = tdu.legalName(namePrefix + par.name).capitalize() 62 | label = labelPrefix + par.label 63 | if self.ownerComp.par.Pageusecustomname: 64 | pageName = self.ownerComp.par.Page.eval() 65 | else: 66 | pageName = par.page.name 67 | if not pageName: 68 | pageName = 'Custom' 69 | existingPar = getattr(dest.par, newName, None) 70 | if existingPar is not None: 71 | self._UpdateStatus('Updating existing parameter: {}'.format(newName)) 72 | existingPar.label = label 73 | newPar = existingPar 74 | else: 75 | self._UpdateStatus('Attempting to get/create page: {!r}'.format(pageName)) 76 | page = dest.appendCustomPage(pageName) 77 | self._UpdateStatus('Attempting to create par {!r}'.format(newName)) 78 | newPar = page.appendPar(newName, par=par, label=label)[0] 79 | self._UpdateStatus('Created parameter {!r}'.format(newPar)) 80 | if newPar.defaultExpr in (None, 'None'): 81 | newPar.defaultExpr = '' 82 | newPar.default = par.default 83 | attachType = self.ownerComp.par.Attachmenttype.eval() 84 | if attachType == 'none': 85 | return 86 | if self.ownerComp.par.Attachdirection == 'oldmaster': 87 | fromOp = dest 88 | toOp = par.owner 89 | fromPar = newPar 90 | toPar = par 91 | else: 92 | fromOp = par.owner 93 | toOp = dest 94 | fromPar = par 95 | toPar = newPar 96 | self._UpdateStatus('Attempting to connect {!r} to {!r} using {}'.format( 97 | toPar, fromPar, attachType)) 98 | expr = self._GetParExpr( 99 | fromOp=fromOp, toOp=toOp, parName=toPar.name) 100 | self._UpdateStatus('Attempting to connect {!r} to {!r} using {}, expr: {!r}'.format( 101 | toPar, fromPar, attachType, expr)) 102 | if not expr: 103 | return 104 | if attachType == 'binding': 105 | fromPar.bindExpr = expr 106 | elif attachType == 'reference': 107 | fromPar.expr = expr 108 | 109 | def _GetParExpr(self, fromOp: 'OP', toOp: 'OP', parName: str): 110 | pathType = self.ownerComp.par.Referencepathtype.eval() 111 | if pathType == 'absolute': 112 | return 'op({!r}).par.{}'.format(toOp.path, parName) 113 | elif pathType == 'relative': 114 | path = fromOp.relativePath(toOp) 115 | if not path: 116 | self._UpdateStatus('Unable to create relative path from {} to {}'.format(fromOp, toOp)) 117 | return None 118 | return 'op({!r}).par.{}'.format(path, parName) 119 | else: 120 | path = fromOp.shortcutPath(toOp, toParName=parName) 121 | if not path: 122 | self._UpdateStatus('Unable to create shortcut path from {} to {}'.format(fromOp, toOp)) 123 | return path 124 | -------------------------------------------------------------------------------- /tools/PathSetupExt.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | # noinspection PyUnreachableCode 4 | if False: 5 | # noinspection PyUnresolvedReferences 6 | from _stubs import * 7 | 8 | class PathSetup: 9 | def __init__(self, ownerComp): 10 | self.ownerComp = ownerComp 11 | 12 | def BuildPossiblePathsFromParams(self, dat: 'DAT'): 13 | dat.clear() 14 | for i in range(1, 5): 15 | name = getattr(self.ownerComp.par, 'Path{}name'.format(i), None) 16 | if not name: 17 | continue 18 | for j in range(1, 5): 19 | folder = getattr(self.ownerComp.par, 'Path{}folder{}'.format(i, j), None) 20 | if folder: 21 | dat.appendRow([name, folder]) 22 | 23 | @staticmethod 24 | def PreparePathTable(outDat: 'DAT', inDat: 'DAT'): 25 | paths = {} 26 | for cells in inDat.rows(): 27 | name = cells[0].val 28 | if name in paths: 29 | continue 30 | path = os.path.expanduser(os.path.expandvars(cells[1].val)) 31 | if os.path.exists(path): 32 | paths[name] = path 33 | outDat.clear() 34 | for name, path in paths.items(): 35 | outDat.appendRow([name, path]) 36 | 37 | def ApplyPaths(self): 38 | table = self.ownerComp.op('path_table') # type: DAT 39 | newPaths = {name.val: path.val for name, path in table.rows()} 40 | missingPathNames = [name for name in project.paths if name not in newPaths] 41 | for name, path in newPaths.items(): 42 | project.paths[name] = path 43 | if self.ownerComp.par.Removeotherpaths: 44 | for name in missingPathNames: 45 | del project.paths[name] 46 | self.ownerComp.op('build_current_paths_table').cook() 47 | 48 | @staticmethod 49 | def ClearAllPaths(): 50 | project.paths.clear() 51 | 52 | @staticmethod 53 | def BuildCurrentPathsTable(dat: 'DAT'): 54 | dat.clear() 55 | for name, path in project.paths.items(): 56 | dat.appendRow([name, path]) 57 | -------------------------------------------------------------------------------- /tools/UIColorEditorExt.py: -------------------------------------------------------------------------------- 1 | class UIColorEditor: 2 | def __init__(self, ownerComp): 3 | self.ownerComp = ownerComp 4 | 5 | @staticmethod 6 | def GetUIColors(): 7 | return { 8 | key: ui.colors[key] 9 | for key in ui.colors 10 | } 11 | 12 | @staticmethod 13 | def BuildColorNameTable(dat): 14 | dat.clear() 15 | dat.appendRow(['fullname', 'namespace', 'subname']) 16 | for fullname in sorted(ui.colors): 17 | fullname: str 18 | if '.' not in fullname: 19 | dat.appendRow([fullname, '', fullname]) 20 | else: 21 | parts = fullname.split('.', maxsplit=3) 22 | dat.appendRow([fullname, '.'.join(parts[:-1]), parts[-1]]) 23 | 24 | @staticmethod 25 | def BuildColorTable(dat): 26 | dat.clear() 27 | dat.appendRow(['name', 'r', 'g', 'b']) 28 | for key in sorted(ui.colors): 29 | dat.appendRow([key] + list(ui.colors[key])) 30 | 31 | def UpdateListColors(self, colorTable): 32 | colorList = self.ownerComp.op('color_list').par.Lister.eval() 33 | for i in range(1, colorTable.numRows): 34 | color = colorTable[i, 'r'], colorTable[i, 'g'], colorTable[i, 'b'], 1 35 | colorList.SetCellOverlay(i, 1, color) 36 | colorList.SetCellOverlay(i, 2, color) 37 | colorList.SetCellOverlay(i, 3, color) 38 | 39 | pass 40 | -------------------------------------------------------------------------------- /tools/base_save.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/tools/base_save.tox -------------------------------------------------------------------------------- /tools/paramAdjuster.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/tools/paramAdjuster.tox -------------------------------------------------------------------------------- /tools/paramAdjusterExt.py: -------------------------------------------------------------------------------- 1 | # noinspection PyUnreachableCode 2 | if False: 3 | # noinspection PyUnresolvedReferences 4 | from _stubs import * 5 | 6 | def getNormVal(p: 'Par'): 7 | if p is None or p.isPulse or p.isMomentary or p.isOP: 8 | return 0 9 | if p.isMenu: 10 | return p.menuIndex / (len(p.menuNames) - 1) 11 | if p.isString: 12 | return 0 13 | if p.isNumber: 14 | return p.normVal 15 | return 0 16 | 17 | def setNormVal(p: 'Par', normVal: float): 18 | if p is None or p.isOP: 19 | return False 20 | if p.isPulse or p.isMomentary: 21 | p.pulse(1) 22 | elif p.isMenu: 23 | p.menuIndex = round(normVal * (len(p.menuNames) - 1)) 24 | elif p.isString: 25 | return False 26 | elif p.isNumber: 27 | p.normVal = normVal 28 | else: 29 | return False 30 | return True 31 | 32 | lastControl = None 33 | 34 | def onMidiInput(channel: 'Channel'): 35 | p = ui.rolloverPar 36 | if p is None: 37 | return 38 | name = channel.name 39 | d = tdu.digits(name) 40 | if d is not None: 41 | name = tdu.base(name) + str(d - 1) 42 | op('set_output_val').par.name0 = name 43 | setNormVal(p, float(channel)) 44 | pass 45 | 46 | def onTableChange(dat): 47 | path = dat['path', 1].val 48 | o = path and op(path) 49 | if not o: 50 | return 51 | p = o.par[dat['param', 1]] 52 | normVal = 0 53 | if p is None: 54 | return 55 | if p.isPulse or p.isMomentary: 56 | return 57 | if p.isMenu: 58 | normVal = p.menuIndex / (len(p.menuNames)-1) 59 | elif p.isString: 60 | return 61 | elif p.isNumber: 62 | normVal = p.normVal 63 | op('set_output_val').par.value0 = normVal 64 | return 65 | 66 | def onValueChange(channel, sampleIndex, val, prev): 67 | p = ui.rolloverPar 68 | # op('set_last_input_name')[0,0] = channel.name 69 | name = channel.name 70 | d = tdu.digits(name) 71 | if d is not None: 72 | name = tdu.base(name) + str(d - 1) 73 | op('set_output_val').par.name0 = name 74 | if p is None or p.isOP or p.isPython: 75 | return 76 | if p.isPulse or p.isMomentary: 77 | p.pulse() 78 | else: 79 | val /= 127.0 80 | if p.isMenu: 81 | i = round(val * (len(p.menuNames) - 1)) 82 | p.menuIndex = i 83 | elif p.isString: 84 | return 85 | elif p.isNumber: 86 | if p.isInt: 87 | p.val = round(tdu.remap(val, 0, 1, p.normMin, p.normMax)) 88 | else: 89 | p.normVal = val 90 | 91 | class ParamAdjuster: 92 | def __init__(self, ownerComp: 'COMP'): 93 | self.ownerComp = ownerComp 94 | self.lastControl = None 95 | 96 | def onMidiInput(self, channel: 'Channel'): 97 | p = ui.rolloverPar 98 | if p is None: 99 | return 100 | if setNormVal(p, float(channel)): 101 | self.lastControl = tdu.digits(channel.name) 102 | 103 | def onRolloverParamChange(self): 104 | p = ui.rolloverPar 105 | pass 106 | -------------------------------------------------------------------------------- /tools/param_helper.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/tools/param_helper.tox -------------------------------------------------------------------------------- /tools/path_setup.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/tools/path_setup.tox -------------------------------------------------------------------------------- /tools/saveEXT.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if False: 4 | from _stubs import * 5 | 6 | 7 | class ExternalFiles: 8 | """ 9 | The ExternalFiles class is used to handle working with both externalizing files, 10 | as well as ingesting files that were previously externalized. This helps 11 | to minimize the amount of manual work that might need to otherise be used 12 | for handling external files. 13 | """ 14 | 15 | def __init__(self, my_op): 16 | """ 17 | Stands in place of an execute dat - ensures all elements start-up correctly 18 | 19 | Notes 20 | --------- 21 | 22 | Args 23 | --------- 24 | myOp (touchDesignerOperator): 25 | > the operator that is loading the current extension 26 | 27 | Returns 28 | --------- 29 | none 30 | """ 31 | 32 | self.my_op = my_op 33 | self.Flash_duration = 4 34 | 35 | init_msg = "Save init from {}".format(my_op) 36 | self.Defaultcolor = self.my_op.pars('Defaultcolor*') 37 | self.Op_finder = self.my_op.op('opfind1') 38 | self.Extension_flag = self.my_op.par.Extensionflag.val 39 | 40 | print(init_msg) 41 | 42 | return 43 | 44 | def Prompt_to_save(self, current_loc): 45 | """ 46 | The method used to save an external TOX to file 47 | 48 | Notes 49 | --------- 50 | 51 | Args 52 | --------- 53 | current_loc (str): 54 | > the operator that's related to the currently focused 55 | > pane object. This is required to ensure that we correctly 56 | > grab the appropriate COMP and check to see if needs to be saved 57 | 58 | Returns 59 | --------- 60 | none 61 | """ 62 | 63 | ext_color = self.my_op.pars("Extcolor*") 64 | msg_box_title = "TOX Save" 65 | msg_box_msg = "Replacing External\n\nYou are about to overwite an external TOX" 66 | msg_box_buttons = ["Cancel", "Continue"] 67 | 68 | sav_msg_box_title = "Externalize Tox" 69 | sav_msg_box_msg = "This TOX is not yet externalized\n\nWould you like to externalize this TOX?" 70 | sav_msg_box_buttons = ["No", "Yes"] 71 | 72 | save_msg_buttons_parent_too = ["No", "This COMP Only", "This COMP and the Parent"] 73 | 74 | # check if location is the root of the project 75 | if current_loc == '/': 76 | # skip if we're at the root of the project 77 | pass 78 | 79 | else: 80 | # if we're not at the root of the project 81 | 82 | # check if external 83 | if current_loc.par.externaltox != '' and 'noextern' not in current_loc.tags: 84 | confirmation = ui.messageBox( 85 | msg_box_title, msg_box_msg + '\ncomponent: ' + current_loc.path, 86 | buttons=msg_box_buttons) 87 | 88 | if confirmation: 89 | 90 | # save external file 91 | self.Save_over_tox(current_loc) 92 | 93 | else: 94 | # if the user presses "cancel" we pass 95 | pass 96 | 97 | # in this case we are not external, so let's ask if we want to externalize the file 98 | else: 99 | 100 | # check if the parent is externalized 101 | if current_loc.parent().par.externaltox != '' and 'noextern' not in current_loc.parent().tags: 102 | save_ext = ui.messageBox( 103 | sav_msg_box_title, 104 | sav_msg_box_msg + 105 | '\nparent: ' + current_loc.parent().path + 106 | '\ncomponent: ' + current_loc.path, 107 | buttons=save_msg_buttons_parent_too) 108 | 109 | # save this comp only 110 | if save_ext == 1: 111 | self.Save_tox(current_loc) 112 | 113 | # save this comp and the parent 114 | elif save_ext == 2: 115 | self.Save_tox(current_loc) 116 | print("save this tox") 117 | 118 | # save parent() COMP 119 | self.Save_over_tox(current_loc.parent()) 120 | print("Save the parent too!") 121 | 122 | # user selected 'No' 123 | else: 124 | pass 125 | 126 | # the parent is not external, so let's ask about externalizing the tox 127 | elif 'noextern' not in current_loc.tags: 128 | save_ext = ui.messageBox( 129 | sav_msg_box_title, sav_msg_box_msg + '\ncomponent: ' + current_loc.path, 130 | buttons=sav_msg_box_buttons) 131 | 132 | if save_ext: 133 | self.Save_tox(current_loc) 134 | 135 | else: 136 | # the user selected "No" 137 | pass 138 | 139 | return 140 | 141 | def Save_over_tox(self, current_loc): 142 | ext_color = self.my_op.pars("Extcolor*") 143 | external_path = current_loc.par.externaltox 144 | current_loc.save(external_path) 145 | 146 | # set color for COMP 147 | current_loc.color = (ext_color[0], ext_color[1], ext_color[2]) 148 | 149 | # flash color 150 | self.Flash_bg("Bgcolor") 151 | 152 | # create and print log message 153 | log_msg = "{} saved to {}/{}".format( 154 | current_loc, 155 | project.folder, 156 | external_path) 157 | 158 | self.Logtotextport(log_msg) 159 | 160 | return 161 | 162 | def Save_tox(self, current_loc): 163 | ext_color = self.my_op.pars("Extcolor*") 164 | 165 | # ask user for a save location 166 | save_loc = ui.chooseFolder(title="TOX Location", start=project.folder) 167 | 168 | if not save_loc: 169 | self.Logtotextport('No file location selected for {}'.format(current_loc)) 170 | return 171 | 172 | # construct a relative path and relative loaction for our elements 173 | print(save_loc) 174 | rel_path = tdu.collapsePath(save_loc) 175 | 176 | # check to see if the location is at the root of the project folder structure 177 | if rel_path == "$TOUCH": 178 | rel_loc = '{new_tox}/{new_tox}.tox'.format(new_tox=current_loc.name) 179 | 180 | # save path is not in the root of the project 181 | else: 182 | rel_loc = '{new_module}/{new_tox}/{new_tox}.tox'.format(new_module=rel_path, new_tox=current_loc.name) 183 | 184 | # create path and directory in the OS 185 | new_path = '{selected_path}/{new_module}'.format(selected_path=save_loc, new_module=current_loc.name) 186 | os.mkdir(new_path) 187 | 188 | # format our tox path 189 | tox_path = '{dir_path}/{tox}.tox'.format(dir_path=new_path, tox=current_loc.name) 190 | 191 | # setup our module correctly 192 | current_loc.par.externaltox = rel_loc 193 | current_loc.par.savebackup = False 194 | 195 | # set color for COMP 196 | current_loc.color = (ext_color[0], ext_color[1], ext_color[2]) 197 | 198 | # save our tox 199 | current_loc.save(tox_path) 200 | 201 | # flash color 202 | self.Flash_bg("Bgcolor") 203 | 204 | # create and print log message 205 | log_msg = "{} saved to {}/{}".format( 206 | current_loc, 207 | project.folder, 208 | tox_path) 209 | self.Logtotextport(log_msg) 210 | return 211 | 212 | def Reload_ext_dat(self, external_file): 213 | """ 214 | Used to reload DAT files, and to re-init modules. 215 | 216 | Notes 217 | --------- 218 | 219 | Args 220 | --------- 221 | external_file (str): 222 | > the operator that's related to the currently focused 223 | > pane object. This is required to ensure that we correctly 224 | > grab the appropriate COMP and check to see if needs to be saved 225 | 226 | Returns 227 | --------- 228 | none 229 | """ 230 | 231 | file_path = '{project}/{file}'.format(project=project.folder, file=external_file) 232 | 233 | # loop through all the dats 234 | for each_op in self.Op_finder.cols(2)[0][1:]: 235 | each_op_path = op(each_op).par.file.val 236 | # print( file_path == each_op_path) 237 | 238 | # check our file path to see if it's relative or absolute 239 | # change our path if we need to 240 | if os.path.isabs(each_op_path): 241 | pass 242 | else: 243 | each_op_path = '{}/{}'.format(project.folder, each_op_path) 244 | 245 | # if there's a match reload the DAT 246 | if file_path == each_op_path: 247 | op(each_op.val).par.loadonstartpulse.pulse() 248 | 249 | # flash the background so we know a file has been loaded 250 | self.Flash_bg("Savecolor") 251 | 252 | # check to see if the external file is python 253 | if external_file.split('.')[1] == "py": 254 | 255 | # check to see if an op is flagged as an extension: 256 | if self.Extension_flag in op(each_op.val).tags: 257 | 258 | # check to see the op's parent has any extensions 259 | extension_pars = [ext for ext in op(each_op.val).parent().pars('extension*')] 260 | if len(extension_pars) > 0: 261 | # print(op(each_op.val).parent()) 262 | 263 | # reinit the parent's extensions 264 | op(each_op.val).parent().par.reinitextensions.pulse() 265 | 266 | elif op(each_op.val).parent().isCOMP and op(each_op.val).parent().par.externaltox != '': 267 | # print("This needs reinit") 268 | 269 | # if the DAT has a parent COMP, reinit the extension 270 | op(each_op.val).parent().par.reinitextensions.pulse() 271 | 272 | self.Flash_bg("Savecolor") 273 | 274 | else: 275 | # COMP is the only consideration we care about at the moment 276 | pass 277 | else: 278 | # skip other file types for now 279 | pass 280 | 281 | # stop once we have a hit 282 | break 283 | 284 | else: 285 | pass 286 | 287 | return 288 | 289 | def Flash_bg(self, parColors): 290 | """ 291 | Used to flash the background of the TD network. 292 | 293 | Notes 294 | --------- 295 | This is a simple tool to flash indicator colors in the 296 | background to help you have some visual confirmation that 297 | you have in fact externalized a file. 298 | 299 | Args 300 | --------- 301 | parColors (str): 302 | > this is the string name to match against the parent's pars() 303 | > for to pull colors to use for changing the background 304 | 305 | Returns 306 | --------- 307 | none 308 | """ 309 | par_color = '{}*'.format(parColors) 310 | over_ride_color = self.my_op.pars(par_color) 311 | 312 | # change background color (0.1, 0.105, 0.12) 313 | ui.colors['worksheet.bg'] = over_ride_color 314 | delay_script = "ui.colors['worksheet.bg'] = args[0]" 315 | 316 | # want to change the background color back 317 | run(delay_script, self.Defaultcolor, delayFrames=self.Flash_duration) 318 | 319 | return 320 | 321 | def Logtotextport(self, logMsg): 322 | 323 | if self.my_op.par.Logtotextport: 324 | print(logMsg) 325 | 326 | else: 327 | pass 328 | 329 | return 330 | -------------------------------------------------------------------------------- /tools/ui_color_editor.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/tools/ui_color_editor.tox -------------------------------------------------------------------------------- /ui/slider_value_overlay.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/ui/slider_value_overlay.tox -------------------------------------------------------------------------------- /ui/statusOverlay-readme.txt: -------------------------------------------------------------------------------- 1 | statusOverlay 2 | Version: 1.0 3 | Author: Tekt 4 | 5 | Shows temporary or permanent status messages as an overlay 6 | on a UI panel. 7 | 8 | By default the panel will be non-visible. When messages are 9 | added, the panel will show the messages and block the UI 10 | underneath it. Once no more messages are present, the overlay 11 | will hide itself. 12 | 13 | Temporary messages will be shown for a limited time (controlled 14 | by the "Temporary Message Duration" parameter), after which 15 | they will disappear. 16 | 17 | Static messages will be shown until they are explicitly removed. 18 | 19 | 20 | To use, make it a child of the panel that it should cover, and 21 | make sure that the following parameters are set (which they 22 | will be by default when dropping in the tox): 23 | 24 | - Depth Layer: 1 25 | - Horizontal Mode: Fill 26 | - Vertical Mode: Fill 27 | - Parent Alignment: Ignore 28 | - Display: False 29 | 30 | 31 | To add a temporary message, either call `.AddMessage('some text')` 32 | or put the text in the "Message to Add" parameter and click the 33 | "Add Temporary Message" pulse parameter. 34 | 35 | To add a static message, either call `.AddStaticMessage('some text')` 36 | or use the "Add Static Message" pulse parameter. Calling the method 37 | will return an identifier that can later be passed to 38 | `.ClearMessage(messageId)`. 39 | 40 | To remove all messages, either call `.ClearAllMessages()` or use 41 | the "Clear Messages" pulse parameter. -------------------------------------------------------------------------------- /ui/statusOverlay-thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/ui/statusOverlay-thumb.jpg -------------------------------------------------------------------------------- /ui/statusOverlay.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/ui/statusOverlay.tox -------------------------------------------------------------------------------- /ui/statusOverlayExt.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from dataclasses import dataclass 3 | 4 | # noinspection PyUnreachableCode 5 | if False: 6 | # noinspection PyUnresolvedReferences 7 | from _stubs import * 8 | 9 | class StatusOverlay: 10 | def __init__(self, ownerComp): 11 | self.ownerComp = ownerComp # type: panelCOMP 12 | self.messages = [] # type: List[_Message] 13 | self.timer = ownerComp.op('update_timer') # type: timerCHOP 14 | self.labelComp = ownerComp.op('label') # type: panelCOMP 15 | self.nextId = 0 16 | 17 | def ClearAllMessages(self): 18 | self.stopTimer() 19 | self.messages.clear() 20 | self.updateUI() 21 | 22 | def ClearMessage(self, messageId: int): 23 | record = self.getMessageById(messageId) 24 | if not record: 25 | return 26 | self.messages.remove(record) 27 | if not self.messages: 28 | self.stopTimer() 29 | self.updateUI() 30 | 31 | def getMessageById(self, messageId: int): 32 | for record in self.messages: 33 | if record.id == messageId: 34 | return record 35 | 36 | def AddStaticMessage(self, message: str): 37 | return self.AddMessage(message, temporary=False) 38 | 39 | def Clearmessages(self, par: 'Par'): 40 | self.ClearAllMessages() 41 | 42 | def Addtemporarymessage(self, par: 'Par'): 43 | self.addMessageFromPar(temporary=True) 44 | 45 | def Addstaticmessage(self, par: 'Par'): 46 | self.addMessageFromPar(temporary=False) 47 | 48 | def addMessageFromPar(self, temporary: bool): 49 | message = self.ownerComp.par.Messagetoadd.eval() 50 | print('OMG ADD MESSAGE FROM PAR: ', repr(message)) 51 | if message: 52 | self.AddMessage(message, temporary=temporary) 53 | 54 | @staticmethod 55 | def now(): 56 | return absTime.seconds 57 | 58 | def AddMessage(self, message: str, temporary: bool = True): 59 | if temporary: 60 | record = _Message( 61 | self.nextId, message, self.now() + self.ownerComp.par.Temporaryduration) 62 | else: 63 | record = _Message(self.nextId, message) 64 | self.nextId += 1 65 | self.messages.append(record) 66 | self.updateUI() 67 | if temporary: 68 | # if not self.timer['timer_active']: 69 | self.startTimer() 70 | return record.id 71 | 72 | def startTimer(self): 73 | self.timer.par.start.pulse() 74 | 75 | def stopTimer(self): 76 | self.timer.par.initialize.pulse() 77 | 78 | def updateUI(self): 79 | if not self.messages: 80 | self.labelComp.par.Widgetlabel = '' 81 | self.ownerComp.par.display = False 82 | else: 83 | self.labelComp.par.Widgetlabel = '\n'.join([ 84 | record.text 85 | for record in self.messages 86 | ]) 87 | self.ownerComp.par.display = True 88 | 89 | def onUpdateTrigger(self): 90 | needAnotherUpdate = self.clearExpiredMessages() 91 | self.updateUI() 92 | if needAnotherUpdate: 93 | self.startTimer() 94 | 95 | def clearExpiredMessages(self): 96 | now = self.now() 97 | self.messages = [ 98 | record 99 | for record in self.messages 100 | if record.endTime is None or record.endTime > now 101 | ] 102 | changed = False 103 | needAnotherUpdate = False 104 | filteredMessages = [] 105 | for record in self.messages: 106 | if record.endTime is None: 107 | filteredMessages.append(record) 108 | elif record.endTime > now: 109 | needAnotherUpdate = True 110 | filteredMessages.append(record) 111 | else: 112 | changed = True 113 | if changed: 114 | self.messages = filteredMessages 115 | return needAnotherUpdate 116 | 117 | @dataclass 118 | class _Message: 119 | id: int 120 | text: str 121 | endTime: Optional[float] = None # in seconds 122 | -------------------------------------------------------------------------------- /ui/ui_overrides.txt: -------------------------------------------------------------------------------- 1 | template path parameter value 2 | global_slider * Font 'Roboto' 3 | global_slider * Labelfontbold 0 4 | global_slider * Labelbgcolorr 0.2 5 | global_slider * Labelbgcolorg 0.2 6 | global_slider * Labelbgcolorb 0.2 7 | global_slider * Labelbgcolora 1.0 8 | global_slider * Labelfontcolorr 0.65 9 | global_slider * Labelfontcolorg 0.65 10 | global_slider * Labelfontcolorb 0.65 11 | global_slider * Labelfontcolora 1.0 12 | global_slider * Sliderlabelfontcolorr 0.8 13 | global_slider * Sliderlabelfontcolorg 0.8 14 | global_slider * Sliderlabelfontcolorb 0.8 15 | global_slider * Sliderlabelfontcolora 1.0 16 | global_slider * Sliderbgcolorr 0.15 17 | global_slider * Sliderbgcolorg 0.15 18 | global_slider * Sliderbgcolorb 0.15 19 | global_slider * Sliderbgcolora 1.0 20 | global_slider * Sliderknobcolorr 0.6 21 | global_slider * Sliderknobcolorg 0.6 22 | global_slider * Sliderknobcolorb 0.6 23 | global_slider * Sliderknobcolora 1.0 24 | global_slider * Sliderlabelfontcolorr 0.8 25 | global_slider * Sliderlabelfontcolorg 0.8 26 | global_slider * Sliderlabelfontcolorb 0.8 27 | global_slider * Sliderlabelfontcolora 1.0 28 | global_slider * Sliderindicatorcolorr 0.325 29 | global_slider * Sliderindicatorcolorg 0.325 30 | global_slider * Sliderindicatorcolorb 0.325 31 | global_slider * Sliderindicatorcolora 1.0 32 | button *_button Buttonfont 'Roboto' 33 | buttonicon *_buttonicon Buttonfont 'Material Design Icons' 34 | dropfield *_dropfield Buttonfont 'Material Design Icons' 35 | filefield *_filefield Buttonfont 'Material Design Icons' 36 | radio *_radio Radiobasecolorr 0.0 37 | radio *_radio Radiobasecolorg 0.0 38 | radio *_radio Radiobasecolorb 0.0 39 | radio *_radio Radiobasecolora 0.0 40 | radio *_radio Radioofffacecolorr 0.28 41 | radio *_radio Radioofffacecolorg 0.28 42 | radio *_radio Radioofffacecolorb 0.28 43 | radio *_radio Radioofffacecolora 1.0 44 | radio *_radio Radioonfacecolorr 0.3276 45 | radio *_radio Radioonfacecolorg 0.52 46 | radio *_radio Radioonfacecolorb 0.343633 47 | radio *_radio Radioonfacecolora 1.0 48 | radio *_radio Radioofffontcolorr 0.9 49 | radio *_radio Radioofffontcolorg 0.9 50 | radio *_radio Radioofffontcolorb 0.9 51 | radio *_radio Radioofffontcolora 1.0 52 | radio *_radio Radioonfontcolorr 1.0 53 | radio *_radio Radioonfontcolorg 1.0 54 | radio *_radio Radioonfontcolorb 1.0 55 | radio *_radio Radioonfontcolora 1.0 56 | radio *_radio Radioborderacolorr 1.0 57 | radio *_radio Radioborderacolorg 1.0 58 | radio *_radio Radioborderacolorb 1.0 59 | radio *_radio Radioborderacolora 1.0 60 | radio *_radio Radioborderbcolorr 0.0 61 | radio *_radio Radioborderbcolorg 0.0 62 | radio *_radio Radioborderbcolorb 0.0 63 | radio *_radio Radioborderbcolora 1.0 64 | section *_section Buttonfont 'Material Design Icons' 65 | toggle *_toggle Buttonfont 'Roboto' 66 | toggleicon *_toggleicon Buttonfont 'Material Design Icons' 67 | trigger *_trigger Buttonfont 'Roboto' 68 | global_toggle * Buttonbasecolorr 0.0 69 | global_toggle * Buttonbasecolorg 0.0 70 | global_toggle * Buttonbasecolorb 0.0 71 | global_toggle * Buttonbasecolora 0.0 72 | global_toggle * Buttonofffacecolorr 0.28 73 | global_toggle * Buttonofffacecolorg 0.28 74 | global_toggle * Buttonofffacecolorb 0.28 75 | global_toggle * Buttonofffacecolora 1.0 76 | global_toggle * Buttononfacecolorr 0.3276 77 | global_toggle * Buttononfacecolorg 0.52 78 | global_toggle * Buttononfacecolorb 0.343633 79 | global_toggle * Buttononfacecolora 1.0 80 | global_toggle * Buttonofffontcolorr 0.9 81 | global_toggle * Buttonofffontcolorg 0.9 82 | global_toggle * Buttonofffontcolorb 0.9 83 | global_toggle * Buttonofffontcolora 1.0 84 | global_toggle * Buttononfontcolorr 1.0 85 | global_toggle * Buttononfontcolorg 1.0 86 | global_toggle * Buttononfontcolorb 1.0 87 | global_toggle * Buttononfontcolora 1.0 88 | global_toggle * Buttonbordercolorr 1.0 89 | global_toggle * Buttonbordercolorg 1.0 90 | global_toggle * Buttonbordercolorb 1.0 91 | global_toggle * Buttonbordercolora 1.0 92 | section *_section Buttontype 'Toggle Down' 93 | section *_section Sectionlabelbgcolorr 0.0 94 | section *_section Sectionlabelfontcolorr 0.956863 95 | section *_section Sectionlabelbgcolorg 0.0 96 | section *_section Sectionlabelfontcolorg 1.0 97 | section *_section Sectionlabelbgcolorb 0.0 98 | section *_section Sectionlabelfontcolorb 0.498039 99 | section *_section Sectionlabelbgcolora 0.0 100 | section *_section Sectionlabelfontcolora 0.6 101 | overlay *_overlay Markercolorr 0.3276 102 | overlay *_overlay Markercolorg 0.52 103 | overlay *_overlay Markercolorb 0.343633 104 | overlay *_overlay Markercolora 1.0 105 | list *_list Listerbasecolorr 0.1 106 | list *_list Listerheadercolorr 0.2 107 | list *_list Listerheaderfontcolorr 0.8 108 | list *_list Listeritemscolorr 0.45 109 | list *_list Listerodditemscolorr 0.45 110 | list *_list Listeritemsfontcolorr 0.1 111 | list *_list Listerrowselectcolorr 0.413424 112 | list *_list Listerrowselectfontcolorr 0.0 113 | list *_list Listerrowrollcolorr 0.75 114 | list *_list Listerrowrollfontcolorr 0.1 115 | list *_list Listerbuttonoffcolorr 0.7 116 | list *_list Listerbuttonrollcolorr 0.3276 117 | list *_list Listerbuttononcolorr 0.0 118 | list *_list Listerbuttonofffontcolorr 0.2 119 | list *_list Listerbuttonrollfontcolorr 0.0 120 | list *_list Listerbuttononfontcolorr 0.0 121 | list *_list Listerbasecolorg 0.1 122 | list *_list Listerheadercolorg 0.2 123 | list *_list Listerheaderfontcolorg 0.8 124 | list *_list Listeritemscolorg 0.45 125 | list *_list Listerodditemscolorg 0.45 126 | list *_list Listeritemsfontcolorg 0.1 127 | list *_list Listerrowselectcolorg 0.957 128 | list *_list Listerrowselectfontcolorg 0.1012 129 | list *_list Listerrowrollcolorg 0.75 130 | list *_list Listerrowrollfontcolorg 0.1 131 | list *_list Listerbuttonoffcolorg 0.7 132 | list *_list Listerbuttonrollcolorg 0.52 133 | list *_list Listerbuttononcolorg 0.0 134 | list *_list Listerbuttonofffontcolorg 0.2 135 | list *_list Listerbuttonrollfontcolorg 0.0 136 | list *_list Listerbuttononfontcolorg 0.0 137 | list *_list Listerbasecolorb 0.1 138 | list *_list Listerheadercolorb 0.2 139 | list *_list Listerheaderfontcolorb 0.8 140 | list *_list Listeritemscolorb 0.45 141 | list *_list Listerodditemscolorb 0.45 142 | list *_list Listeritemsfontcolorb 0.1 143 | list *_list Listerrowselectcolorb 0.458721 144 | list *_list Listerrowselectfontcolorb 0.184 145 | list *_list Listerrowrollcolorb 0.75 146 | list *_list Listerrowrollfontcolorb 0.1 147 | list *_list Listerbuttonoffcolorb 0.7 148 | list *_list Listerbuttonrollcolorb 0.343633 149 | list *_list Listerbuttononcolorb 0.0 150 | list *_list Listerbuttonofffontcolorb 0.2 151 | list *_list Listerbuttonrollfontcolorb 0.0 152 | list *_list Listerbuttononfontcolorb 0.0 153 | list *_list Listerbasecolora 1.0 154 | list *_list Listerheadercolora 1.0 155 | list *_list Listerheaderfontcolora 1.0 156 | list *_list Listeritemscolora 1.0 157 | list *_list Listerodditemscolora 1.0 158 | list *_list Listeritemsfontcolora 1.0 159 | list *_list Listerrowselectcolora 0.6 160 | list *_list Listerrowselectfontcolora 1.0 161 | list *_list Listerrowrollcolora 1.0 162 | list *_list Listerrowrollfontcolora 1.0 163 | list *_list Listerbuttonoffcolora 1.0 164 | list *_list Listerbuttonrollcolora 1.0 165 | list *_list Listerbuttononcolora 1.0 166 | list *_list Listerbuttonofffontcolora 1.0 167 | list *_list Listerbuttonrollfontcolora 1.0 168 | list *_list Listerbuttononfontcolora 1.0 169 | topMenu * Menubuttonbgcolorr 0.3 170 | topMenu * Menubuttonbgcolorg 0.3 171 | topMenu * Menubuttonbgcolorb 0.3 172 | topMenu * Menubuttonbgcolora 1.0 173 | topMenu * Menubuttonfontcolorr 0.7 174 | topMenu * Menubuttonfontcolorg 0.7 175 | topMenu * Menubuttonfontcolorb 0.7 176 | topMenu * Menubuttonfontcolora 1.0 177 | topMenu * Menubuttononbgcolorr 0.8 178 | topMenu * Menubuttononbgcolorg 0.8 179 | topMenu * Menubuttononbgcolorb 0.8 180 | topMenu * Menubuttononbgcolora 1.0 181 | topMenu * Menubuttononfontcolorr 0.1 182 | topMenu * Menubuttononfontcolorg 0.1 183 | topMenu * Menubuttononfontcolorb 0.1 184 | topMenu * Menubuttononfontcolora 1.0 185 | topMenu * Menubuttondisablecolorr 0.0 186 | topMenu * Menubuttondisablecolorg 0.0 187 | topMenu * Menubuttondisablecolorb 0.0 188 | topMenu * Menubuttondisablecolora 0.5 189 | topMenu * Menubgcolorr 0.3 190 | topMenu * Menubgcolorg 0.3 191 | topMenu * Menubgcolorb 0.3 192 | topMenu * Menubgcolora 1.0 193 | topMenu * Menufontcolorr 0.9 194 | topMenu * Menufontcolorg 0.9 195 | topMenu * Menufontcolorb 0.9 196 | topMenu * Menufontcolora 1.0 197 | topMenu * Menurollbgcolorr 1.0 198 | topMenu * Menurollbgcolorg 1.0 199 | topMenu * Menurollbgcolorb 1.0 200 | topMenu * Menurollbgcolora 0.1 201 | topMenu * Menurollfontcolorr 0.9 202 | topMenu * Menurollfontcolorg 0.9 203 | topMenu * Menurollfontcolorb 0.9 204 | topMenu * Menurollfontcolora 1.0 205 | topMenu * Menuselectbgcolorr 0.5 206 | topMenu * Menuselectbgcolorg 0.5 207 | topMenu * Menuselectbgcolorb 0.5 208 | topMenu * Menuselectbgcolora 1.0 209 | topMenu * Menuselectfontcolorr 0.9 210 | topMenu * Menuselectfontcolorg 0.9 211 | topMenu * Menuselectfontcolorb 0.9 212 | topMenu * Menuselectfontcolora 1.0 213 | topMenu * Menudisablebgcolorr 0.2 214 | topMenu * Menudisablebgcolorg 0.2 215 | topMenu * Menudisablebgcolorb 0.2 216 | topMenu * Menudisablebgcolora 0.5 217 | topMenu * Menudisablefontcolorr 0.4 218 | topMenu * Menudisablefontcolorg 0.4 219 | topMenu * Menudisablefontcolorb 0.4 220 | topMenu * Menudisablefontcolora 1.0 221 | topMenu * Menuhighlightbgcolorr 0.3 222 | topMenu * Menuhighlightbgcolorg 0.3 223 | topMenu * Menuhighlightbgcolorb 0.6 224 | topMenu * Menuhighlightbgcolora 1.0 225 | topMenu * Menuhighlightfontcolorr 0.9 226 | topMenu * Menuhighlightfontcolorg 0.9 227 | topMenu * Menuhighlightfontcolorb 0.9 228 | topMenu * Menuhighlightfontcolora 1.0 229 | topMenu * Menubordercolorr 0.9 230 | topMenu * Menubordercolorg 0.9 231 | topMenu * Menubordercolorb 0.9 232 | topMenu * Menubordercolora 1.0 233 | topMenu * Menudividercolorr 0.9 234 | topMenu * Menudividercolorg 0.9 235 | topMenu * Menudividercolorb 0.9 236 | topMenu * Menudividercolora 1.0 237 | -------------------------------------------------------------------------------- /ui/widget_ui_overrides.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/ui/widget_ui_overrides.tox -------------------------------------------------------------------------------- /utils/DataLock.py: -------------------------------------------------------------------------------- 1 | def update(scriptOp): 2 | scriptOp.copy(scriptOp.inputs[0]) 3 | 4 | def onCook(scriptOp): 5 | if not parent().par.Locked: 6 | update(scriptOp) 7 | 8 | def onPulse(par): 9 | scriptOp = op('data_lock') 10 | if par.name == 'Update': 11 | update(scriptOp) 12 | elif par.name == 'Clear': 13 | scriptOp.clear() 14 | 15 | onStart = update 16 | onCreate = update 17 | -------------------------------------------------------------------------------- /utils/ParamFilter.py: -------------------------------------------------------------------------------- 1 | # noinspection PyUnreachableCode 2 | if False: 3 | # noinspection PyUnresolvedReferences 4 | from _stubs import * 5 | 6 | def InitHost(): 7 | filterOp = parent() 8 | hostOp = filterOp.par.Parfilterop.eval() 9 | if not hostOp: 10 | return 11 | prefix = filterOp.par.Installedlabelprefix.eval() 12 | page = hostOp.appendCustomPage('Param Filter') 13 | fp = filterOp.par.Parfilterenable 14 | page.appendToggle(fp.name, label=prefix + fp.label) 15 | fp = filterOp.par.Parfiltertype 16 | p = page.appendMenu(fp.name, label=prefix + fp.label)[0] 17 | p.menuNames = fp.menuNames 18 | p.menuLabels = fp.menuLabels 19 | p.default = fp.default 20 | _copyFloatPar(page, filterOp.par.Parfilterwidth, prefix) 21 | _copyFloatPar(page, filterOp.par.Parfilterlag1, prefix) 22 | _copyFloatPar(page, filterOp.par.Parfilterovershoot1, prefix) 23 | 24 | exprPrefix = 'op("{}").par.'.format(filterOp.relativePath(hostOp)) 25 | for p in filterOp.customPages[0].pars: 26 | p.expr = exprPrefix + p.name 27 | 28 | def _copyFloatPar(page: 'Page', src: 'Par', prefix: str): 29 | sourceTuplet = src.tuplet 30 | parTuplet = page.appendFloat(src.tupletName, label=prefix + src.label, size=len(sourceTuplet)) 31 | for i, srcp in enumerate(sourceTuplet): 32 | for a in ( 33 | 'default', 'normMin', 'normMax', 34 | 'clampMin', 'clampMax', 'min', 'max' 35 | ): 36 | setattr(parTuplet[i], a, getattr(srcp, a)) 37 | -------------------------------------------------------------------------------- /utils/channel_remap.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/channel_remap.tox -------------------------------------------------------------------------------- /utils/chop_lock.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/chop_lock.tox -------------------------------------------------------------------------------- /utils/dat_lock.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/dat_lock.tox -------------------------------------------------------------------------------- /utils/lfo_generator.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/lfo_generator.tox -------------------------------------------------------------------------------- /utils/modulated_value.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/modulated_value.tox -------------------------------------------------------------------------------- /utils/param_animator.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/param_animator.tox -------------------------------------------------------------------------------- /utils/param_filter.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/param_filter.tox -------------------------------------------------------------------------------- /utils/settings/SettingsExt.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | # noinspection PyUnreachableCode 4 | if False: 5 | # noinspection PyUnresolvedReferences 6 | from _stubs import * 7 | from typing import Any 8 | iop.hostedComp = COMP() 9 | ipar.compEditor = Any() 10 | from _stubs.TDCallbacksExt import CallbacksExt 11 | ext.Callbacks = CallbacksExt(None) 12 | 13 | class Settings: 14 | def __init__(self, ownerComp): 15 | self.ownerComp = ownerComp # type: COMP 16 | 17 | @staticmethod 18 | def BuildSettings(parameters: 'DAT'): 19 | settings = {} 20 | for i in range(1, parameters.numRows): 21 | if parameters[i, 'readonly'] == '1' or parameters[i, 'enabled'] == '0': 22 | continue 23 | path = parameters[i, 'path'].val 24 | if path not in settings: 25 | settings[path] = {} 26 | name = parameters[i, 'name'].val 27 | mode = parameters[i, 'mode'] 28 | if mode == '': 29 | settings[path][name] = parameters[i, 'value'].val 30 | elif mode == 'expression': 31 | settings[path][name] = {'$': parameters[i, 'expression'].val} 32 | elif mode == 'constant': 33 | settings[path][name] = parameters[i, 'constant'].val 34 | return settings 35 | 36 | def ParseSettings(self, settings: dict): 37 | if not settings: 38 | return 39 | for path, opSettings in settings.items(): 40 | if not opSettings: 41 | continue 42 | o = op(path) 43 | if not o: 44 | continue 45 | for name, val in opSettings.items(): 46 | par = o.par[name] # type: Par 47 | if par is None: 48 | print(f'{self.ownerComp.path}: Warning par not found. path: {path} name: {name}') 49 | continue 50 | if isinstance(val, dict): 51 | if '$' not in val: 52 | print(f'{self.ownerComp.path}: Warning invalid setting. path: {path} name: {name} val: {val!r}') 53 | continue 54 | par.expr = val['$'] 55 | else: 56 | if par.isNumber: 57 | par.val = float(val) 58 | elif par.isToggle: 59 | par.val = val in ['1', 'true', 1, True] 60 | else: 61 | par.val = val 62 | 63 | def OnFileInChange(self, dat: 'DAT'): 64 | if dat.text: 65 | settings = json.loads(dat.text) 66 | else: 67 | settings = {} 68 | self.ParseSettings(settings) 69 | 70 | def Save(self, par): 71 | file = self.ownerComp.par.File.eval() 72 | if not file: 73 | raise Exception('No settings file specified') 74 | self.ownerComp.op('fileout').par.write.pulse() 75 | 76 | def Load(self, par): 77 | fileIn = self.ownerComp.op('filein') 78 | if not fileIn.par.file: 79 | return 80 | fileIn.par.refreshpulse.pulse() 81 | if not self.ownerComp.par.Autoload: 82 | self.OnFileInChange(fileIn) 83 | 84 | def Autoload(self, par): 85 | if par: 86 | self.Load(par) 87 | 88 | def Autosave(self, par): 89 | if par: 90 | self.Save(par) 91 | -------------------------------------------------------------------------------- /utils/settings/settings.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/settings/settings.tox -------------------------------------------------------------------------------- /utils/speed_generator.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/speed_generator.tox -------------------------------------------------------------------------------- /utils/taskManager.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/taskManager.tox -------------------------------------------------------------------------------- /utils/taskManagerExt.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, Callable, List, Optional, Union 2 | 3 | # noinspection PyUnreachableCode 4 | if False: 5 | # noinspection PyUnresolvedReferences 6 | from _stubs import * 7 | 8 | T = TypeVar('T') 9 | 10 | class TaskManager: 11 | def __init__(self): 12 | self.tasks = [] # type: List[Callable] 13 | self.totalTasks = 0 14 | self.batchFutureTasks = 0 15 | 16 | def updateProgress(self): 17 | pass 18 | 19 | def getProgressRatio(self): 20 | if not self.tasks: 21 | return 1 22 | remaining = max(0, len(self.tasks) - self.batchFutureTasks) 23 | total = self.totalTasks 24 | if not total or not remaining: 25 | return 1 26 | return 1 - (remaining / total) 27 | 28 | def getFrameInterval(self): 29 | return 1 30 | 31 | def getDelayRef(self): 32 | return None 33 | 34 | def onTaskSucceeded(self, result): 35 | pass 36 | 37 | def onTaskFailed(self, err): 38 | pass 39 | 40 | def runNextTask(self): 41 | if not self.tasks: 42 | self.ClearTasks() 43 | return 44 | task = self.tasks.pop(0) 45 | result = task() 46 | 47 | def _onSuccess(res): 48 | self.onTaskSucceeded(res) 49 | self.updateProgress() 50 | self.queueNextTask() 51 | 52 | def _onFailure(err): 53 | self.onTaskFailed(err) 54 | # self.ClearTasks() 55 | self.updateProgress() 56 | self.queueNextTask() 57 | 58 | if isinstance(result, Future): 59 | result.then(success=_onSuccess, failure=_onFailure) 60 | else: 61 | _onSuccess(result) 62 | 63 | def queueNextTask(self): 64 | if not self.tasks: 65 | self.ClearTasks() 66 | return 67 | td.run( 68 | 'args[0].runNextTask()', self, 69 | delayFrames=self.getFrameInterval(), 70 | delayRef=self.getDelayRef()) 71 | 72 | def AddTaskBatch(self, tasks: List[Callable], label=None) -> 'Future': 73 | label = f'TaskBatch({label or ""})' 74 | tasks = [task for task in tasks if task] 75 | if not tasks: 76 | return Future.immediate(label=f'{label or ""} (empty batch)') 77 | result = Future(label=label) 78 | self.tasks.extend(tasks) 79 | 80 | # TODO: get rid of this and fix the queue system! 81 | def _noOp(): 82 | # Log('NO-OP for batch: {}'.format(label)) 83 | pass 84 | self.tasks.append(_noOp) 85 | self.batchFutureTasks += 1 86 | 87 | def _finishBatch(): 88 | result.resolve() 89 | 90 | self.tasks.append(_finishBatch) 91 | self.totalTasks += len(tasks) 92 | self.batchFutureTasks += 1 93 | self.queueNextTask() 94 | return result 95 | 96 | def ClearTasks(self): 97 | self.tasks.clear() 98 | self.totalTasks = 0 99 | self.batchFutureTasks = 0 100 | self.updateProgress() 101 | 102 | class Future(Generic[T]): 103 | def __init__(self, onListen=None, onInvoke=None, label=None): 104 | self._successCallback = None # type: Optional[Callable[[Union[T, 'Future']], None]] 105 | self._failureCallback = None # type: Optional[Callable[[Union[T, 'Future']], None]] 106 | self._resolved = False 107 | self._canceled = False 108 | self._result = None # type: Optional[T] 109 | self._error = None 110 | self._onListen = onListen # type: Callable 111 | self._onInvoke = onInvoke # type: Callable 112 | self.label = label 113 | 114 | def then( 115 | self, 116 | success: Callable[[Union[T, 'Future']], None] = None, 117 | failure: Callable[[Union[object, 'Future']], None] = None): 118 | if self._successCallback or self._failureCallback: 119 | raise Exception('Future already has success and/or failure callbacks!') 120 | if self._onListen: 121 | self._onListen() 122 | self._successCallback = success 123 | self._failureCallback = failure 124 | if self._resolved: 125 | self._invoke() 126 | return self 127 | 128 | def _invoke(self): 129 | if self._error is not None: 130 | if self._failureCallback: 131 | self._failureCallback(self._error) 132 | else: 133 | if self._successCallback: 134 | self._successCallback(self._result if self._result is not None else self) 135 | if self._onInvoke: 136 | self._onInvoke() 137 | 138 | def _resolve(self, result: T, error): 139 | if self._canceled: 140 | return 141 | if self._resolved: 142 | raise Exception('Future has already been resolved') 143 | self._resolved = True 144 | self._result = result 145 | self._error = error 146 | # if self._error is not None: 147 | # Log('FUTURE FAILED {}'.format(self)) 148 | # else: 149 | # Log('FUTURE SUCCEEDED {}'.format(self)) 150 | if self._successCallback or self._failureCallback: 151 | self._invoke() 152 | 153 | def resolve(self, result: Optional[T] = None): 154 | self._resolve(result, None) 155 | return self 156 | 157 | def fail(self, error): 158 | self._resolve(None, error or Exception()) 159 | return self 160 | 161 | def cancel(self): 162 | if self._resolved: 163 | raise Exception('Future has already been resolved') 164 | self._canceled = True 165 | 166 | @property 167 | def isResolved(self): 168 | return self._resolved 169 | 170 | @property 171 | def result(self) -> T: 172 | return self._result 173 | 174 | def __str__(self): 175 | if self._canceled: 176 | state = 'canceled' 177 | elif self._resolved: 178 | if self._error is not None: 179 | state = 'failed: {}'.format(self._error) 180 | elif self._result is None: 181 | state = 'succeeded' 182 | else: 183 | state = 'succeeded: {}'.format(self._result) 184 | else: 185 | state = 'pending' 186 | return '{}({}, {})'.format(self.__class__.__name__, self.label or '<>', state) 187 | 188 | @classmethod 189 | def immediate(cls, value: T = None, onListen=None, onInvoke=None, label=None) -> 'Future[T]': 190 | future = cls(onListen=onListen, onInvoke=onInvoke, label=label) 191 | future.resolve(value) 192 | return future 193 | 194 | @classmethod 195 | def immediateError(cls, error, onListen=None, onInvoke=None, label=None): 196 | future = cls(onListen=onListen, onInvoke=onInvoke, label=label) 197 | future.fail(error) 198 | return future 199 | 200 | @classmethod 201 | def of(cls, obj): 202 | if isinstance(obj, Future): 203 | return obj 204 | return cls.immediate(obj) 205 | 206 | @classmethod 207 | def all(cls, *futures: 'Future', onListen=None, onInvoke=None) -> 'Future[List]': 208 | if not futures: 209 | return cls.immediate([], onListen=onListen, onInvoke=onInvoke) 210 | merged = cls(onListen=onListen, onInvoke=onInvoke) 211 | state = { 212 | 'succeeded': 0, 213 | 'failed': 0, 214 | 'results': [None] * len(futures), 215 | 'errors': [None] * len(futures), 216 | } 217 | 218 | def _checkComplete(): 219 | if (state['succeeded'] + state['failed']) < len(futures): 220 | return 221 | if state['failed'] > 0: 222 | merged.fail((state['errors'], state['results'])) 223 | else: 224 | merged.resolve(state['results']) 225 | 226 | def _makeCallbacks(index): 227 | def _resolver(val): 228 | state['results'][index] = val 229 | state['succeeded'] += 1 230 | _checkComplete() 231 | 232 | def _failer(err): 233 | state['errors'][index] = err 234 | state['failed'] += 1 235 | _checkComplete() 236 | 237 | return _resolver, _failer 238 | 239 | for i, f in enumerate(futures): 240 | cls.of(f).then(*_makeCallbacks(i)) 241 | return merged 242 | --------------------------------------------------------------------------------