├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── Changelog.md ├── Common ├── builtin_uniforms.glsl └── macros.glsl ├── Functions └── src │ └── miu_lab │ ├── gen │ └── noise │ │ ├── FBM.glsl │ │ └── fastNoiseLite.glsl │ └── raymarch │ ├── getAO.glsl │ ├── getFog.glsl │ ├── getNormal.glsl │ ├── getSoftShadow.glsl │ ├── rayCast.glsl │ └── structs │ ├── PointLightBasic.glsl │ └── RayHit.glsl ├── LICENSE ├── Pixel_Wrangle.tox ├── Presets └── Factory Library │ ├── _init.json │ ├── filter │ ├── gaussian1D(Fast Static Multipass - Compute ONLY).json │ └── kochFractalSpace.json │ ├── gen │ ├── art │ │ ├── artOfCodeTheDriveHome.json │ │ ├── eyeOfAzkhaban.json │ │ ├── motionLoop.json │ │ └── sdWires3D.json │ ├── noise │ │ ├── FBM_mono.json │ │ └── uberNoise(Slow).json │ ├── raymarch │ │ ├── rm_2D_raycast.json │ │ ├── rm_simple.json │ │ └── sdSphereCut3D.json │ └── shape │ │ ├── artOfCodeBezier.json │ │ ├── sdCircle2D.json │ │ └── sdRoundedBox2D.json │ ├── help │ ├── feedback.json │ ├── readWriteData.json │ ├── useExternalLibs.json │ └── useInputParameters.json │ ├── lygia │ ├── filter │ │ └── blur │ │ │ ├── gaussian1D_20230212.json │ │ │ └── gaussian2D.json │ └── gen │ │ ├── random │ │ └── random.json │ │ └── shape │ │ └── circleSteps.json │ ├── miu_lab │ └── raymarch │ │ └── rm_simple_scene.json │ └── sim │ ├── curlNoiseInstancing.json │ ├── fluidFlood.json │ ├── gameOfLife.json │ ├── simpleFeedback.json │ ├── sutureFluid.json │ ├── sutureFluid_crispRandom.json │ ├── sutureFluid_fluidRandom.json │ └── sutureFluid_roadRandom.json ├── README.md ├── Tox ├── AUTOFOCUS.tox ├── GET_FILE.tox └── KEYBOARD_SHORTCUTS.tox ├── font ├── Fira_Code │ └── FiraCode-SemiBold.ttf └── Space_Grotesk │ └── SpaceGrotesk-Regular.ttf ├── script ├── bash │ └── setup.sh └── py │ ├── export │ └── EXPORTER.py │ ├── function │ └── save.py │ ├── init │ ├── INIT.py │ ├── LOAD_SETTINGS.py │ ├── RESET_KEYS.py │ ├── RESET_PANEL.py │ └── UPDATE_VSCODE_PATH.py │ ├── libs │ └── _stubs │ │ ├── ArcBallExt.pyi │ │ ├── CallbacksExt.pyi │ │ ├── ListerExt.pyi │ │ ├── PopDialogExt.pyi │ │ ├── PopMenuExt.pyi │ │ ├── TDCallbacksExt.pyi │ │ ├── TDCodeGen.pyi │ │ ├── TDFunctions.pyi │ │ ├── TDJSON.pyi │ │ ├── TDStoreTools.pyi │ │ ├── TreeListerExt.pyi │ │ ├── Updater.pyi │ │ └── __init__.pyi │ ├── logs.py │ ├── parm │ ├── bound_glsl_parms.py │ ├── bound_glsl_utils.py │ └── create_parent_parms.py │ ├── parse │ ├── build-libs.py │ ├── common │ │ ├── gl_types.py │ │ └── str_line_utils.py │ ├── get-function-signatures.py │ ├── glsl_code_analyzer │ │ ├── analyzer_code_splitter.py │ │ ├── analyzer_exec.py │ │ ├── analyzer_ext.py │ │ ├── analyzer_line_identifier.py │ │ ├── analyzer_line_matcher.py │ │ ├── analyzer_on_input_change.py │ │ ├── analyzer_on_reset.py │ │ ├── analyzer_regex_patterns.py │ │ ├── analyzer_uniform_parser.py │ │ └── analyzer_utils.py │ ├── glsl_parameter_manager │ │ ├── manager_actions.py │ │ ├── manager_classes.py │ │ ├── manager_exec.py │ │ ├── manager_ext.py │ │ ├── manager_factory.py │ │ ├── manager_on_input_change.py │ │ └── manager_on_target_change.py │ ├── io_parser.py │ └── io_parserExec.py │ ├── preset │ ├── load.py │ └── save.py │ ├── replicator │ ├── feedback.py │ ├── input.py │ ├── output.py │ └── select.py │ ├── storage │ ├── on_code_change.py │ └── on_path_change.py │ ├── tdcomponents_resolve.py │ ├── time │ ├── play-pause.py │ └── timedep.py │ ├── update │ └── on_select_instance.py │ ├── utils │ ├── os │ │ └── open_folder.py │ └── parm │ │ └── utils.py │ └── vscode │ ├── on_destroy.py │ ├── on_open.py │ ├── on_project_save.py │ └── utils.py └── vscode ├── extensions.json ├── keybindings.json └── settings.json /.gitignore: -------------------------------------------------------------------------------- 1 | /settings.json 2 | Functions/dist/**/* 3 | Toe/Backup 4 | venv/**/* 5 | desktop.ini -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Functions/src/lygia"] 2 | path = Functions/src/lygia 3 | url = https://github.com/patriciogonzalezvivo/lygia.git 4 | [submodule "doc"] 5 | path = doc 6 | url = https://github.com/miu-lab/Pixel-Wrangle-doc 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.extraPaths": [ 3 | "./script/py/libs", 4 | "./script/py/parse/common", 5 | "./script/py/parse/glsl_code_analyzer" 6 | ], 7 | } -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.2.0 (Touchdesigner 2023.xx compatible) 4 | 5 | ### BACKWARD COMPATIBILTY 6 | 7 | Due to major changes in Touchdesigner 2023.xx builds, this version does not support old Touchdesigner builds (before 2023.xx), but you can recall all of your presets and functions created in the previous versions of Pixel Wrangle 8 | 9 | ### IMPROVEMENTS 10 | 11 | - Use the new Relative path workflow for TOX components 12 | - Use Text file in sync mode for all external source code files 13 | 14 | ### BUGFIXES 15 | 16 | - Remove old workaround cache nodes due to a bug in 2022 builds that cook each frame inside comp even without time dependency 17 | - Fix script errors due to par name updates in GLSLMultiTOP in 2023 builds 18 | 19 | ### DEPENDENCIES 20 | 21 | - Lygia updated to the current main branch (1.2.0) 22 | 23 | ## v1.1.3-rc2 24 | 25 | ### NEW FEATURES 26 | 27 | - Added Instance manager Panel (browse all Pixel Wrangle exported instances in the project, reload in editor with double click, open parameters, viewers, network) 28 | - Added instance linking (Track currently selected instance and load it automatically in the main UI to edit) 29 | - Added macros allowing to switch between Compute and Vertex/Pixel shader modes on the fly without editing code (see help/readWrite preset for usage) 30 | - Added miu_lab utilities functions for basic raymarching (ray and light structs, shadow, ao, normals, uniform fog) 31 | - Added miu_lab noise functions (FBM, FastNoiseLite) 32 | - Added raymarching presets based on miu_lab utilities functions 33 | - Added pin instance CTRL L - pin parameter/viewer - code CTRL SHIFT L 34 | - Added layout mode in viewer to check all buffers in single viewer 35 | 36 | ### IMPROVEMENTS 37 | 38 | - Global UI improvements (add header bar, auto layout parameter pane, layout mode in viewer panes, pins, etc.) 39 | - Allow single Pixel Wrangle OP manipulate multiple nodes (called 'Instances') 40 | - Added storage in /storage. Pixel Wrangle is now an external tox that reload at startup 41 | - Allow drag and drop previously exported instance in UI to reload it using op storage 42 | - Recalling an instance will keep all parameter values as they are tuned in the instance 43 | - When Pixel Wrangle is linked to an instance : Feed instance inputs as inputs fallback in main UI 44 | - Improved autofocus current OP in parameter pane (contextual edit viewer, node type hints : blue = Pixel Wrangle Master, green = Pixel Wrangle instance, grey= regular OP) 45 | - Edit node name in parameter pane 46 | - Move build lib and export icons in header 47 | - Move resolution, mode, etc. icons on the right 48 | - Updated Macros and presets to use the new write() function macro 49 | - Updated help presets 50 | 51 | ### BUGFIXES 52 | 53 | - Include lygia readme and license in imported libraries 54 | - Fixed pane w/h min size 55 | - Fixed string expression properties in IO tab 56 | - Fixed some sticky keyboard shortcuts 57 | 58 | ## v1.0.2-rc1 59 | 60 | ### NEW FEATURES 61 | 62 | - Compute shaders, 2D array buffers, and 3D are all supported, with 100% functional parity with GLSLMultiTOP. 63 | - All GLSL TOP parameter types are now supported, with an additional "Header" type parameter to aid in parameter layout in the Controls page. 64 | - All parameter properties are available for customizing appearance. 65 | - Ability to save your own functions/snippets to build your custom GLSL library. 66 | - External library auto-parser to convert any arbitrary GLSL library (in the "Functions/dist" folder of the Pixel-Wrangle root) to a TD-compatible GLSL library (correcting paths and old syntaxes). 67 | - One-click to parse and import all external libraries in your project (with static path resolving). 68 | - One-click to export your shader in a new PanelCOMP (removing the Pixel-Wrangle dependency). 69 | - The UI has been completely rebuilt, allowing you to customize the main background color, text size, text font, and text line height. 70 | - Built-in mouse interactions are available everywhere while hovering a viewer and holding the [CTRL] key. 71 | - A variety of macros have been added for common operations. 72 | - Fuzzy search (and path-based) to find relevant functions and presets. 73 | 74 | ### IMPROVEMENTS 75 | 76 | - All Pixel-Wrangle related data has been moved to a USER folder (Documents/Pixel-Wrangle) including vscode environment, cached code panes, saved presets and functions, and macros. 77 | - The function loader now has preview hints (documentation headers and signatures) 78 | - Keyboard shortcuts have been updated 79 | - All parameters have hover-over help when the ALT key is held down 80 | - Most of the UI elements have help message on hovering 81 | - Non-default parameters are now always saved with presets (including resolution, bit depth, shader mode, etc.) 82 | - Integration with Visual Studio Code has been improved and is now more stable 83 | - The official documentation and Lygia are both integrated as submodules, see the installation instructions for more information. 84 | - Crashes are now extremely rare. 85 | 86 | ### KNOWN BUGS 87 | 88 | - Loading presets is still buggy, you may need to load them twice. 89 | - Most keybindings are slightly sticky (but still usable). 90 | - Releasing the [CTRL] key after an interaction in the parameter pane viewer will reset the mouse position to the last main viewer position. 91 | - Navigation in the presets and functions browser with arrow keys does not allow you to press and hold to scroll through the list, and the left arrow key collapses all opened directories (so, it is recommended to use your mouse instead). 92 | - Some cacheTOP in subnet may cook unnecessarily after loading a preset. Toggling the time dependency generally solves the problem. 93 | -------------------------------------------------------------------------------- /Common/builtin_uniforms.glsl: -------------------------------------------------------------------------------- 1 | /* Pixel Wrangle - BUILTIN UNIFORMS */ 2 | 3 | // From TD 4 | uniform vec3 Time; // x : Time in seconds, y : Time in frames, z : 1/FPS 5 | uniform vec4 MousePos; // xy : normalized position (LMB DOWN), zw : normalized position 6 | uniform vec4 MouseClicks; // x : ANY Mouse button pressed, y: LMB pressed, z: MMB pressed, w: RMB pressed 7 | uniform vec4 MouseDeltaLMB; // xy : LMB Position - Current Mouse Position, zw : Relative Position (LMB Down) 8 | uniform vec4 MouseDeltaMMB; // xy : MMB Position - Current Mouse Position, zw : Relative Position (MMB Down) 9 | uniform vec4 MouseDeltaRMB; // xy : RMB Position - Current Mouse Position, zw : Relative Position (RMB Down) 10 | uniform vec2 MouseWheel; // [Mouse Wheel Value, Mouse Wheel Slope] 11 | uniform vec2 MouseSlope; // Mouse Slope 12 | 13 | 14 | // Shadertoy uniforms 15 | uniform vec2 iResolution; // Resolution in pixels 16 | uniform float iTime; // Time in seconds 17 | uniform float iTimeDelta; // 1/FPS 18 | uniform float iFrameRate; // FPS 19 | uniform float iFrame; // Time in frames 20 | uniform vec4 iMouse; // xy : normalized position (LMB DOWN), zw : normalized position -------------------------------------------------------------------------------- /Common/macros.glsl: -------------------------------------------------------------------------------- 1 | /* Pixel Wrangle - FACTORY MACROS */ 2 | 3 | 4 | /* Common Function Macros */ 5 | 6 | #define td(COLOR) TDOutputSwizzle(COLOR) // td(vec4 COLOR) => TDOutputSwizzle(COLOR) => vec4 7 | 8 | #define tx(IN,POS,BIAS) texture(IN, POS, BIAS) // tx(sampler*D IN, vec* POS, float OR vec* OR float[] bias) => texture(IN, POS, BIAS) => float OR vec4 9 | #define fetch(IN,POS,LOD) texelFetch(IN, POS, LOD) // fetch(sampler*D IN, ivec* POS, int LOD) => texelFetch( IN, POS, LOD ) => vec4 10 | 11 | #define store(OUT,POS,VALUE) imageStore(OUT, POS, VALUE) // store(gimage*D OUT, vec* POS, vec* VALUE) => imageStore( OUT, POS, VALUE ) 12 | 13 | 14 | 15 | /* Context Specific Macros */ 16 | 17 | /* IF */ 18 | 19 | #ifdef TD_COMPUTE_SHADER 20 | 21 | #extension GL_NV_compute_shader_derivatives:enable 22 | 23 | /* Shader Mode is 'Compute' */ 24 | #define _MODE true // int _MODE = Return True (The shader mode is 'Compute') 25 | 26 | #define _UV ivec2(gl_GlobalInvocationID.xy) // ivec2 _UV = 2D Position in Pixels /* ivec2(gl_GlobalInvocationID.xy) */ 27 | #define _P ivec3(gl_GlobalInvocationID.xyz) // ivec3 _P = 3D Position in Pixels /* ivec3(gl_GlobalInvocationID.xyz) */ 28 | 29 | #define write(OUT, VALUE) imageStore(OUT, _UV, td(VALUE)) // write(gimage*D OUT, vec* VALUE) => imageStore(OUT, _UV, td(VALUE)) 30 | #define writeP(OUT, POS, VALUE) imageStore(OUT, POS, td(VALUE)) // writeP(gimage*D OUT ,vec* POS, vec* VALUE) => imageStore(OUT, POS, td(VALUE)) 31 | 32 | #define read(IN) texelFetch(IN, _UV, 0) // read(sampler*D IN) => texelFetch(IN, _UV, 0) => vec4 33 | #define readP(IN, POS) texelFetch(IN, POS, 0) // readP(sampler*D IN, vec* POS) => texelFetch(IN, POS, 0) => vec4 34 | #define readLOD(IN, POS, LOD) texelFetch(IN, POS, LOD) // readLOD(sampler*D IN, vec* POS, int LOD) => texelFetch(IN, POS, LOD) => vec4 35 | 36 | #define load(IN) imageLoad(IN, _UV) // load(gimage*D IN) => imageLoad(IN, _UV, 0) => vec4 37 | #define loadP(IN, POS) imageLoad(IN, POS) // loadP(gimage*D IN, ivec* POS) => imageLoad(IN, POS, 0) => vec4 38 | #define loadArray(IN, POS, SAMPLE) imageLoad(IN, POS, SAMPLE) // loadArray(gimage*D IN, vec* POS, int LOD) => imageLoad(IN, POS, LOD) => vec4 39 | 40 | #define o0 mTDComputeOutputs[0] // image*D o0 = First Compute Output (mTDComputeOutputs[0]) 41 | #define o1 mTDComputeOutputs[1] // image*D o1 = Second Compute Output (mTDComputeOutputs[1]) 42 | #define o2 mTDComputeOutputs[2] // image*D o2 = Third Compute Output (mTDComputeOutputs[2]) 43 | #define o3 mTDComputeOutputs[3] // image*D o3 = Fourth Compute Output (mTDComputeOutputs[3]) 44 | #define o4 mTDComputeOutputs[4] // image*D o4 = Fifth Compute Output (mTDComputeOutputs[4]) 45 | #define o5 mTDComputeOutputs[5] // image*D o5 = Sixth Compute Output (mTDComputeOutputs[5]) 46 | #define o6 mTDComputeOutputs[6] // image*D o6 = Seventh Compute Output (mTDComputeOutputs[6]) 47 | #define o7 mTDComputeOutputs[7] // image*D o7 = Eighth Compute Output (mTDComputeOutputs[7]) 48 | 49 | #else 50 | /* ELSE */ 51 | 52 | /* Shader Mode is 'Vertex/Pixel' */ 53 | #define _MODE false // int _MODE = Return False (The shader mode is 'Vertex/Pixel') 54 | 55 | #define _UV ivec2(vUV.st * uTDOutputInfo.res.zw) // ivec2 _UV = 2D Position in Pixels /* ivec2(vUV.st * uTDOutputInfo.res.zw) */ 56 | #define _P ivec3(vec3(vUV.st * uTDOutputInfo.res.zw, uTDOutputInfo.depth.y )) // ivec3 _P = 3D Position in Pixels /* ivec3(vec3(vUV.st * uTDOutputInfo.res.zw, uTDOutputInfo.depth.y )) */ 57 | 58 | #define write(OUT, VALUE) OUT = td(VALUE) // write(OUT, vec* VALUE) => OUT = td(VALUE) 59 | #define writeP(OUT, POS, VALUE) OUT = td(VALUE) // writeP(OUT, vec* POS, vec* VALUE) => OUT = td(VALUE) 60 | 61 | #define read(IN) texture(IN, vUV.st, 0) // read(sampler*D IN) => texture( IN, vUV.st, 0 ) => float OR vec4 62 | #define readP(IN, P) texture(IN, P, 0) // read(sampler*D IN, vec* P) => texture( IN, P, 0 ) => float OR vec4 63 | #define readLOD(IN, P, LOD) texture(IN, P, LOD) // read(sampler*D IN, vec* P, float OR vec* OR float[] LOD) => texture( IN, P, LOD ) => float OR vec4 64 | 65 | #endif 66 | /* END IF */ 67 | 68 | 69 | 70 | /* Common Variable Macros */ 71 | 72 | /* Constants */ 73 | #define _PI 3.1415926535897932384626433832795 // float _PI = PI Value (3.14...) 74 | #define _GOLD 1.618033988749894 // float _GOLD = Golden Ratio (1.618...) 75 | #define _E 2.7182818284590452353602874713527 // float _E = Euler Number 76 | 77 | /* Screen */ 78 | #define _RES vec2(uTDOutputInfo.res.z,uTDOutputInfo.res.w) // vec2 _RES = 2D Resolution in Pixels 79 | #define _PXSIZE vec2(1) / _RES // vec2 _PXSIZE = Size of 1 pixel 1/Resolution 80 | #define _DEPTH uTDOutputInfo.depth.y // float _DEPTH = Depth 81 | #define _RATIO2D vec2(max(_RES.x/_RES.y, 1),max(_RES.y/_RES.x, 1)) // vec2 _RATIO2D = Ratio from min axis (min axis == 1, max axis >= 1) 82 | 83 | /* 2D Position */ 84 | #define _UVN vec2(_UV) / _RES+vec2((min(_RATIO2D, 1)*.5)/_RES) // vec2 _UVN = 2D Normalized Position /* vUV.st */ 85 | #define _UVC _UV - vec2(_RES/2) // vec2 _UVC = 2D Centered Position in Pixels 86 | #define _UVNC (vec2(_UV) / _RES - .5) // vec2 _UVNC = 2D Normalized + Centered Position 87 | #define _UVNR _UVN * _RATIO2D // vec2 _UVNR = 2D Normalized + Ratio Position 88 | #define _UVNCR (vec2(_RATIO2D) * vec2(_UVN - .5)) // vec2 _UVNCR = 2D Normalized + Centered + Ratio Position 89 | 90 | /* 3D Position */ 91 | #define _PN vec3(_UVN,_DEPTH) // vec3 _PN = 3D Normalized Position /* vUV.stp */ 92 | #define _PC _P - vec3(vec2(_RES/2), _DEPTH/2) // vec3 _PC = 3D Centered Position in Pixels 93 | #define _PNC _PN - .5 // vec3 _PNC = 3D Normalized + Centered Position 94 | #define _PNR _PN * vec3(_RATIO2D, _DEPTH/2) // vec3 _PNC = 3D Normalized + Ratio Position 95 | #define _PNCR ( vec3(_RATIO2D, _DEPTH/2) * vec3(_PN.x - .5, _PN.y - .5, 1 ) ) // vec3 _PNCR = 3D Normalized + Centered + Ratio Position 96 | 97 | /* Inputs Samplers Arrays */ 98 | #define i2D sTD2DInputs // sampler2D i2D[] = Array of 2D Inputs Sampler /* sTD2DInputs */ 99 | #define i3D sTD3DInputs // sampler3D i3D[] = Array of 3D Inputs Sampler /* sTD3DInputs */ 100 | #define iCube sTDCubeInputs // samplerCube iCube[] = Array of Cube Inputs Sampler /* sTDCubeInputs */ 101 | #define iArray sTD2DArrayInputs // sampler2DArray iArray[] = Array of Cube Inputs Sampler /* sTD2DArrayInputs */ 102 | 103 | /* 2D Inputs shortcuts */ 104 | #define _i0 sTD2DInputs[0] // sampler2D i0 = First 2D Input Channel /* sTD2DInputs[0] */ 105 | #define _i1 sTD2DInputs[1] // sampler2D i1 = Second 2D Input Channel /* sTD2DInputs[1] */ 106 | #define _i2 sTD2DInputs[2] // sampler2D i2 = Third 2D Input Channel /* sTD2DInputs[2] */ 107 | #define _i3 sTD2DInputs[3] // sampler2D i3 = Fourth 2D Input Channel /* sTD2DInputs[3] */ 108 | #define _i4 sTD2DInputs[4] // sampler2D i4 = Fifth 2D Input Channel /* sTD2DInputs[4] */ 109 | #define _i5 sTD2DInputs[5] // sampler2D i5 = Sixth 2D Input Channel /* sTD2DInputs[5] */ 110 | #define _i6 sTD2DInputs[6] // sampler2D i6 = Seventh 2D Input Channel /* sTD2DInputs[6] */ 111 | #define _i7 sTD2DInputs[7] // sampler2D i7 = Eighth 2D Input Channel /* sTD2DInputs[7] */ 112 | 113 | /* Shadertoy Mapping */ 114 | #define fragCoord _UV // vec2 fragCoord = 2D Position in Pixels 115 | 116 | #define iChannel0 _i0 // sampler2D iChannel0 = First Input Channel /* sTD2DInputs[0] */ 117 | #define iChannel1 _i1 // sampler2D iChannel1 = Second Input Channel /* sTD2DInputs[1] */ 118 | #define iChannel2 _i2 // sampler2D iChannel2 = Third Channel /* sTD2DInputs[2] */ 119 | #define iChannel3 _i3 // sampler2D iChannel3 = Fourth Channel /* sTD2DInputs[3] */ 120 | -------------------------------------------------------------------------------- /Functions/src/miu_lab/gen/noise/FBM.glsl: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | original_author: Patricio Gonzalez Vivo 4 | 5 | description: Generate a Simplex Noise FBM 6 | 7 | use: 8 | - float sFBM(vec3 p, int octaves, float lacunarity, float gain) 9 | - vec3 sFBMColor(vec3 p, int octaves, float lacunarity, float gain) 10 | 11 | */ 12 | 13 | #include "/libs/lygia/generative/snoise_glsl" 14 | 15 | float sFBM(vec3 p, int octaves, float lacunarity, float gain) { 16 | 17 | // Initial values 18 | float val = snoise(p); 19 | float amp = 0.5; 20 | 21 | // Loop 22 | for(int i = 0; i < octaves; i++) { 23 | val += amp * snoise(p); 24 | p *= lacunarity; 25 | amp *= gain; 26 | } 27 | return val; 28 | 29 | } 30 | 31 | vec3 sFBMColor(vec3 p, int octaves, float lacunarity, float gain) { 32 | 33 | // Initial values 34 | vec3 val = snoise3(p); 35 | float amp = 0.5; 36 | 37 | // Loop 38 | for(int i = 0; i < octaves; i++) { 39 | val += amp * snoise3(p); 40 | p *= lacunarity; 41 | amp *= gain; 42 | } 43 | return val; 44 | 45 | } -------------------------------------------------------------------------------- /Functions/src/miu_lab/raymarch/getAO.glsl: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | original_author: Inigo Quiles 4 | 5 | description: Compute AO from Position and Normals 6 | 7 | use: 8 | - float raymarchAO( vec3 Position, vec3 Normal, int samples ) 9 | 10 | */ 11 | 12 | #ifndef MAP_FNC 13 | #define MAP_FNC(Position) getDistance(Position) 14 | #endif 15 | 16 | float getAO(in vec3 pos, in vec3 nor, int samples) { 17 | float occ = 0.0; 18 | float sca = 1.0; 19 | for(int i = 0; i < samples; i++) { 20 | float hr = 0.01 + 0.12 * float(i) * 0.2; 21 | float dd = MAP_FNC(hr * nor + pos); 22 | occ += (hr - dd) * sca; 23 | sca *= 0.9; 24 | if(occ > 0.35) 25 | break; 26 | } 27 | return saturate(1.0 - 3.0 * occ) * (0.5 + 0.5 * nor.y); 28 | } -------------------------------------------------------------------------------- /Functions/src/miu_lab/raymarch/getFog.glsl: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | original_author: Inigo Quiles 4 | 5 | description: Generate Uniform Fog from Color and SDF 6 | 7 | use: 8 | - float getFog( vec3 inputColor, float distance, vec3 fogColor, float force) 9 | 10 | */ 11 | vec3 getFog( vec3 inputColor, // Input color 12 | float distance, // Distance from camera 13 | vec3 fogColor, // Fog Color 14 | float force) 15 | { 16 | float fogAmount = 1 - exp( -distance*force ); 17 | return mix( inputColor, fogColor, fogAmount ); 18 | } -------------------------------------------------------------------------------- /Functions/src/miu_lab/raymarch/getNormal.glsl: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | original_author: miu-lab 4 | 5 | description: This function retrieves surface normals from a given position. 6 | You can also use a vec2 offset to adjust screen space sampling precision. Default is 8/Resolution 7 | It returns a vec3 that contains normals data. 8 | 9 | use: 10 | - getNormal(vec3 p, float d) 11 | - getNormal(vec3 p) 12 | 13 | options: 14 | - NSAMPLE_DIST 32/_RES 15 | */ 16 | 17 | #ifndef NSAMPLE_DIST 18 | #define NSAMPLE_DIST min(32/_RES.x, 32/_RES.y) 19 | #endif 20 | 21 | vec3 getNormal(vec3 position, float d) { 22 | 23 | vec2 k = vec2(1.0, -1.0); // Switch Vector 24 | 25 | return normalize( k.xyy * MAP_FNC( position + k.xyy * d) + 26 | k.yyx * MAP_FNC( position + k.yyx * d) + 27 | k.yxy * MAP_FNC( position + k.yxy * d) + 28 | k.xxx * MAP_FNC( position + k.xxx * d) ); 29 | } 30 | 31 | vec3 getNormal(vec3 position) { 32 | float offset = NSAMPLE_DIST; 33 | return getNormal(position, offset); 34 | } -------------------------------------------------------------------------------- /Functions/src/miu_lab/raymarch/getSoftShadow.glsl: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | original_author: Inigo Quiles 4 | 5 | description: calculate soft shadows http://iquilezles.org/www/articles/rmshadows/rmshadows.htm 6 | 7 | use: float raymarchSoftshadow( vec3 ro, vec3 rd, float tmin, float tmax ) 8 | 9 | */ 10 | 11 | #ifndef MAP_FNC 12 | #define MAP_FNC(Position) getDistance(Position) 13 | #endif 14 | 15 | float getSoftShadow( vec3 ro, vec3 rd, in float tmin, in float tmax, float k, float q ) { 16 | float res = 1.0; 17 | float t = tmin; 18 | float ph = 1e20; 19 | for (int i = 0; i < 3000; i++) { 20 | float h = MAP_FNC(ro + rd * t); 21 | 22 | if (t > tmax) 23 | break; 24 | 25 | else if (h < 0.001) { 26 | res = 0.0; 27 | break; 28 | } 29 | 30 | float y = h*h/(2.0*ph); 31 | float d = sqrt(h*h-y*y); 32 | res = min( res, k*d/max(0.0,t-y) ); 33 | ph = h; 34 | t += h * 1/q; 35 | } 36 | return res; 37 | } -------------------------------------------------------------------------------- /Functions/src/miu_lab/raymarch/rayCast.glsl: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | original_author: miu-lab 4 | 5 | 6 | description: This function cast rays using standard raymarching algorithm based on a pre-declared map function. 7 | 8 | It will return a RenderData struct that contains : 9 | 10 | - Position (vec3 RenderData.position) 11 | - Normal (vec3 RenderData.normal) 12 | - Signed Distance Field (float RenderData.distance) 13 | 14 | Before usage, you have to declare your map function that contains your SDF stuff above this import. 15 | 16 | The default attempted form is : 17 | 18 | float getDistance(vec3 pos){ 19 | // Your SDF Functions here... 20 | 21 | return d; // return your resulting SDF 22 | } 23 | 24 | Use MAP_FNC to override function name 25 | 26 | note: Raymarching algorithm principles, a great interactive example => https://www.shadertoy.com/view/4dKyRz 27 | 28 | use: 29 | RenderData render = rayCast( vec3 ro, 30 | vec3 rd, 31 | float tmin, 32 | float tmax, 33 | float hitPrecision, 34 | int maxIterations, 35 | float quality ) 36 | 37 | options: 38 | 39 | - MAP_FNC(Position) getDistance(POS) 40 | - NORMAL_FNC(Position) getNormal(POS) 41 | 42 | */ 43 | 44 | #include "structs/RayHit.glsl" 45 | #include "getNormal.glsl" 46 | 47 | #ifndef MAP_FNC 48 | #define MAP_FNC(Position) getDistance(Position) 49 | #endif 50 | 51 | #ifndef NORMAL_FNC 52 | #define NORMAL_FNC(Position, Sample_Dist) getNormal(P, Sample_Dist) 53 | #endif 54 | 55 | RayHit rayCast(vec3 eye, vec3 direction, float minDistance, float maxDistance, float hitDistance, int maxSteps, float Quality, float normalSmooth) { 56 | 57 | // Init Position and Normals 58 | vec3 position = vec3(0.0); 59 | vec3 normal = vec3(0.0); 60 | float distance = 0.0; 61 | 62 | // Start marching at the minimum distance 63 | float distanceFromOrigin = minDistance; 64 | 65 | // For each marching step 66 | for(int i = 0; i < maxSteps; i++) { 67 | 68 | // Get the current position 69 | vec3 currentPosition = eye + direction * distanceFromOrigin; 70 | 71 | // Get the distance to the closest surface from the current position and adjust step size by quality factor 72 | float distanceToClosestSurface = MAP_FNC(currentPosition) * 1 / Quality; 73 | 74 | // March 1 step further using the distance to the closest surface 75 | distanceFromOrigin += distanceToClosestSurface; 76 | 77 | /* 78 | If the distance from the origin is greater than the maximum distance 79 | OR 80 | The distance to the closest surface is less than the hit distance 81 | */ 82 | 83 | // Write Position and Normal 84 | if(distanceToClosestSurface < hitDistance * .001) { 85 | position = currentPosition; 86 | normal = getNormal(position, min(1/_RES.x, 1/_RES.y) * normalSmooth); 87 | break; 88 | } 89 | 90 | // Set SDF default empty value 91 | else if(distanceFromOrigin > maxDistance) { 92 | distanceFromOrigin = maxDistance; 93 | break; 94 | } 95 | } 96 | 97 | // Return the distance from the origin 98 | return RayHit(position, normal, distanceFromOrigin); 99 | } 100 | -------------------------------------------------------------------------------- /Functions/src/miu_lab/raymarch/structs/PointLightBasic.glsl: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | original_author: miu-lab 4 | 5 | description: This struct defines the output structure of a Pixel Wrangle Basic PointLight model 6 | 7 | It contains : 8 | 9 | - Position (vec3 PointLightBasic.position) 10 | - Intensity (float PointLightBasic.intensity) 11 | - Diffuse Contribution (float PointLightBasic.diffContrib) 12 | - Specular Contribution (float PointLightBasic.specContrib) 13 | 14 | */ 15 | 16 | struct PointLightBasic 17 | { 18 | vec3 position; 19 | float intensity; 20 | float diffContrib; 21 | float specContrib; 22 | }; -------------------------------------------------------------------------------- /Functions/src/miu_lab/raymarch/structs/RayHit.glsl: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | original_author: miu-lab 4 | 5 | description: This struct defines the output structure of the Pixel Wrangle default raymarcher ( the rayCast() function ) 6 | 7 | It contains : 8 | 9 | - Position (vec3 Hit.position) 10 | - Normal (vec3 Hit.normal) 11 | - Signed Distance Field (float Hit.distance) 12 | 13 | */ 14 | 15 | struct RayHit { 16 | vec3 position; 17 | vec3 normal; 18 | float distance; 19 | }; -------------------------------------------------------------------------------- /Pixel_Wrangle.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miu-lab/Pixel-Wrangle/9e2bcf66596f5e4e74086ada198be9b125267c8b/Pixel_Wrangle.tox -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://user-images.githubusercontent.com/97438154/226935988-b20ef615-6295-4836-828b-64a16d27c9f7.png) 2 | 3 | # Pixel Wrangle 4 | 5 | > Pixel Wrangle is a minimalist GLSL framework built on top of GLSL TOP. 6 | > This is delivered as .tox component that allows you to quick prototyping GLSL in Touchdesigner. 7 | 8 | ## **Philosophy and main features** 9 | 10 | - Functional parity with GLSL TOP 11 | - A user interface that is as simple, pleasant, and straightforward as possible 12 | - Dynamically generated TouchDesigner parameters by declaring uniforms (with customizable properties) 13 | - Minimal CPU overhead 14 | - Easy use, creation, and management of your own libraries of presets and functions 15 | - One-click auto-parsing and importing of third-party libraries (Lygia is fully integrated) 16 | - One-click export of your shader as a standard GLSL network, eliminating framework dependency (called 'Instance') 17 | 18 | This project is heavily inspired by the VEX Wranglers in the Houdini ecosystem. 19 | 20 | You can find the official "work-in-progress" documentation at the following [link](https://miu-lab.github.io/Pixel-Wrangle-doc) (available in both English and French) 21 | 22 | # Changelog 23 | 24 | ## v1.2.0 (Touchdesigner 2023.xx compatible) 25 | 26 | ### BACKWARD COMPATIBILTY 27 | 28 | Due to major changes in Touchdesigner 2023.xx builds, this version does not support old Touchdesigner builds (before 2023.xx), but you can recall all of your presets and functions created in the previous versions of Pixel Wrangle 29 | 30 | ### IMPROVEMENTS 31 | 32 | - Use the new Relative path workflow for TOX components 33 | - Use Text file in sync mode for all external source code files 34 | 35 | ### BUGFIXES 36 | 37 | - Remove old workaround cache nodes due to a bug in 2022 builds that cook each frame inside comp even without time dependency 38 | - Fix script errors due to par name updates in GLSLMultiTOP in 2023 builds 39 | 40 | ### DEPENDENCIES 41 | 42 | - Lygia updated to the current main branch (1.2.0) 43 | 44 | ## **Quick Install** 45 | 46 | ### Requirements 47 | 48 | - [TouchDesigner 2023.11340 or later](https://derivative.ca/download) 49 | - [Git](https://git-scm.com/downloads) 50 | 51 | ### Installation 52 | 53 | #### Manual 54 | 55 | - Use Git to clone the repository with all its submodules at the root of your Palette folder located in ocuments/Derivative 56 | 57 | ```bash 58 | git clone --recurse-submodules https://github.com/miu-lab/Pixel-Wrangle.git 59 | ``` 60 | 61 | - Update your Palette 62 | 63 | #### Automatic 64 | 65 | - Download and execute setup.sh (with Git Bash) provided in the [Release section](https://github.com/miu-lab/Pixel-Wrangle/releases) 66 | - Update your Palette 67 | -------------------------------------------------------------------------------- /Tox/AUTOFOCUS.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miu-lab/Pixel-Wrangle/9e2bcf66596f5e4e74086ada198be9b125267c8b/Tox/AUTOFOCUS.tox -------------------------------------------------------------------------------- /Tox/GET_FILE.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miu-lab/Pixel-Wrangle/9e2bcf66596f5e4e74086ada198be9b125267c8b/Tox/GET_FILE.tox -------------------------------------------------------------------------------- /Tox/KEYBOARD_SHORTCUTS.tox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miu-lab/Pixel-Wrangle/9e2bcf66596f5e4e74086ada198be9b125267c8b/Tox/KEYBOARD_SHORTCUTS.tox -------------------------------------------------------------------------------- /font/Fira_Code/FiraCode-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miu-lab/Pixel-Wrangle/9e2bcf66596f5e4e74086ada198be9b125267c8b/font/Fira_Code/FiraCode-SemiBold.ttf -------------------------------------------------------------------------------- /font/Space_Grotesk/SpaceGrotesk-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miu-lab/Pixel-Wrangle/9e2bcf66596f5e4e74086ada198be9b125267c8b/font/Space_Grotesk/SpaceGrotesk-Regular.ttf -------------------------------------------------------------------------------- /script/bash/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Define the target directory 4 | TARGET_DIR="$HOME/Documents/Derivative/Palette/Pixel-Wrangle" 5 | 6 | # Define the repository URL 7 | REPO_URL="https://github.com/miu-lab/Pixel-Wrangle.git" 8 | 9 | # Check if the target directory exists 10 | if [ -d "$TARGET_DIR" ]; then 11 | # Get the list of branches from the repository 12 | branches=($(git ls-remote --heads ${REPO_URL} | cut -d "/" -f 3)) 13 | 14 | # Prompt the user to select the branch 15 | PS3="Select the branch you want to use: " 16 | select branch in "${branches[@]}"; do 17 | if [[ -n $branch ]]; then 18 | git -C $TARGET_DIR pull origin $branch 19 | break 20 | else 21 | echo "Invalid selection" 22 | fi 23 | done 24 | else 25 | # Get the list of branches from the repository 26 | branches=($(git ls-remote --heads ${REPO_URL} | cut -d "/" -f 3)) 27 | 28 | # Prompt the user to select the branch 29 | PS3="Select the branch you want to use: " 30 | select branch in "${branches[@]}"; do 31 | if [[ -n $branch ]]; then 32 | git clone --progress --recursive -b $branch ${REPO_URL} ${TARGET_DIR} 33 | break 34 | else 35 | echo "Invalid selection" 36 | fi 37 | done 38 | fi 39 | echo " 40 | 41 | ============= SETUP IS COMPLETE ============= 42 | 43 | Launch Touchdesigner and update your Palette 44 | with right-click on My Components -> Refresh 45 | 46 | " 47 | read -n1 -r -p "Press any key to close this window..." 48 | exit -------------------------------------------------------------------------------- /script/py/export/EXPORTER.py: -------------------------------------------------------------------------------- 1 | from TDJSON import ( 2 | pageToJSONDict, 3 | addParametersFromJSONDict, 4 | parameterToJSONPar, 5 | addParametersFromJSONList, 6 | destroyOtherPagesAndParameters, 7 | ) 8 | from TDFunctions import getCustomPage 9 | from PRESET_UTILS import buildPreset 10 | 11 | 12 | def getSelectedInstance(parent=parent.Comp): 13 | tarn = op(parent.op("UI/HEADER/GET_SELECTED_OP")[0, 0].val) 14 | if tarn != None and "preset" in tarn.storage: 15 | return op(parent.op("UI/HEADER/GET_SELECTED_OP")[0, 0].val) 16 | else: 17 | return None 18 | 19 | 20 | def setContainer(src=parent.Comp, name=f"{parent.Comp.name}_export"): 21 | container = getSelectedInstance() 22 | # If container does not exist, create it 23 | if container == None: 24 | container = src.parent().create(containerCOMP, name) 25 | # Set container position, parent shortcut, color from source 26 | container.par.parentshortcut = "Comp" 27 | container.nodeY = src.nodeY - 200 28 | container.nodeX = src.nodeX 29 | container.color = tuple([x * 0.75 for x in list(src.color)]) 30 | 31 | # else reset container 32 | else: 33 | childs = container.ops("*") 34 | destroyOtherPagesAndParameters(container, [], []) 35 | for op in childs: 36 | if op.valid: 37 | op.destroy() 38 | 39 | # Get source Pages 40 | controlsPage = getCustomPage(src, "Controls") 41 | glslPage = getCustomPage(src, "GLSL") 42 | globalsPage = getCustomPage(src, "Globals") 43 | inputsPage = getCustomPage(src, "Inputs") 44 | 45 | # Inputs Page parameters to keep 46 | inputsFilteredPage = [] 47 | pFilterList = [ 48 | "Inputstimeh", 49 | "Inputsplay", 50 | "Inputstimedep", 51 | "Inputspanelh", 52 | "Inputspanelsource", 53 | "Inputspanel", 54 | "Inputssmooth", 55 | "Inputssmoothlag", 56 | "Inputsglobalh", 57 | "Inputsresetall", 58 | ] 59 | 60 | for p in inputsPage: 61 | if p.name in pFilterList: 62 | inputsFilteredPage.append( 63 | parameterToJSONPar(p, extraAttrs="*", forceAttrLists=True)) 64 | 65 | # Set nodename patterns to copy 66 | nodeNamesToCopy = ( 67 | "renderselect", 68 | "selectInputsactive", 69 | "Inputsactive", 70 | "feedbackInputsactive", 71 | "output", 72 | "local", 73 | # "commenth", 74 | "glsl1", 75 | "BUILTINS_INIT", 76 | "MOUSE", 77 | "PANEL", 78 | ) 79 | 80 | nodesToCopy = [] 81 | nodesToUpdate = [] 82 | activeOutputs = [] 83 | # Get corresponding nodes 84 | for node in src.children: 85 | if node.name.startswith(nodeNamesToCopy): 86 | if container.op(node.name) == None: 87 | nodesToCopy.append(node) 88 | elif node.OPType == "glslmultiTOP" and container.op(node.name): 89 | container.op(node.name).destroy() 90 | nodesToCopy.append(node) 91 | else: 92 | nodesToUpdate.append(container.op(node.name)) 93 | if node.name.startswith("output"): 94 | activeOutputs.append(node.digits) 95 | 96 | # Batch Copy node in the new container 97 | nodes = container.copyOPs(nodesToCopy) + nodesToUpdate 98 | toDestroyOuts = [] 99 | # Set init parameters for the new nodes 100 | for node in nodes: 101 | if node.name.startswith(nodeNamesToCopy): 102 | n = node 103 | if n.name.startswith("feedbackInputsactive"): 104 | n.par.reset.bindExpr = "parent.Comp.par.Inputsresetall" 105 | n.par.resetpulse.bindExpr = "parent.Comp.par.Inputsresetall" 106 | if n.type in ["in", "out"]: 107 | n.par.label.mode = ParMode.CONSTANT 108 | label = src.op(node.name).par.label.eval() 109 | n.par.label.val = label 110 | if n.name in ["local", "PANEL", "MOUSE"]: 111 | n.nodeY = 1800 112 | if n.name in ["BUILTINS_INIT"]: 113 | n.nodeY = 2000 114 | if (n.name.startswith(("renderselect", "cache")) 115 | and n.digits not in activeOutputs): 116 | toDestroyOuts.append(n) 117 | 118 | # Merge Pages 119 | pages = [controlsPage, glslPage, globalsPage] 120 | 121 | # Destroy unnecessary Outputs 122 | for n in toDestroyOuts: 123 | n.destroy() 124 | 125 | # Get GLSL OP 126 | glsln = container.op("glsl1") 127 | glslCodeOPName = "GLSL_PIXEL" 128 | # Copy GLSL Full Code 129 | glslPixelCodeText = src.op("BUILD_GLSL_CODE/OUT_GLSL_COMBINED").text 130 | 131 | try: 132 | glslPixelCoden = container.op(glslCodeOPName) 133 | glslPixelCoden.text = glslPixelCodeText 134 | except: 135 | glslPixelCoden = container.create(textDAT) 136 | glslPixelCoden.name = "GLSL_PIXEL" 137 | glslPixelCoden.par.language = "glsl" 138 | glslPixelCoden.text = glslPixelCodeText 139 | 140 | # Place GLSL CODE OP 141 | glslPixelCoden.nodeX = glsln.nodeX 142 | glslPixelCoden.nodeY = glsln.nodeY - 200 143 | 144 | # Set GLSL CODE PATH and Buffer numbers according to source 145 | glsln.par.pixeldat = glslPixelCoden.name 146 | glsln.par.computedat = glslPixelCoden.name 147 | glsln.par.numcolorbufs.mode = ParMode.CONSTANT 148 | glsln.par.numcolorbufs = src.op("glsl1").par.numcolorbufs.eval() 149 | 150 | # Copy Macros and builtins 151 | if container.op("FACTORY_MACROS") != None: 152 | macroFactoryOP = container.op("FACTORY_MACROS") 153 | else: 154 | macroFactorySrc = src.op("BUILD_GLSL_CODE/FACTORY_MACROS") 155 | macroFactoryOP = container.copy(macroFactorySrc) 156 | macroFactoryOP.par.syncfile = False 157 | macroFactoryOP.par.file = "" 158 | macroFactoryOP.nodeX = glsln.nodeX - 200 159 | macroFactoryOP.nodeY = glsln.nodeY - 100 160 | 161 | if container.op("USER_MACROS") != None: 162 | macroUserSrc = src.op("BUILD_GLSL_CODE/USER_MACROS") 163 | macroUserOP = container.op("USER_MACROS") 164 | macroUserOP.par.file = macroUserSrc.par.file.eval() 165 | macroUserOP.par.syncfile = False 166 | macroUserOP.par.file = "" 167 | else: 168 | macroUserSrc = src.op("BUILD_GLSL_CODE/USER_MACROS") 169 | macroUserOP = container.copy(macroUserSrc) 170 | macroUserOP.nodeX = glsln.nodeX - 200 171 | macroUserOP.nodeY = glsln.nodeY - 200 172 | macroUserOP.par.file = macroUserSrc.par.file.eval() 173 | macroUserOP.par.syncfile = False 174 | macroUserOP.par.file = "" 175 | if container.op("BUILTIN_UNIFORMS") != None: 176 | builtinUniformsSrc = container.op("BUILTIN_UNIFORMS") 177 | else: 178 | builtinUniformsSrc = src.op("BUILD_GLSL_CODE/BUILTIN_UNIFORMS") 179 | builtinUniformOP = container.copy(builtinUniformsSrc) 180 | builtinUniformOP.nodeX = glsln.nodeX - 200 181 | builtinUniformOP.nodeY = glsln.nodeY - 300 182 | 183 | # Set Panel references 184 | panel = container.op("PANEL") 185 | panel.par.component.expr = "parent() if op('..').par.Inputspanelsource == 'internal' else parent().par.Inputspanel" 186 | 187 | # Set Mouse reference from parent panel 188 | container.op("MOUSE/VIEWER_MOUSE").par.panels.expr = "parent.Comp" 189 | 190 | # Set First output as Background TOP 191 | container.par.top = container.op("output1") 192 | 193 | # Set container resolution according to GLSL TOP Resolution 194 | container.par.w.expr = "op('./GET_INFOS/RES')['resx']" 195 | container.par.h.expr = "op('./GET_INFOS/RES')['resy']" 196 | container.par.w.readOnly = True 197 | container.par.h.readOnly = True 198 | container.par.top = container.op("output1") 199 | container.par.opshortcut.expr = 'f"pwexport_{me.id}"' 200 | container.par.opshortcut.readOnly = True 201 | 202 | # Set Viewer on parent container 203 | container.viewer = True 204 | 205 | # Set Parameter pages on parent 206 | for page in pages: 207 | addParametersFromJSONDict( 208 | container, pageToJSONDict(page, 209 | extraAttrs="*", 210 | forceAttrLists=True)) 211 | addParametersFromJSONList(container, inputsFilteredPage) 212 | 213 | # # Sort pages 214 | if len(controlsPage.pars) > 0: 215 | container.sortCustomPages("Controls", "Inputs", "GLSL", "Globals") 216 | else: 217 | container.sortCustomPages("Inputs", "GLSL", "Globals") 218 | 219 | # Update Storage 220 | container.store("preset", {"lastExportState": buildPreset(src)}) 221 | return 222 | 223 | 224 | def onPulse(par): 225 | # Source OP 226 | src = parent.Comp 227 | parent.Comp.op("UI/HEADER/pop_set_name").Open() 228 | setContainer(src) 229 | return 230 | 231 | 232 | def onSelect(info): 233 | src = parent.Comp 234 | if info["button"] == "OK": 235 | name = info["enteredText"].replace(" ", "_") 236 | setContainer(src, name) 237 | 238 | 239 | def onOpen(info): 240 | src = parent.Comp 241 | if getSelectedInstance() != None: 242 | parent.Comp.op("pop_set_name").Close() 243 | setContainer(src) 244 | else: 245 | pass 246 | -------------------------------------------------------------------------------- /script/py/function/save.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | 3 | from pathlib import Path, PurePosixPath 4 | 5 | 6 | def onSelect(info): 7 | if info["button"] != "Save": 8 | return 9 | name = str(PurePosixPath(info["enteredText"]).name) 10 | path = parent.Comp.par.Codeuserfunctionpath 11 | relpath = "" 12 | 13 | with contextlib.suppress(Exception): 14 | relpath = op("SAVE_TO_PATH")[1, "relpath"].val 15 | path = f"{path}/{relpath}" 16 | if "/" in str(PurePosixPath(info["enteredText"])): 17 | folder = PurePosixPath(f"{path}/{PurePosixPath(info['enteredText'])}").parent 18 | Path(str(folder)).mkdir(parents=True, exist_ok=True) 19 | path = folder 20 | 21 | with open(f"{path}/{name}.glsl", "w") as f: 22 | f.write(op("FUNCTION_OUT").text) 23 | -------------------------------------------------------------------------------- /script/py/init/INIT.py: -------------------------------------------------------------------------------- 1 | from LOAD_SETTINGS import * 2 | from pathlib import Path 3 | from PRESET_UTILS import buildPreset 4 | 5 | import os 6 | 7 | sourcesn = [iop.uniform, iop.outputs, iop.code, iop.function] 8 | 9 | glCodeContainer = op(f"{parent.Comp.path}/BUILD_GLSL_CODE") 10 | glPath = glCodeContainer.path 11 | opid = parent.Comp.id 12 | 13 | 14 | def initStorage(): 15 | try: 16 | op.pwstorage.valid 17 | except Exception: 18 | parent = op("/").create(baseCOMP, "storage") 19 | tarn = parent.create(baseCOMP, "Pixel_Wrangle") 20 | tarn.par.opshortcut = "pwstorage" 21 | 22 | 23 | def initStorageOP(comp=parent.Comp): 24 | lastState = buildPreset(comp) 25 | op.pwstorage.store( 26 | comp.path, 27 | { 28 | "opid": comp.id, 29 | "lastState": lastState, 30 | "connections": {"inputs": [], "outputs": []}, 31 | }, 32 | ) 33 | 34 | 35 | def savePresetInStorage(comp=parent.Comp): 36 | opid = comp.id 37 | oppath = comp.path 38 | inputs = [ 39 | { 40 | "index": x.index, 41 | "op": x.connections[0].owner.path if len(x.connections) >= 1 else None, 42 | } 43 | for x in comp.inputConnectors 44 | ] 45 | outputs = [ 46 | { 47 | "index": x.index, 48 | "op": x.connections[0].owner.path if len(x.connections) >= 1 else None, 49 | } 50 | for x in comp.outputConnectors 51 | ] 52 | 53 | lastState = buildPreset(comp) 54 | op.pwstorage.store( 55 | oppath, 56 | { 57 | "opid": opid, 58 | "lastState": lastState, 59 | "connections": {"inputs": inputs, "outputs": outputs}, 60 | }, 61 | ) 62 | 63 | 64 | def resetKeys(opTable): 65 | rows = n.rows() 66 | rows.pop(0) 67 | for i, row in enumerate(rows): 68 | oppath = n[i + 1, "path"].val 69 | curOP = op(oppath) 70 | curOP.par.clear.pulse(1, frames=1) 71 | return 72 | 73 | 74 | def retrieveConnections(inputs, outputs): 75 | if len(inputs) > 0: 76 | for i in inputs: 77 | index = i["index"] 78 | tarn = i["op"] 79 | parent.Comp.inputCOMPConnectors[index].connect(op(tarn)) 80 | if len(outputs) > 0: 81 | for o in outputs: 82 | index = o["index"] 83 | tarn = o["op"] 84 | parent.Comp.outputCOMPConnectors[index].connect(op(tarn)) 85 | 86 | 87 | def createUserDirectories(): 88 | Path(parent.Comp.par.Codeuserpresetpath.eval()).mkdir(parents=True, exist_ok=True) 89 | Path(parent.Comp.par.Codeuserfunctionpath.eval()).mkdir(parents=True, exist_ok=True) 90 | if Path(f"{parent.Comp.par.Codeuserpath.eval()}/Macros.glsl").exists != True: 91 | with open( 92 | str(Path(f"{parent.Comp.par.Codeuserpath.eval()}/Macros.glsl")), "w" 93 | ) as f: 94 | f.write("/* USER MACROS */\n") 95 | 96 | 97 | def onCreate(): 98 | parent( 99 | ).par.externaltox = f"{app.userPaletteFolder}/Pixel-Wrangle/Pixel_Wrangle.tox" 100 | parent().par.enableexternaltox = 1 101 | initStorage() 102 | initStorageOP() 103 | createUserDirectories() 104 | resetKeys = op("RESET_KEYS") 105 | resetKeys.run(delayFrames=2) 106 | 107 | resetPanel = op("RESET_PANEL") 108 | op(f"{parent.Comp}/KEYBOARDS_SHORTCUTS/panel5").bypass = True 109 | resetPanel.run(delayFrames=2) 110 | 111 | try: 112 | with open(file) as settings: 113 | vscodePath = json.loads(settings.read())["vscodePath"] 114 | nComp.par.Codeexternaleditorpath = vscodePath 115 | except Exception: 116 | with open(file, "w") as settings: 117 | settings.write(initData) 118 | nComp.par.Codeexternaleditorpath = json.loads( 119 | initData)["vscodePath"] 120 | # parent().par.enableexternaltoxpulse.pulse() 121 | return 122 | 123 | 124 | def onStart(): 125 | from LOAD_PRESET import loadPreset 126 | 127 | initStorage() 128 | preset = op.pwstorage.fetch(parent.Comp.path) 129 | try: 130 | loadPreset(preset["lastState"]) 131 | except Exception: 132 | pass 133 | createUserDirectories() 134 | resetKeys = op("RESET_KEYS") 135 | resetKeys.run(delayFrames=2) 136 | resetPanel = op("RESET_PANEL") 137 | op(f"{parent.Comp}/KEYBOARDS_SHORTCUTS/panel5").bypass = True 138 | resetPanel.run(delayFrames=2) 139 | run('parent.Comp.op("ON_CODE_CHANGE").par.active = 1', delayMilliSeconds=100) 140 | op("UPDATE_GLSL_PARMS").run(delayFrames=15) 141 | 142 | 143 | def onProjectPreSave(): 144 | from LOAD_PRESET import loadPreset 145 | 146 | initStorage() 147 | savePresetInStorage(parent.Comp) 148 | 149 | 150 | def onExit(): 151 | initStorage() 152 | savePresetInStorage(parent.Comp) 153 | -------------------------------------------------------------------------------- /script/py/init/LOAD_SETTINGS.py: -------------------------------------------------------------------------------- 1 | import json 2 | from platform import system 3 | 4 | nComp = parent.Comp 5 | 6 | file = f"{parent.Comp.par.Codelibrarypath.eval()}/settings.json" 7 | vscodePath = "" 8 | defaultvscodePath = f"{'/'.join(var('MYDOCUMENTS').split('/')[:-1])}/AppData/Local/Programs/Microsoft VS Code/Code.exe" 9 | 10 | if system == "darwin": 11 | defaultvscodePath = ( 12 | "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" 13 | ) 14 | 15 | initData = json.dumps({"vscodePath": defaultvscodePath}, indent=4) 16 | -------------------------------------------------------------------------------- /script/py/init/RESET_KEYS.py: -------------------------------------------------------------------------------- 1 | 2 | def resetKeys(opTable): 3 | rows = n.rows() 4 | rows.pop(0) 5 | for i, row in enumerate(rows): 6 | oppath = n[i + 1, "path"].val 7 | curOP = op(oppath) 8 | curOP.par.clear.pulse(1, frames=1) 9 | return 10 | 11 | 12 | n = op("KEYBOARDS") 13 | resetKeys(n) 14 | -------------------------------------------------------------------------------- /script/py/init/RESET_PANEL.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | op(f"{parent.Comp}/KEYBOARDS_SHORTCUTS/panel5").bypass = False 4 | mode = parent.Comp.par.Glmode.val 5 | if mode == "compute": 6 | parent.Comp.par.Glmode.pulse(0, frames=4) 7 | else: 8 | parent.Comp.par.Glmode.pulse(1, frames=4) 9 | 10 | op("Preset_Manager/POP_PRESET").par.Close.pulse(1, frames=1) 11 | op("Function_Manager/POP_FUNCTION").par.Close.pulse(1, frames=1) 12 | -------------------------------------------------------------------------------- /script/py/init/UPDATE_VSCODE_PATH.py: -------------------------------------------------------------------------------- 1 | from LOAD_SETTINGS import * 2 | 3 | jsonFile = None 4 | 5 | 6 | def onValueChange(par, _): 7 | 8 | # Read file as Python dict 9 | with open(file, "r") as settings: 10 | jsonFile = json.load(settings) 11 | 12 | # Update Python dict 13 | jsonFile["vscodePath"] = par.eval() 14 | 15 | # Write modified Python dict as json 16 | with open(file, "w") as settings: 17 | json.dump(jsonFile, settings) 18 | return 19 | -------------------------------------------------------------------------------- /script/py/libs/_stubs/ArcBallExt.pyi: -------------------------------------------------------------------------------- 1 | from _stubs import * 2 | class ArcBallExt(): 3 | """ 4 | The ArcBallExt Class helps with interactively controlling a Camera COMP 5 | given a Container COMP with the Mouse UV Buttons Parameters for left, 6 | middle and right enabled. 7 | 8 | Attributes 9 | ---------- 10 | ownerComp : OP 11 | Reference to the COMP this Class is initiated by. 12 | 13 | Methods 14 | ------- 15 | StartTransform(btn=None, u=0, v=0) 16 | Will begin a transform depending on the mouse button pressed. 17 | Transform(btn=None, u=0, v=0, scaler=1) 18 | Applies a transform to the ArcBall depending on the mouse button pressed. 19 | Reset() 20 | Resets the ArcBall. 21 | LoadTransform(dat=None,matrix=None) 22 | Given a tableDAT or a tdu.Matrix() it can be used to recall a saved transformation. 23 | SaveTransform(dat=op('newMat')) 24 | Will save out the current ArcBall's transformation matrix to a tableDAT. If no 25 | TableDAT is given, the internal newMat TableDAT is being used. 26 | fillMat() 27 | Utility function used by the ArcBall. 28 | """ 29 | def __init__(self, ownerComp : OP): 30 | #The component to which this extension is attached 31 | self.ownerComp = ownerComp 32 | self.arcInst = tdu.ArcBall(forCamera=True) 33 | self.matrix = tdu.Matrix() 34 | 35 | # initialize ArcBall to saved out transformation matrix 36 | matrixDAT = op('newMat') 37 | for i in range(4): 38 | for j in range(4): 39 | self.matrix[i,j] = float(matrixDAT[i,j]) 40 | 41 | matCopy = tdu.Matrix(self.matrix) 42 | self.arcInst.setTransform(matCopy) 43 | 44 | def StartTransform(self, btn : str = None, u : float = 0, v : float = 0) -> None: 45 | """ 46 | 47 | Parameters 48 | ---------- 49 | btn : str 50 | The mouse btn pressed. Can be one of 'lselect', 'rselect' or 'mselect' corrseponding to 'rotate', 'pan' and 'zoom'. 51 | u : float 52 | The horizontal mouse position on the control panel. 53 | v : float 54 | The vertical mouse position on the control panel. 55 | """ 56 | 57 | if btn == 'lselect': 58 | #if lselect ==> rotate 59 | self.arcInst.beginRotate(u,v) 60 | elif btn == 'rselect': 61 | #if rselect ==> pan 62 | self.arcInst.beginPan(u,v) 63 | elif btn == 'mselect': 64 | #if mselect ==> zoom 65 | self.arcInst.beginDolly(u,v) 66 | 67 | return 68 | 69 | def Transform(self, btn : str = None, u : float = 0, v : float = 0, scaler : float = 1) -> None: 70 | """ 71 | 72 | Parameters 73 | ---------- 74 | btn : str 75 | The mouse btn pressed. Can be one of 'lselect', 'rselect' or 'mselect' corrseponding to 'rotate', 'pan' and 'zoom'. 76 | u : float 77 | The horizontal mouse position on the control panel. 78 | v : float 79 | The vertical mouse position on the control panel. 80 | scaler : float 81 | A multiplier to increase or decrease the transformation. 82 | """ 83 | 84 | if btn == 'lselect': 85 | #if lselect ==> rotate 86 | self.arcInst.rotateTo(u,v,scale=scaler) 87 | elif btn == 'rselect': 88 | #if rselect ==> pan 89 | self.arcInst.panTo(u,v,scale=scaler) 90 | elif btn == 'mselect': 91 | #if mselect ==> zoom 92 | self.arcInst.dollyTo(u,v,scale=scaler) 93 | 94 | self.fillMat() 95 | return 96 | 97 | def Reset(self) -> None: 98 | op('autoRotate/hold1').par.pulse.pulse() 99 | op('autoRotate/hold2').par.pulse.pulse() 100 | op('autoRotate/hold3').par.pulse.pulse() 101 | self.arcInst.identity() 102 | self.fillMat() 103 | return 104 | 105 | def LoadTransform(self, dat : tableDAT = None, matrix : tdu.Matrix = None) -> None: 106 | """ 107 | Parameters 108 | ---------- 109 | dat : tableDAT 110 | An tableDAT operator reference to the tableDAT that holds the matrix to be loaded. 111 | matrix : tdu.MAtrix 112 | A tdu.Matrix object that holds the matrix to be loaded. 113 | """ 114 | 115 | if dat and dat.numRows == 4 and dat.numCols == 4: 116 | matrix = tdu.Matrix() 117 | for i in range(4): 118 | for j in range(4): 119 | matrix[i,j] = float(dat[i,j]) 120 | 121 | self.arcInst.setTransform(matrix) 122 | self.fillMat() 123 | return 124 | 125 | def SaveTransform(self, dat : tableDAT = op('newMat')) -> None: 126 | """ 127 | Parameters 128 | ---------- 129 | dat : tableDAT 130 | A tableDAT operator reference to the tableDAT where to write the current transform matrix into. 131 | """ 132 | 133 | if dat.OPType == 'tableDAT': 134 | self.matrix.fillTable(dat) 135 | 136 | def fillMat(self): 137 | newMat = self.arcInst.transform() 138 | self.matrix = newMat 139 | self.ownerComp.cook(force=True) 140 | newMat.fillTable(op('newMat')) 141 | return -------------------------------------------------------------------------------- /script/py/libs/_stubs/CallbacksExt.pyi: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /script/py/libs/_stubs/PopDialogExt.pyi: -------------------------------------------------------------------------------- 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 | from _stubs import * 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) -------------------------------------------------------------------------------- /script/py/libs/_stubs/TDCallbacksExt.pyi: -------------------------------------------------------------------------------- 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 | from _stubs import * 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 | -------------------------------------------------------------------------------- /script/py/libs/_stubs/TDCodeGen.pyi: -------------------------------------------------------------------------------- 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 | from _stubs import * 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] -------------------------------------------------------------------------------- /script/py/libs/_stubs/TDStoreTools.pyi: -------------------------------------------------------------------------------- 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 | from _stubs import * 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 | -------------------------------------------------------------------------------- /script/py/libs/_stubs/Updater.pyi: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /script/py/parm/bound_glsl_parms.py: -------------------------------------------------------------------------------- 1 | 2 | from bound_glsl_utils import * 3 | 4 | 5 | def updateParms(): 6 | 7 | # MASTER 8 | bindMaster = parent.Comp 9 | selectedPage = "Controls" 10 | 11 | # GLSL NODE 12 | target = bindMaster.op("glsl1") 13 | bindMasterRelativePath = target.relativePath(bindMaster) 14 | 15 | # PARM LIST FROM MASTER OP 16 | source = op("PARMS_TO_BIND") 17 | 18 | # PARM LIST FROM CODE DECLARATIONS 19 | fromCodeParms = op("CODE_DECLARATIONS") 20 | 21 | # GET SOURCE LIST 22 | plist = source.rows() # Structure [name, label, style, pargroup] 23 | namesRow = plist[0] 24 | plist.pop(0) 25 | updatedPlist = [ 26 | p 27 | for p in plist 28 | if bindMaster.par[p[0].val].page == selectedPage 29 | and p[2].val 30 | in ["Int", "Float", "RGB", "RGBA", "Menu", "CHOP", "Toggle", "Pulse"] 31 | ] 32 | plist = updatedPlist 33 | 34 | # GROUP BY PARGROUPS 35 | parGroups = getParGroups(plist, namesRow) 36 | 37 | # Custom Parameters Offsets to leave space to built-ins 38 | GLUniParOffset = 14 39 | GLarrayParOffset = 0 40 | GLmatParOffset = 0 41 | GLacParOffset = 0 42 | GLconstParOffset = 0 43 | 44 | GLuniCurrentParmsOP = op("CURRENT_UNIFORMS") 45 | GLarrayCurrentParmsOP = op("CURRENT_ARRAY") 46 | GLmatCurrentParmsOP = op("CURRENT_MATRIX") 47 | GLacCurrentParmsOP = op("CURRENT_ATOMIC") 48 | GLconstCurrentParmsOP = op("CURRENT_CONST") 49 | 50 | GLuniParGroups = [] 51 | GLarrayParGroups = [] 52 | GLmatParGroups = [] 53 | GLacParGroups = [] 54 | GLconstParGroups = [] 55 | 56 | toResetParms = [ 57 | {"op": GLuniCurrentParmsOP, "offset": GLUniParOffset}, 58 | {"op": GLarrayCurrentParmsOP, "offset": GLarrayParOffset}, 59 | {"op": GLmatCurrentParmsOP, "offset": GLmatParOffset}, 60 | {"op": GLacCurrentParmsOP, "offset": GLacParOffset}, 61 | {"op": GLconstCurrentParmsOP, "offset": GLconstParOffset}, 62 | ] 63 | 64 | # Reset parms with corresponding offsets 65 | resetParameters( 66 | target, 67 | toResetParms) 68 | 69 | # Reordering each GL Parm 70 | setParameterSection( 71 | fromCodeParms, 72 | parGroups, 73 | GLuniParGroups, 74 | GLarrayParGroups, 75 | GLmatParGroups, 76 | GLacParGroups, 77 | GLconstParGroups, 78 | ) 79 | 80 | ## SET STANDARD UNIFORMS ## 81 | setUniforms( 82 | target, 83 | source, 84 | bindMasterRelativePath, 85 | GLUniParOffset, 86 | GLuniParGroups) 87 | 88 | ## SET ARRAY UNIFORMS ## 89 | setArrays( 90 | target, 91 | fromCodeParms, 92 | bindMasterRelativePath, 93 | GLarrayParOffset, 94 | GLarrayParGroups, 95 | ) 96 | 97 | ## SET MATRIX UNIFORMS ## 98 | setMatrices( 99 | target, 100 | bindMasterRelativePath, 101 | GLmatParOffset, 102 | GLmatParGroups) 103 | 104 | ## SET ATOMIC COUNTERS ## 105 | setAtomicCounters( 106 | target, 107 | fromCodeParms, 108 | bindMasterRelativePath, 109 | GLacParOffset, 110 | GLacParGroups 111 | ) 112 | 113 | ## SET CONST ## 114 | setVulkanConstants( 115 | target, 116 | bindMasterRelativePath, 117 | GLconstParOffset, 118 | GLconstParGroups 119 | ) 120 | return 121 | 122 | 123 | ## BUILTIN FUNCTIONS ## 124 | def onOffToOn(channel, sampleIndex, val, prev): 125 | updateParms() 126 | return 127 | -------------------------------------------------------------------------------- /script/py/parm/bound_glsl_utils.py: -------------------------------------------------------------------------------- 1 | from pprint import pprint 2 | 3 | from re import findall 4 | 5 | ## CUSTOM FUNCTIONS ## 6 | def getUntouched(refTable, offset): 7 | result = [] 8 | for row in refTable.rows(): 9 | val = row[0].val 10 | digit = int(findall(r"\d+", val)[0]) 11 | if digit < offset: 12 | result.append(val) 13 | return result 14 | 15 | 16 | def resetParms(refTable, targetGLSL, offset): 17 | untouched = getUntouched(refTable, offset) 18 | for row in refTable.rows(): 19 | if row[0].val not in untouched: 20 | p = targetGLSL.par[row[0].val] 21 | p.bindExpr = "" 22 | p.expr = "" 23 | p.mode = ParMode.CONSTANT 24 | if p.style in [ 25 | "SOP", 26 | "PanelCOMP", 27 | "Python", 28 | "TOP", 29 | "MAT", 30 | "COMP", 31 | "CHOP", 32 | "File", 33 | "Folder", 34 | "Str", 35 | "StrMenu", 36 | ]: 37 | p.val = "" 38 | else: 39 | p.val = 0 40 | 41 | 42 | def getCellValue(dat, row, search, refRow=0): 43 | return row[dat.findCell(search, rows=[refRow]).col].val 44 | 45 | 46 | def getParGroups(parmList, namesRow, selectedColName="pargroup"): 47 | if type(selectedColName) == int: 48 | rmDouble = {k[selectedColName].val: v for v, k in enumerate(reversed(parmList))} 49 | elif type(selectedColName) == str: 50 | index = [name.val for name in namesRow].index(selectedColName) 51 | selectedColName = index 52 | rmDouble = {k[selectedColName].val: v for v, k in enumerate(reversed(parmList))} 53 | sortedKeys = sorted(rmDouble, key=rmDouble.get, reverse=True) 54 | result = [] 55 | for pGroupName in sortedKeys: 56 | parGroup = [p for p in parmList if p[selectedColName].val == pGroupName] 57 | result.append(parGroup) 58 | return result 59 | 60 | def buildTargetName(index, ptype): 61 | #build type dict 62 | #get current ptype 63 | # 64 | pass 65 | 66 | def setAtomicCounters( 67 | target, fromCodeParms, bindMasterRelativePath, GLacParOffset, GLacParGroups 68 | ): 69 | for i, parGroup in enumerate(GLacParGroups): 70 | index = i + GLacParOffset 71 | pGroupName = parGroup[0][3].val 72 | pAtomicType = fromCodeParms[pGroupName, "acinitval"].val 73 | target.par[f"ac{str(index)}name"].val = pGroupName # 2022 -> f"acname{str(index)}" 74 | target.par[f"ac{str(index)}initvalue"].val = pAtomicType # 2022 -> f"acinitval{str(index)}" 75 | if pAtomicType == "chop": 76 | target.par[ 77 | f"ac{str(index)}chopvalue" # 2022 -> f"acchopval{str(index)}" 78 | ].expr = f"op('{bindMasterRelativePath}').par['{pGroupName}']" 79 | else: 80 | target.par[ 81 | f"ac{str(index)}singlevalue" # 2022 -> f"acsingleval{str(index)}" 82 | ].expr = f"op('{bindMasterRelativePath}').par['{pGroupName}']" 83 | 84 | 85 | def setExpression(pGroupName, tarpName, tarpVal, bindMasterRelativePath): 86 | tarpName.val = pGroupName 87 | tarpVal.mode = ParMode.EXPRESSION 88 | tarpVal.expr = f"op('{bindMasterRelativePath}').par['{pGroupName}']" 89 | 90 | 91 | def setVulkanConstants( 92 | target, bindMasterRelativePath, GLconstParOffset, GLconstParGroups 93 | ): 94 | for i, parGroup in enumerate(GLconstParGroups): 95 | index = i + GLconstParOffset 96 | pGroupName = parGroup[0][3].val 97 | tarpName = target.par[f"const{str(index)}name" ] # 2022 -> f"constname{str(index)}" 98 | tarpVal = target.par[f"const{str(index)}value"] # 2022 -> f"constvalue{str(index)}" 99 | setExpression(pGroupName, tarpName, tarpVal, bindMasterRelativePath) 100 | 101 | 102 | def setMatrices(target, bindMasterRelativePath, GLmatParOffset, GLmatParGroups): 103 | for i, parGroup in enumerate(GLmatParGroups): 104 | index = i + GLmatParOffset 105 | tarpName = target.par[f"matrix{str(index)}name"] # 2022 -> f"matuniname{str(index)}" 106 | tarpVal = target.par[f"matrix{str(index)}value"] # 2022 -> f"matvalue{str(index)}" 107 | pGroupName = parGroup[0][3].val 108 | setExpression(pGroupName, tarpName, tarpVal, bindMasterRelativePath) 109 | 110 | 111 | def setArrays( 112 | target, fromCodeParms, bindMasterRelativePath, GLarrayParOffset, GLarrayParGroups 113 | ): 114 | for i, parGroup in enumerate(GLarrayParGroups): 115 | index = i + GLarrayParOffset 116 | pGroupName = parGroup[0][3].val 117 | pArrayType = fromCodeParms[pGroupName, "arraytype"].val 118 | pArrayValueType = fromCodeParms[pGroupName, "vtype"].val 119 | pArrayChop = fromCodeParms[pGroupName, "chop"].val 120 | target.par[f"array{str(index)}name"].val = pGroupName # 2022 -> f"chopuniname{str(index)}" 121 | target.par[f"array{str(index)}arraytype"].val = pArrayType # 2022 -> f"choparraytype{str(index)}" 122 | target.par[f"array{str(index)}type"].val = pArrayValueType # 2022 -> f"chopunitype{str(index)}" 123 | if pArrayChop: 124 | target.par[f"array{str(index)}chop"].mode = ParMode.EXPRESSION # 2022 -> f"chop{str(index)}" 125 | target.par[ 126 | f"array{str(index)}chop" # 2022 -> f"chop{str(index)}" 127 | ].expr = f"op('{bindMasterRelativePath}').par['{pGroupName}']" 128 | 129 | 130 | def setUniforms(target, source, bindMasterRelativePath, GLUniParOffset, GLuniParGroups): 131 | for i, parGroup in enumerate(GLuniParGroups): 132 | uniComponent = ["x", "y", "z", "w"] 133 | index = i + GLUniParOffset 134 | pGroupName = parGroup[0][3].val 135 | target.par[f"vec{str(index)}name"].val = pGroupName # 2022 -> f"uniname{str(index)}" 136 | 137 | ## BIND EACH PARM COMPONENTS ## 138 | for j, p in enumerate(parGroup): 139 | pName = getCellValue(source, p, "name") 140 | targetp = target.par[f"vec{str(index)}value{uniComponent[j]}" ] # 2022 -> f"value{str(index)}{uniComponent[j]}" 141 | targetp.mode = ParMode.EXPRESSION 142 | targetp.expr = f"op('{bindMasterRelativePath}').par['{pName}']" 143 | 144 | 145 | def setParameterSection( 146 | fromCodeParms, 147 | parGroups, 148 | GLuniParGroups, 149 | GLarrayParGroups, 150 | GLmatParGroups, 151 | GLacParGroups, 152 | GLconstParGroups, 153 | ): 154 | for parGroup in parGroups: 155 | pGroupName = parGroup[0][3].val 156 | pGLType = fromCodeParms[pGroupName, "gl_type"].val 157 | pIsArray = fromCodeParms[pGroupName, "array"].val 158 | # Array uniform type 159 | if pIsArray == "1": 160 | GLarrayParGroups.append(parGroup) 161 | elif pGLType == "mat": 162 | GLmatParGroups.append(parGroup) 163 | elif pGLType == "ac": 164 | GLacParGroups.append(parGroup) 165 | elif pGLType == "const": 166 | GLconstParGroups.append(parGroup) 167 | else: 168 | GLuniParGroups.append(parGroup) 169 | 170 | 171 | def resetParameters(target, toResetParms): 172 | for toReset in toResetParms: 173 | curParmOP = toReset["op"] 174 | offset = toReset["offset"] 175 | resetParms(curParmOP, target, offset) 176 | -------------------------------------------------------------------------------- /script/py/parm/create_parent_parms.py: -------------------------------------------------------------------------------- 1 | 2 | from re import sub 3 | from ast import literal_eval 4 | from TDFunctions import getCustomPage 5 | from TDJSON import addParameterFromJSONDict, parameterToJSONPar, pageToJSONDict 6 | 7 | 8 | # CUSTOM FUNCTIONS 9 | 10 | 11 | def addParFromJSON( 12 | target, 13 | src, 14 | replace=True, 15 | setValues=True, 16 | ignoreAttrErrors=True, 17 | fixParNames=False, 18 | setBuiltIns=False, 19 | ): 20 | return addParameterFromJSONDict( 21 | target, src, replace, setValues, ignoreAttrErrors, fixParNames, setBuiltIns 22 | ) 23 | 24 | 25 | def updateParm(src: dict, pNames, JSONpage): 26 | updatedPage = JSONpage 27 | 28 | if src["name"] in updatedPage: 29 | 30 | # Delete unused 31 | if src["name"] not in pNames: 32 | p = parent.Comp.parGroup[src["name"]] 33 | p.destroy() 34 | updatedPage.pop(src["name"]) 35 | 36 | else: 37 | oldp = updatedPage[src["name"]] 38 | for key in list(oldp.keys()): 39 | if key not in ["val", "expr", "bindExpr", "style", "page", "name"]: 40 | del oldp[key] 41 | oldp["defaultExpr"] = "" 42 | oldp = {**src, **oldp} 43 | 44 | addParFromJSON(parent.Comp, oldp) 45 | 46 | else: 47 | addParFromJSON(parent.Comp, src) 48 | 49 | 50 | # OPERATORS 51 | source = op("BUILTIN_PARMS") 52 | target = parent.Comp 53 | page = None 54 | 55 | StringTypes = ( 56 | "SOP", 57 | "PanelCOMP", 58 | "Python", 59 | "TOP", 60 | "MAT", 61 | "COMP", 62 | "CHOP", 63 | "File", 64 | "Folder", 65 | "Str", 66 | "StrMenu", 67 | ) 68 | 69 | 70 | def onTableChange(dat: DAT): 71 | page = None 72 | pageJSONDict = {} 73 | 74 | # GET TARGET PAGE 75 | page = getCustomPage(target, "Controls") or target.appendCustomPage("Controls") 76 | pageJSONDict = pageToJSONDict(page, extraAttrs="*", forceAttrLists=True) 77 | 78 | # SORT PAGES 79 | target.sortCustomPages("Controls", "Code", "Inputs", "Outputs", "GLSL", "Globals") 80 | # GET ALL DECLARED PARMS 81 | plist = dat.rows() 82 | 83 | # REMOVE HEADERS 84 | plist.pop(0) 85 | 86 | pNames = [dat[i + 1, "name"].val for i, _ in enumerate(plist)] 87 | # GET CURRENT UI PARMS 88 | curPars = page.pars 89 | 90 | # CLEAN OLD 91 | for curP in curPars: 92 | if curP.valid and curP.name not in pNames: 93 | curP.destroy() 94 | 95 | # FOR EACH DECLARED PARMS 96 | for index, _ in enumerate(plist): 97 | 98 | # STARTING AT ROW 1 99 | i = index + 1 100 | 101 | # IF MIN MAX EXISI CAST IT TO FLOAT 102 | min = float(dat[i, "min"].val if dat[i, "min"].val != "None" else "0") 103 | max = float(dat[i, "max"].val if dat[i, "max"].val != "None" else "1") 104 | # GET DEFAULT STRING PATTERN 105 | default = dat[i, "default"].val 106 | 107 | # IS AN ARRAY 108 | array = int(dat[i, "array"].val) 109 | 110 | # BUILD STRING LIST FROM MENU RAW STRING IF EXIST, ELSE SET MENU TO EMPTY STRING 111 | menu = [] 112 | 113 | try: 114 | # If menu prop exist, parse to list of string 115 | if dat[i, "menu"].valid: 116 | menu = [str(x) for x in literal_eval(dat[i, "menu"].val)] 117 | else: 118 | menu = "" 119 | except: 120 | menu = "" 121 | 122 | # Vector default string to list 123 | if dat[i, "ptype"].val.find("vec") != -1: 124 | default = [float(x) for x in literal_eval(default)] 125 | # IF OP / STRING TYPE, GET CORRESPONFING KEY 126 | if dat[i, "style"].val.startswith(StringTypes): 127 | tarKey = dat[i, "style"].val.lower() 128 | if dat[i, tarKey].val == "None": 129 | default = "" 130 | pmode = ParMode.CONSTANT 131 | 132 | if dat[i, "expr"].val != "None": 133 | pmode = ParMode.EXPRESSION 134 | 135 | if dat[i, "bindExpr"].val != "None": 136 | pmode = ParMode.BIND 137 | 138 | # SET PARAMETER SETTINGS 139 | 140 | pSettings = { 141 | "name": dat[i, "name"].val, 142 | "mode": pmode, 143 | "label": dat[i, "label"].val, 144 | "order": int(dat[i, "order"].val), 145 | "startSection": int(dat[i, "section"].val) 146 | if dat[i, "section"].val != "None" 147 | else 0, 148 | "readOnly": int(dat[i, "readonly"].val) 149 | if dat[i, "readonly"].val != "None" 150 | else 0, 151 | "help": dat[i, "help"].val if dat[i, "help"].val != "None" else "", 152 | "page": "Controls", 153 | "style": dat[i, "style"].val if dat[i, "style"].val != "None" else "Float", 154 | "menuNames": [sub(r"\W+|^(?=\d)", "_", x.lower()) for x in menu], 155 | "menuLabels": menu, 156 | "type": dat[i, "ptype"].val, 157 | "vtype": dat[i, "vtype"].val, 158 | "array": array, 159 | "arraytype": dat[i, "arraytype"].val.lower() if array == 1 else "", 160 | "size": int(dat[i, "typesize"].val), 161 | "enableExpr": dat[i, "enableExpr"].val 162 | if dat[i, "enableExpr"].val != "None" 163 | else "", 164 | "expr": f"{dat[i, 'expr'].val}" if dat[i, "expr"].val != "None" else "", 165 | "bindExpr": dat[i, "bindExpr"].val 166 | if dat[i, "bindExpr"].val != "None" 167 | else "", 168 | "min": min, 169 | "max": max, 170 | "normMin": min, 171 | "normMax": max, 172 | "default": default, 173 | "val": default, 174 | } 175 | 176 | # UPDATE PARAMETER UI 177 | updateParm(pSettings, pNames, pageJSONDict) 178 | # [TD BUG] Restore Parameter Mode 179 | parent.Comp.parGroup[pSettings["name"]].mode = pmode 180 | 181 | return 182 | -------------------------------------------------------------------------------- /script/py/parse/build-libs.py: -------------------------------------------------------------------------------- 1 | 2 | import itertools 3 | import TDFunctions 4 | from pathlib import PurePosixPath, Path 5 | from re import sub 6 | 7 | tarN = op(me.par.dat) 8 | 9 | 10 | def getUniquePathList(dat, relPathColumn="relpath"): 11 | rows = dat.rows() 12 | rows.pop(0) 13 | pathList = [] 14 | for id, row in enumerate(rows): 15 | relPath = tarN[id + 1, relPathColumn].val 16 | if relPath is not None: 17 | splittedPath = relPath.split("/") 18 | if len(splittedPath) > 1 and relPath.endswith(".glsl"): 19 | del splittedPath[len(splittedPath) - 1] 20 | pathList.append(splittedPath) 21 | pathList.sort() 22 | return ["/".join(x) for x in [l for l, _ in itertools.groupby(pathList)]] 23 | 24 | 25 | def createFolderStructure(pathList, parent="/libs"): 26 | if op(parent) is None: 27 | parent = op("/").create(baseCOMP, "libs") 28 | for path in pathList: 29 | pathSegments = path.split("/") 30 | maxDepth = len(pathSegments) 31 | completePath = [] 32 | for i in range(maxDepth): 33 | completePath.append(pathSegments[i]) 34 | currentPath = "/".join(completePath) 35 | if op(f"{parent}/{currentPath}") is None: 36 | ancestor = PurePosixPath(f"{parent}/{currentPath}").parent 37 | node = op(str(ancestor)).create(baseCOMP, pathSegments[i]) 38 | TDFunctions.arrangeNode(node, position="left", spacing=20) 39 | else: 40 | continue 41 | return parent 42 | 43 | 44 | def correctGLSL(filePath): 45 | with open(filePath) as f: 46 | originalLines = f.readlines() 47 | output = [] 48 | for line in originalLines: 49 | if line.startswith("#include "): 50 | lineList = line.split(" ") 51 | posixPath = PurePosixPath( 52 | lineList[1].replace('"', "").replace("\n", "") 53 | ) 54 | curLineFolder = str(posixPath.parent) 55 | curLineName = str(posixPath.name) 56 | curLineName = curLineName.replace(".", "_") 57 | curLine = f"{curLineFolder}/{curLineName}" 58 | if curLine.startswith("./"): 59 | curLine = curLine[2:] 60 | curLine = f"{lineList[0]} <{curLine}>\n" 61 | output.append(curLine) 62 | elif line.find("texture") != -1: 63 | line = sub("(texture).*[(]", "texture(", line) 64 | output.append(line) 65 | else: 66 | output.append(line) 67 | return "".join(output) 68 | 69 | 70 | def createFiles(root, dat): 71 | rows = dat.rows() 72 | rows.pop(0) 73 | basePath = f"{root}/" 74 | paths = [] 75 | for i, row in enumerate(rows): 76 | relPath = PurePosixPath(basePath + dat[i + 1, "relpath"].val) 77 | opName = str(relPath.stem) 78 | opExt = str(relPath.suffix) 79 | opPath = str(relPath.parent) 80 | targetOP = op(f"{opPath}/{opName}{opExt.replace('.', '_')}") 81 | if targetOP is None: 82 | opTarget = dat[i + 1, "path"].val 83 | current = op(opPath).create(textDAT, f"{opName}{opExt.replace('.', '_')}") 84 | TDFunctions.arrangeNode(current, position="left", spacing=20) 85 | text = correctGLSL(opTarget) 86 | formatedFilePath = ( 87 | f"{parent.Comp.par.Codelibrarypath}/Functions/dist/" 88 | + str(dat[i + 1, "relpath"].val) 89 | ) 90 | formatedFile = Path(formatedFilePath) 91 | formatedFile.parent.mkdir(exist_ok=True, parents=True) 92 | formatedFile.write_text(text) 93 | lang = opExt.replace(".", "") 94 | # current.write(text) 95 | try: 96 | current.par.file = str(Path(formatedFilePath)) 97 | current.par.file.readOnly = True 98 | current.par.syncfile = 1 99 | current.par.syncfile.readOnly = True 100 | current.par.language = lang.lower() 101 | except: 102 | pass 103 | paths.append(current.path) 104 | else: 105 | paths.append(targetOP.path) 106 | 107 | return paths 108 | 109 | 110 | def updateTable(table, pathlist): 111 | for id, path in enumerate(pathlist): 112 | if path != "": 113 | table[id, 0].val = path 114 | else: 115 | continue 116 | 117 | 118 | def onTableChange(dat): 119 | if len(dat.rows()) > 1: 120 | table = op("oppaths") 121 | uniquePathList = getUniquePathList(dat) 122 | rootFolder = createFolderStructure(uniquePathList) 123 | pathList = createFiles(rootFolder, dat) 124 | table.par.rows = len(pathList) 125 | updateTable(table, pathList) 126 | return 127 | 128 | 129 | def onRowChange(dat, rows): 130 | return 131 | 132 | 133 | def onColChange(dat, cols): 134 | return 135 | 136 | 137 | def onCellChange(dat, cells, prev): 138 | return 139 | 140 | 141 | def onSizeChange(dat): 142 | return 143 | -------------------------------------------------------------------------------- /script/py/parse/common/gl_types.py: -------------------------------------------------------------------------------- 1 | import re 2 | from dataclasses import dataclass 3 | from enum import Enum, auto 4 | from typing import TypedDict, Tuple 5 | from str_line_utils import JSONableMutable 6 | 7 | 8 | GLSL_TYPE_MAP = { 9 | "//---": {"style": "Header", "default_name": "Header"}, 10 | "bool": {"style": "Toggle", "dimension": 1}, 11 | "float": {"style": "Float", "dimension": 1}, 12 | "double": {"style": "Float", "dimension": 1}, 13 | "int": {"style": "Int", "dimension": 1}, 14 | "uint": {"style": "Int", "dimension": 1}, 15 | "atomic_uint": {"style": "Int", "dimension": 1}, 16 | "vec2": {"style": "Float", "dimension": 2}, 17 | "vec3": {"style": "Float", "dimension": 3}, 18 | "vec4": {"style": "Float", "dimension": 4}, 19 | "dvec2": {"style": "Float", "dimension": 2}, 20 | "dvec3": {"style": "Float", "dimension": 3}, 21 | "dvec4": {"style": "Float", "dimension": 4}, 22 | "ivec2": {"style": "Int", "dimension": 2}, 23 | "ivec3": {"style": "Int", "dimension": 3}, 24 | "ivec4": {"style": "Int", "dimension": 4}, 25 | "uvec2": {"style": "Int", "dimension": 2}, 26 | "uvec3": {"style": "Int", "dimension": 3}, 27 | "uvec4": {"style": "Int", "dimension": 4}, 28 | "bvec2": {"style": "Int", "dimension": 2}, 29 | "bvec3": {"style": "Int", "dimension": 3}, 30 | "bvec4": {"style": "Int", "dimension": 4}, 31 | "mat2": {"style": "Chop"}, 32 | "mat3": {"style": "Chop"}, 33 | "mat4": {"style": "Chop"}, 34 | } 35 | 36 | @dataclass 37 | class GLItemPage(Enum): 38 | UI = auto() 39 | VECTOR = auto() 40 | ARRAY = auto() 41 | MATRIX = auto() 42 | ATOMIC = auto() 43 | CONSTANT = auto() 44 | HEADER = auto() 45 | 46 | LineMatch = TypedDict("LineMatch", {"gl_page": GLItemPage, "match": re.Match}) 47 | MatchPropTuple = Tuple[str, JSONableMutable] -------------------------------------------------------------------------------- /script/py/parse/common/str_line_utils.py: -------------------------------------------------------------------------------- 1 | import re, ast 2 | from typing import Union 3 | 4 | JSONableValue = Union[int, float, str] 5 | JSONableMutable = Union[dict, list, JSONableValue] 6 | 7 | 8 | def try_parse_int(value: JSONableValue) -> int or None: 9 | try: 10 | return int(value) 11 | except ValueError: 12 | return None 13 | 14 | 15 | def try_parse_float(value: JSONableValue) -> float or None: 16 | try: 17 | return float(value) 18 | except ValueError: 19 | return None 20 | 21 | 22 | def cast_string(value: str) -> JSONableValue: 23 | int_value = try_parse_int(value) 24 | if int_value is not None: 25 | return int_value 26 | float_value = try_parse_float(value) 27 | return float_value if float_value is not None else value 28 | 29 | 30 | def cast_dict(value: dict) -> dict[JSONableMutable]: 31 | return {k: cast_value(v) for k, v in value.items()} 32 | 33 | 34 | def cast_list(value: list) -> list[JSONableMutable]: 35 | return [cast_value(x) for x in value] 36 | 37 | 38 | def cast_value(value: JSONableMutable) -> JSONableMutable: 39 | if isinstance(value, dict): 40 | return cast_dict(value) 41 | elif isinstance(value, list): 42 | return cast_list(value) 43 | elif isinstance(value, str): 44 | return cast_string(value) 45 | else: 46 | return value 47 | 48 | 49 | def try_parse_mutable(any_str: any) -> JSONableMutable: 50 | try: 51 | return eval_type(any_str) 52 | except (SyntaxError, ValueError): 53 | return any_str 54 | 55 | 56 | def eval_type(any_str: str) -> any: 57 | return ast.literal_eval(any_str) 58 | 59 | 60 | def split_string_at_characters( 61 | string: str, characters: list[str], maxsplit: int = 0 62 | ) -> list[str]: 63 | split_pattern = "|".join(map(re.escape, characters)) 64 | split_result = re.split(split_pattern, string, maxsplit) 65 | return [x.strip() for x in split_result if x.strip()] 66 | 67 | def build_TD_par_name(anystr: str, prefix:str = "") -> str: 68 | cleaned_string = prefix + re.sub(r"[^a-zA-Z]+", "", anystr) 69 | return cleaned_string.capitalize() -------------------------------------------------------------------------------- /script/py/parse/get-function-signatures.py: -------------------------------------------------------------------------------- 1 | from re import match 2 | 3 | types = ( 4 | "float", 5 | "double", 6 | "int", 7 | "bool", 8 | "vec2", 9 | "vec3", 10 | "vec4", 11 | "uvec2", 12 | "uvec3", 13 | "uvec4", 14 | "ivec2", 15 | "ivec3", 16 | "ivec4", 17 | "bvec2", 18 | "bvec3", 19 | "bvec4", 20 | "mat2", 21 | "mat3", 22 | "mat4", 23 | ) 24 | 25 | variantn = op("Variants") 26 | descriptionn = op("Description") 27 | 28 | 29 | def onTableChange(dat): 30 | text = dat.text 31 | start = text.find("/*") + 2 32 | end = text.find("*/") 33 | variantn.clear() 34 | descriptionn.clear() 35 | 36 | if start + end > -1: 37 | descriptionn.write("/*\n\n" + text[start:end].strip() + "\n\n*/") 38 | for line in text.split("\n"): 39 | if line.startswith(types) or match("^[\w]+\s.*", line): 40 | variantn.write(line.split("{")[0].strip() + "\n\n") 41 | 42 | return 43 | -------------------------------------------------------------------------------- /script/py/parse/glsl_code_analyzer/analyzer_code_splitter.py: -------------------------------------------------------------------------------- 1 | from analyzer_line_identifier import GLSLLineIdentifier 2 | 3 | 4 | class GLSLCodeSplitter: 5 | def __init__( 6 | self, code: str, line_identifier: GLSLLineIdentifier = GLSLLineIdentifier() 7 | ): 8 | self.identifier = line_identifier 9 | self.code = code 10 | 11 | def run(self) -> list[str]: 12 | '''Splits the code into lines and removes comments and empty lines.''' 13 | code_lines = list(filter(self.identifier.is_comment, self.code.splitlines())) 14 | return [x for x in code_lines if x != ""] 15 | -------------------------------------------------------------------------------- /script/py/parse/glsl_code_analyzer/analyzer_exec.py: -------------------------------------------------------------------------------- 1 | jsonOP = parent.Analyzer.op("Analyzer_Result") 2 | 3 | 4 | def run(): 5 | print("Analyzer: Started") 6 | result = parent.Analyzer.Send_to_textDAT(jsonOP) 7 | print("Analyzer: Finished") 8 | return result 9 | -------------------------------------------------------------------------------- /script/py/parse/glsl_code_analyzer/analyzer_ext.py: -------------------------------------------------------------------------------- 1 | from analyzer_uniform_parser import GLSLUniformCodeParser 2 | from json import dumps 3 | 4 | class GLSLCodeAnalyzer: 5 | @staticmethod 6 | def __getCode(oppath: DAT) -> str or None: 7 | return op(oppath).text if isinstance(op(oppath), DAT) else None 8 | 9 | def __init__( 10 | self, 11 | ownerComp: COMP, 12 | parser: GLSLUniformCodeParser = GLSLUniformCodeParser 13 | ): 14 | self.__Parser = parser 15 | self.owner = ownerComp 16 | self.last_result = None 17 | 18 | @property 19 | def code(self): 20 | return self.__getCode(self.owner.par.Op) 21 | 22 | def Build_JSON(self) -> str: 23 | parser = self.__Parser 24 | line_obj_list = parser(self.code).get_items() 25 | self.last_result = dumps(line_obj_list, sort_keys=True, indent=4) 26 | return self.last_result 27 | 28 | def Send_to_textDAT(self, OP:textDAT) -> str or None: 29 | try: 30 | OP.clear() 31 | OP.text = self.Build_JSON() 32 | return self.last_result 33 | except Exception: 34 | print("This OP is not valid") 35 | return None 36 | -------------------------------------------------------------------------------- /script/py/parse/glsl_code_analyzer/analyzer_line_identifier.py: -------------------------------------------------------------------------------- 1 | class GLSLLineIdentifier: 2 | @staticmethod 3 | def is_header(stringLineGLSL: str) -> bool: 4 | return stringLineGLSL.startswith("//---") 5 | 6 | @staticmethod 7 | def is_constant(stringLineGLSL: str) -> bool: 8 | return stringLineGLSL.startswith("const ") 9 | 10 | @staticmethod 11 | def is_matrix(stringLineGLSL: str) -> bool: 12 | return stringLineGLSL.startswith("mat") 13 | 14 | @staticmethod 15 | def is_atomic_counter(stringLineGLSL: str) -> bool: 16 | return stringLineGLSL.startswith("atomic_uint") 17 | 18 | @staticmethod 19 | def is_array(stringLineGLSL: str) -> bool: 20 | lineBegin = stringLineGLSL.split("//")[0] 21 | return "[" in lineBegin and "]" in lineBegin 22 | 23 | @staticmethod 24 | def is_comment(stringLineGLSL: str) -> bool: 25 | is_comment = stringLineGLSL.startswith(("//", "/*")) 26 | return not is_comment or GLSLLineIdentifier.is_header(stringLineGLSL) 27 | 28 | -------------------------------------------------------------------------------- /script/py/parse/glsl_code_analyzer/analyzer_line_matcher.py: -------------------------------------------------------------------------------- 1 | import re 2 | from analyzer_line_identifier import GLSLLineIdentifier 3 | import analyzer_regex_patterns as regex_patterns 4 | from gl_types import GLItemPage, LineMatch 5 | 6 | 7 | class GLSLLineMatcher: 8 | @staticmethod 9 | def __match_GLSL_variable(stringLineGLSL: str, pattern: re.Pattern) -> re.Match: 10 | return re.match(pattern, stringLineGLSL) 11 | 12 | def __init__( 13 | self, code_line: str, line_identifier: GLSLLineIdentifier = GLSLLineIdentifier() 14 | ): 15 | self.code_line = code_line 16 | self.__line_identifier = line_identifier 17 | self.__match_functions = { 18 | self.__line_identifier.is_header: self.__build_ui_match_object, 19 | self.__line_identifier.is_constant: self.__build_constant_match_object, 20 | self.__line_identifier.is_atomic_counter: self.__build_atomic_counter_match_object, 21 | self.__line_identifier.is_matrix: self.__build_matrix_match_object, 22 | self.__line_identifier.is_array: self.__build_array_match_object, 23 | } 24 | 25 | def __build_standard_match_object(self) -> LineMatch: 26 | return { 27 | "gl_page": GLItemPage.VECTOR.name, 28 | "match": self.__match_GLSL_variable( 29 | self.code_line, regex_patterns.standard 30 | ), 31 | } 32 | 33 | def __build_array_match_object(self) -> LineMatch: 34 | return { 35 | "gl_page": GLItemPage.ARRAY.name, 36 | "match": self.__match_GLSL_variable(self.code_line, regex_patterns.array), 37 | } 38 | 39 | def __build_matrix_match_object(self) -> LineMatch: 40 | return { 41 | "gl_page": GLItemPage.MATRIX.name, 42 | "match": self.__match_GLSL_variable( 43 | self.code_line, regex_patterns.standard 44 | ), 45 | } 46 | 47 | def __build_atomic_counter_match_object(self) -> LineMatch: 48 | return { 49 | "gl_page": GLItemPage.ATOMIC.name, 50 | "match": self.__match_GLSL_variable( 51 | self.code_line, regex_patterns.standard 52 | ), 53 | } 54 | 55 | def __build_constant_match_object(self) -> LineMatch: 56 | return { 57 | "gl_page": GLItemPage.CONSTANT.name, 58 | "match": self.__match_GLSL_variable( 59 | self.code_line, regex_patterns.constant 60 | ), 61 | } 62 | 63 | def __build_ui_match_object(self) -> LineMatch: 64 | return { 65 | "gl_page": GLItemPage.UI.name, 66 | "match": self.__match_GLSL_variable(self.code_line, regex_patterns.ui), 67 | } 68 | 69 | def run(self): 70 | for check_func, match_func in self.__match_functions.items(): 71 | if check_func(self.code_line): 72 | return match_func() 73 | return self.__build_standard_match_object() 74 | -------------------------------------------------------------------------------- /script/py/parse/glsl_code_analyzer/analyzer_on_input_change.py: -------------------------------------------------------------------------------- 1 | import analyzer_exec 2 | 3 | def onCellChange(_, __, ___): 4 | analyzer_exec.run() 5 | return 6 | -------------------------------------------------------------------------------- /script/py/parse/glsl_code_analyzer/analyzer_on_reset.py: -------------------------------------------------------------------------------- 1 | import analyzer_exec 2 | 3 | def onPulse(_): 4 | return analyzer_exec.run() 5 | -------------------------------------------------------------------------------- /script/py/parse/glsl_code_analyzer/analyzer_regex_patterns.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | OPTIONAL_UNIFORM_PATTERN = r"(uniform\s+)?" 4 | CONSTANT_PATTERN = r"const\s+" 5 | TYPE_PATTERN = r"(?P\w+)\s+" 6 | NAME_PATTERN = r"(?P\w+)\s*" 7 | ARRAY_LENGTH_PATTERN = r"\[(?P\d+)\]" 8 | EQUAL_PATTERN = r"=\s*" 9 | INIT_VAL_PATTERN = r"(?P[\w.]+)" 10 | SEMICOLON_PATTERN = r";?" 11 | OPTIONAL_PROPS_PATTERN = r"(?:\s*//\s*(?P.*))?" 12 | UI_TYPE_PATTERN = r"(?P//---)?" 13 | UI_PROPS_PATTERN = r"\s*(?P.*)?" 14 | 15 | 16 | # Example : const int MAX_LIGHTS = 8; // section=1; label=Max Lights; min=1; max=8; 17 | constant = re.compile( 18 | rf""" 19 | {OPTIONAL_UNIFORM_PATTERN} 20 | {CONSTANT_PATTERN} 21 | {TYPE_PATTERN} 22 | {NAME_PATTERN} 23 | {EQUAL_PATTERN} 24 | {INIT_VAL_PATTERN} 25 | {SEMICOLON_PATTERN} 26 | {OPTIONAL_PROPS_PATTERN} 27 | """, 28 | re.VERBOSE, 29 | ) 30 | # Example : uniform vec3 LIGHTS[12]; // section=1; label=Lights 31 | array = re.compile( 32 | rf""" 33 | {OPTIONAL_UNIFORM_PATTERN} 34 | {TYPE_PATTERN} 35 | {NAME_PATTERN} 36 | {ARRAY_LENGTH_PATTERN} 37 | {SEMICOLON_PATTERN} 38 | {OPTIONAL_PROPS_PATTERN} 39 | """, 40 | re.VERBOSE, 41 | ) 42 | 43 | # Example : vec4 Color; // section=1; label=Lights; style=Color 44 | standard = re.compile( 45 | rf""" 46 | {OPTIONAL_UNIFORM_PATTERN} 47 | {TYPE_PATTERN} 48 | {NAME_PATTERN} 49 | {SEMICOLON_PATTERN} 50 | {OPTIONAL_PROPS_PATTERN} 51 | """, 52 | re.VERBOSE, 53 | ) 54 | # Example : //--- label=Light Controls; section=1 55 | ui = re.compile( 56 | rf""" 57 | {UI_TYPE_PATTERN} 58 | {UI_PROPS_PATTERN} 59 | """, 60 | re.VERBOSE, 61 | ) -------------------------------------------------------------------------------- /script/py/parse/glsl_code_analyzer/analyzer_uniform_parser.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Union 3 | from analyzer_line_matcher import GLSLLineMatcher 4 | from analyzer_code_splitter import GLSLCodeSplitter 5 | from str_line_utils import ( 6 | split_string_at_characters, 7 | JSONableMutable, 8 | try_parse_mutable, 9 | cast_value, 10 | ) 11 | from gl_types import GLSL_TYPE_MAP, LineMatch, MatchPropTuple 12 | from str_line_utils import build_TD_par_name 13 | 14 | 15 | class GLSLCodeParser(ABC): 16 | @abstractmethod 17 | def __init__(self, code: str): 18 | self.code = code 19 | return 20 | 21 | @abstractmethod 22 | def get_items(self) -> dict: 23 | return 24 | 25 | 26 | class GLSLUniformCodeParser(GLSLCodeParser): 27 | @staticmethod 28 | def __get_variable_prop(any_str: str) -> JSONableMutable: 29 | return cast_value(try_parse_mutable(any_str)) 30 | 31 | def __init__( 32 | self, 33 | code: str, 34 | line_matcher: GLSLLineMatcher = GLSLLineMatcher, 35 | code_splitter: GLSLCodeSplitter = GLSLCodeSplitter, 36 | ): 37 | self.__line_matcher = line_matcher 38 | self.__code_splitter = code_splitter 39 | self.code = code 40 | return 41 | 42 | def __parse_prop_from_string(self, prop_str: str) -> Union[MatchPropTuple, None]: 43 | key, value_str = split_string_at_characters(prop_str.strip(), ["=", ":"], 1) 44 | try: 45 | value = self.__get_variable_prop(value_str) 46 | return key, value 47 | except ValueError as e: 48 | print(f"Error casting value for property '{key}': {e}") 49 | return None 50 | 51 | def __parse_dict_from_props(self, any_str: str) -> dict: 52 | props_list = split_string_at_characters(any_str, [";"]) 53 | props = {} 54 | 55 | for any_str in props_list: 56 | prop = self.__parse_prop_from_string(any_str) 57 | if prop is not None: 58 | key, value = prop 59 | props[key] = value 60 | 61 | return props 62 | 63 | def __get_parameter_props(self, props_str: str) -> dict: 64 | return self.__parse_dict_from_props(props_str) if props_str else {} 65 | 66 | def __set_variable_item(self, line_match: LineMatch, code_line: str) -> dict: 67 | match = line_match["match"] 68 | item_object_from_match = self.__build_item_from_match(match) 69 | item_object_update = {"gl_page": line_match["gl_page"], "code": code_line} 70 | return {**item_object_from_match, **item_object_update} 71 | 72 | def __set_header_item(self, line_match: LineMatch, code_line: str) -> dict: 73 | match = line_match["match"] 74 | item_object_from_match = self.__build_item_from_match(match) 75 | gl_type = GLSL_TYPE_MAP["//---"]["style"] 76 | name = build_TD_par_name(item_object_from_match["props"]["label"], "H") 77 | item_object_update = { 78 | "gl_page": line_match["gl_page"], 79 | "code": code_line, 80 | "gl_type": gl_type, 81 | "name": name, 82 | } 83 | return {**item_object_from_match, **item_object_update} 84 | 85 | def __build_item_from_match(self, line_match: dict) -> dict: 86 | return { 87 | group: self.__get_parameter_props(line_match.group(group)) 88 | if group == "props" 89 | else self.__get_variable_prop(line_match.group(group)) 90 | for group in line_match.groupdict() 91 | } 92 | 93 | def get_items(self) -> dict: 94 | line_items = {} 95 | code_lines = self.__code_splitter(self.code).run() 96 | for code_line in code_lines: 97 | line_match = self.__line_matcher(code_line).run() 98 | if line_match["gl_page"] == "UI": 99 | line_object = self.__set_header_item(line_match, code_line) 100 | else: 101 | line_object = self.__set_variable_item(line_match, code_line) 102 | key = line_object["name"] 103 | line_items[key] = line_object 104 | return line_items 105 | -------------------------------------------------------------------------------- /script/py/parse/glsl_code_analyzer/analyzer_utils.py: -------------------------------------------------------------------------------- 1 | 2 | def get_parameters_target_op(comp) -> COMP: 3 | return op(comp.par.Host) 4 | 5 | 6 | def get_glsl_target_op(comp) -> glslTOP or glslmultiTOP: 7 | return op(comp.par.Targetgl) 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /script/py/parse/glsl_parameter_manager/manager_actions.py: -------------------------------------------------------------------------------- 1 | class ManagerActions: 2 | def __init__(self, items: dict, host: COMP, glslTOP: glslTOP): 3 | self.glslTOP = glslTOP 4 | self.host = host 5 | self.items = items 6 | 7 | def eval_actions(self): 8 | actions = {"update": [], "create": [], "destroy": []} 9 | self.__eval_destroy_actions(actions) 10 | self.__eval_create_update_actions(actions) 11 | return actions 12 | 13 | def __eval_destroy_actions(self, actions): 14 | for par in self.host.customPars: 15 | if par.name not in self.items: 16 | self.__set_destroy_action(actions, par) 17 | 18 | def __eval_create_update_actions(self, actions): 19 | for k in self.items: 20 | if isinstance(self.host.par[k], Par): 21 | self.__set_update_action(actions, k) 22 | else: 23 | self.__set_create_action(actions, k) 24 | 25 | def __set_destroy_action(self, actions, parK): 26 | print(f"Destroy parameter {parK.name}") 27 | actions["destroy"].append({"item": None, "par": parK}) 28 | 29 | def __set_create_action(self, actions, k): 30 | print(f"Create parameter {k}") 31 | actions["create"].append({"item": self.items[k], "par": None}) 32 | 33 | def __set_update_action(self, actions, k): 34 | print(f"Update parameter {k}") 35 | actions["update"].append({"item": self.items[k], "par": self.host.par[k]}) 36 | -------------------------------------------------------------------------------- /script/py/parse/glsl_parameter_manager/manager_classes.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from gl_types import GLItemPage 3 | 4 | class GLItem(ABC): 5 | def __init__(self, **item_attrs : dict): 6 | self.code: str = item_attrs.get('code','') 7 | self.gl_page: GLItemPage = item_attrs.get('gl_page','VECTOR') 8 | self.gl_type: str = item_attrs.get('gl_type', 'float') 9 | self.name: str = item_attrs.get('name','Parameter') 10 | self.props: dict = item_attrs.get('props', {}) 11 | self.par: Par = item_attrs.get('par') 12 | self.host_comp: COMP = item_attrs.get('host_comp') 13 | self.bind_op: glslmultiTOP or glslTOP = item_attrs.get('bind_op') 14 | 15 | @abstractmethod 16 | def createPar(self) -> Par or None: 17 | print("A parameter was created") 18 | 19 | @abstractmethod 20 | def destroyPar(self, par: Par) -> bool: 21 | print("A parameter was destroyed") 22 | 23 | @abstractmethod 24 | def updatePar(self, par: Par) -> Par: 25 | print("A parameter was updated") 26 | 27 | 28 | class GLVectorPageItem(GLItem): 29 | def __init__(self, **item_attrs : dict): 30 | super().__init__(**item_attrs) 31 | 32 | 33 | class GLArrayPageItem(GLItem): 34 | def __init__(self, **item_attrs : dict): 35 | self.array_length = item_attrs.pop('array_length', 1) 36 | super().__init__(**item_attrs) 37 | 38 | 39 | class GLMatrixPageItem(GLItem): 40 | def __init__(self, **item_attrs : dict): 41 | super().__init__(**item_attrs) 42 | 43 | 44 | class GLAtomicPageItem(GLItem): 45 | def __init__(self, **item_attrs): 46 | super().__init__(**item_attrs) 47 | 48 | 49 | class GLConstantPageItem(GLItem): 50 | def __init__(self, **item_attrs : dict): 51 | self.init_value = item_attrs.pop('init_value', 0) 52 | super().__init__(**item_attrs) 53 | 54 | 55 | class GLUIItem(GLItem): 56 | def __init__(self, **item_attrs : dict): 57 | super().__init__(**item_attrs) 58 | -------------------------------------------------------------------------------- /script/py/parse/glsl_parameter_manager/manager_exec.py: -------------------------------------------------------------------------------- 1 | 2 | host = parent.ParameterManager.ext 3 | 4 | def set_target_operators(): 5 | return 6 | 7 | def update(): 8 | return 9 | -------------------------------------------------------------------------------- /script/py/parse/glsl_parameter_manager/manager_ext.py: -------------------------------------------------------------------------------- 1 | 2 | class GLSLParameterManager: 3 | def __init__( 4 | self, 5 | ownerComp: COMP, 6 | ): 7 | self.owner = ownerComp 8 | self.host = None 9 | self.glslTOP = None 10 | self.input = ownerComp.par.Source.eval().jsonObject 11 | self.actions = {} 12 | self.factory = None 13 | 14 | def Set_target_operators(self, hostCOMP: COMP, glslTOP: glslTOP): 15 | pass 16 | def Update_parameters(self): 17 | pass 18 | -------------------------------------------------------------------------------- /script/py/parse/glsl_parameter_manager/manager_factory.py: -------------------------------------------------------------------------------- 1 | import manager_classes 2 | 3 | GL_PAGE_TO_CLASS = { 4 | "UI": manager_classes.GLUIItem, 5 | "VECTOR": manager_classes.GLVectorPageItem, 6 | "ARRAY": manager_classes.GLArrayPageItem, 7 | "MATRIX": manager_classes.GLMatrixPageItem, 8 | "CONSTANT": manager_classes.GLConstantPageItem, 9 | } 10 | 11 | 12 | class ManagerFactory: 13 | def __init__(self, ownerComp: COMP, host: COMP, glslTOP: glslTOP): 14 | self.owner = ownerComp 15 | self.current_items = {} 16 | self.host = host 17 | self.glslTOP = glslTOP 18 | self.actions = {} 19 | 20 | def set_actions(self, actions: dict) -> dict: 21 | self.actions = actions 22 | return self 23 | 24 | def run(self): 25 | self.__run_actions() 26 | 27 | def __run_actions(self): 28 | for item in self.actions["destroy"]: 29 | self.__destroy_item(item) 30 | 31 | for item in self.actions["create"]: 32 | self.__create_item(item) 33 | 34 | for item in self.actions["update"]: 35 | self.__update_item(item) 36 | 37 | def __create_item(self, item: dict): 38 | print(f"CREATE : {item['item']['gl_page']}") 39 | 40 | def __destroy_item(self, item: dict): 41 | pass 42 | 43 | def __update_item(self, item: dict): 44 | pass 45 | -------------------------------------------------------------------------------- /script/py/parse/glsl_parameter_manager/manager_on_input_change.py: -------------------------------------------------------------------------------- 1 | import manager_exec 2 | 3 | 4 | def onCellChange(_, __, ___): 5 | manager_exec.update() 6 | return 7 | -------------------------------------------------------------------------------- /script/py/parse/glsl_parameter_manager/manager_on_target_change.py: -------------------------------------------------------------------------------- 1 | import manager_exec 2 | 3 | def onValueChange(_, __): 4 | manager_exec.set_target_operators() 5 | manager_exec.update() 6 | return 7 | -------------------------------------------------------------------------------- /script/py/parse/io_parser.py: -------------------------------------------------------------------------------- 1 | from re import sub 2 | 3 | from inspect import cleandoc 4 | from pydoc import locate 5 | from ast import literal_eval 6 | import string 7 | 8 | # Corresponding table of declared types vs ParmTypes 9 | validTypePattern = { 10 | "//---": "header", 11 | "//str:": "str", 12 | "//op:": "op", 13 | "//chop:": "chop", 14 | "//top:": "top", 15 | "//mat:": "mat", 16 | "//comp:": "comp", 17 | "//file:": "file", 18 | "//folder:": "folder", 19 | "float": "float", 20 | "int": "int", 21 | "uint": "int", 22 | "atomic_uint": "int", 23 | "bool": "int", 24 | "vec2": "vec2", 25 | "vec3": "vec3", 26 | "vec4": "vec4", 27 | "ivec2": "ivec2", 28 | "ivec3": "ivec3", 29 | "ivec4": "ivec4", 30 | "uvec2": "ivec2", 31 | "uvec3": "ivec3", 32 | "uvec4": "ivec4", 33 | "bvec2": "ivec2", 34 | "bvec3": "ivec3", 35 | "bvec4": "ivec4", 36 | "mat2": "chop", 37 | "mat3": "chop", 38 | "mat4": "chop", 39 | } 40 | 41 | # Props and target types 42 | commonProps = { 43 | "ptype": {"type": str, "default": "float"}, 44 | "vtype": {"type": str, "default": "float"}, 45 | "gl_type": {"type": str, "default": "uni"}, 46 | "array": {"type": int, "default": 0}, 47 | "arraytype": {"type": str, "default": "uniformarray"}, 48 | "arraysamples": {"type": int, "default": 0}, 49 | "acinittype": {"type": int, "default": "0"}, 50 | "acinitval": {"type": str, "default": "val"}, 51 | "typesize": {"type": int, "default": 1}, 52 | "label": {"type": str, "default": "Header"}, 53 | "section": {"type": int, "default": 0}, 54 | "order": {"type": int, "default": 0}, 55 | "readonly": {"type": int, "default": 0}, 56 | "enable": {"type": int, "default": 1}, 57 | "enableExpr": {"type": str, "default": ""}, 58 | "help": {"type": str, "default": ""}, 59 | "style": {"type": str, "default": "Float"}, 60 | } 61 | 62 | pythonProps = { 63 | "expr": {"type": str, "default": ""}, 64 | "bindExpr": {"type": str, "default": ""}, 65 | } 66 | 67 | numberProps = { 68 | "chop": {"type": str, "default": ""}, 69 | "min": {"type": float, "default": 0}, 70 | "max": {"type": float, "default": 1}, 71 | "default": {"type": float, "default": 0}, 72 | } 73 | 74 | toggleProps = { 75 | "menu": {"type": list, "default": ""}, 76 | "toggle": {"type": int, "default": 0}, 77 | "pulse": {"type": int, "default": 0}, 78 | } 79 | 80 | matrixProps = { 81 | "chop": {"type": str, "default": ""}, 82 | } 83 | 84 | stringProps = { 85 | "file": {"type": str, "default": ""}, 86 | "folder": {"type": str, "default": ""}, 87 | "str": {"type": str, "default": ""}, 88 | "strmenu": {"type": str, "default": ""}, 89 | "python": {"type": str, "default": ""}, 90 | "comp": {"type": str, "default": ""}, 91 | "object": {"type": str, "default": ""}, 92 | "panelcomp": {"type": str, "default": ""}, 93 | "op": {"type": str, "default": ""}, 94 | "top": {"type": str, "default": ""}, 95 | "chop": {"type": str, "default": ""}, 96 | "dat": {"type": str, "default": ""}, 97 | "mat": {"type": str, "default": ""}, 98 | } 99 | 100 | allProps = ( 101 | commonProps | pythonProps | numberProps | toggleProps | matrixProps | stringProps 102 | ) 103 | 104 | 105 | def setParmProps(stringList: str): 106 | result = [] 107 | # For each line 108 | for line in stringList: 109 | 110 | # Get orignal line number 111 | id = line["id"] 112 | # Get original text line 113 | text = line["text"] 114 | # Get Parameter type 115 | parmTypeItem = checkParmType(text) 116 | 117 | parmVTypeName = parmTypeItem[0] 118 | parmTypeName = parmTypeItem[1] 119 | # Is a constant parameter ? 120 | parmIsConst = text.startswith("const") 121 | # Props of corresponding type 122 | parmTypeAvailableProps = getAvailablePropsOfParmType(parmTypeName) 123 | parmFullName = "" 124 | parmName = parmFullName 125 | 126 | # IF HEADER PARM 127 | if parmTypeName == "header": 128 | parmGLtypeName = "None" 129 | # Set default Header Name 130 | parmFullName = f"UntitledHeader{id}" 131 | parmName = cleanName(parmFullName) 132 | # Get individual key/value pair ["key=value"] 133 | specifiedProps = cleanStrProps(text, True) 134 | 135 | # IF CONST PARM 136 | elif parmIsConst: 137 | 138 | # Get name 139 | parmFullName = text.split(";")[0].split(" ")[2].split("=")[0] 140 | parmName = cleanName(parmFullName).rstrip(string.digits) 141 | parmGLtypeName = "const" 142 | # Get property list as string 143 | propList = text.split(";", 1) 144 | # Remove spaces 145 | specifiedProps = propList[1] 146 | # Get individual key/value pair ["key=value"] if specified props 147 | if "//" in specifiedProps: 148 | specifiedProps = cleanStrProps(specifiedProps) 149 | else: 150 | specifiedProps = [] 151 | 152 | # IF ARRAY 153 | elif text.split(";")[0].split(" ")[1].endswith("]"): 154 | 155 | # Get name 156 | parmFullName = text.split(";")[0].split(" ")[1] 157 | parmName = cleanName(parmFullName.split("[")[0]) 158 | parmGLtypeName = "chop" 159 | # Get property list as string 160 | propList = text.split(";", 1) 161 | # Remove spaces 162 | specifiedProps = propList[1] 163 | # Get individual key/value pair ["key=value"] if specified props 164 | if "//" in specifiedProps: 165 | specifiedProps = cleanStrProps(specifiedProps) 166 | 167 | else: 168 | specifiedProps = [] 169 | 170 | # IF NORMAL PARM 171 | else: 172 | 173 | # Get name 174 | parmFullName = text.split(";")[0].split(" ")[1] 175 | parmName = cleanName(parmFullName) 176 | parmGLtypeName = "None" 177 | if parmVTypeName.startswith("atomic"): 178 | parmGLtypeName = "ac" 179 | if parmVTypeName.startswith("mat"): 180 | parmGLtypeName = "mat" 181 | 182 | # Get property list as string 183 | propList = text.split(";", 1) 184 | # Remove spaces 185 | specifiedProps = propList[1] 186 | # Get individual key/value pair ["key=value"] if specified props 187 | if "//" in specifiedProps: 188 | specifiedProps = cleanStrProps(specifiedProps) 189 | else: 190 | specifiedProps = [] 191 | 192 | parmProps = setProps( 193 | id, 194 | parmFullName, 195 | parmName, 196 | parmTypeName, 197 | parmVTypeName, 198 | parmGLtypeName, 199 | specifiedProps, 200 | parmTypeAvailableProps, 201 | allProps, 202 | ) 203 | result.append(parmProps) 204 | 205 | return result 206 | 207 | 208 | def cleanName(strName): 209 | # Replace non compatible chars by _, make lowercase and capitalize first letter 210 | return sub(r"\W+|^(?=\d)", "", strName).lower().capitalize() 211 | 212 | 213 | def cleanStrProps(strProp, ISCOMMENTLINEPARM=False): 214 | 215 | # Leave spaces in str properties 216 | preservedSpaceProps = ("help", "label", "expr", "bindExpr", "enableExpr") 217 | # GLSL compatible parm 218 | prop = ( 219 | [x.strip() for x in strProp.split("//---")[1].split(";")] 220 | if ISCOMMENTLINEPARM 221 | else [x.strip() for x in strProp.split("//")[1].split(";")] 222 | ) 223 | # Conditionnal remove space 224 | 225 | prop = [ 226 | x 227 | if x.startswith(preservedSpaceProps) 228 | else x.translate(str.maketrans("", "", string.whitespace)) 229 | for x in prop 230 | ] 231 | 232 | return prop 233 | 234 | 235 | def checkParmType(line: str): 236 | # Remove All spaces 237 | currentLine = line.translate(str.maketrans("", "", string.whitespace)) 238 | 239 | # Remove Start and split each specified prop 240 | specifiedProps = currentLine.split(";") 241 | 242 | # If const offset string check after const keyword and check type 243 | if specifiedProps[0].startswith("const"): 244 | for key, value in validTypePattern.items(): 245 | if specifiedProps[0].startswith(key, len("const"), len(key) + len("const")): 246 | return [key, value] 247 | 248 | # Else check type 249 | else: 250 | for key, value in validTypePattern.items(): 251 | if specifiedProps[0].startswith(key, 0, len(key)): 252 | return [key, value] 253 | 254 | 255 | def getAvailablePropsOfParmType(parmType): 256 | 257 | # Set common properties 258 | parmTypeAvailableProps = {**commonProps} 259 | 260 | # Simple integers, menu, toggle parm types 261 | if parmType in ["int"]: 262 | return {**commonProps, **numberProps, **toggleProps, **pythonProps} 263 | 264 | # Float and vectors parm types 265 | elif parmType in [ 266 | "float", 267 | "vec2", 268 | "vec3", 269 | "vec4", 270 | "ivec2", 271 | "ivec3", 272 | "ivec4", 273 | "uvec2", 274 | "uvec3", 275 | "uvec4", 276 | "bvec2", 277 | "bvec3", 278 | "bvec4", 279 | ]: 280 | return {**commonProps, **numberProps, **pythonProps} 281 | 282 | # OP, files and strings parm types 283 | elif parmType in ["chop"]: 284 | return {**commonProps, **matrixProps, **pythonProps} 285 | 286 | else: 287 | return parmTypeAvailableProps 288 | 289 | 290 | def removeEmpty(stringList: str): 291 | 292 | # Init 293 | result = [] 294 | 295 | for i, line in enumerate(stringList): 296 | 297 | # String is empty 298 | if len(line) < 1: 299 | continue 300 | 301 | # String is comment 302 | elif line.strip().startswith(("//", "/*")) and not line.strip().startswith( 303 | "//---" 304 | ): 305 | continue 306 | 307 | # String is valid 308 | else: 309 | result.append({"text": line, "id": i + 1}) 310 | 311 | return result 312 | 313 | 314 | def setProps( 315 | id, 316 | parmFullName: str, 317 | parmName: str, 318 | parmTypeName: str, 319 | parmVTypeName: str, 320 | parmGLTypeName: str, 321 | specifiedProps, 322 | parmTypeAvailableProps, 323 | parmAllProps, 324 | ): 325 | 326 | # INITIAL PARAMETER SETTINGS 327 | parmFinalName = parmName 328 | 329 | parmAllKeysDefault = {x[0]: x[1]["default"] for x in parmAllProps.items()} 330 | 331 | parmKeysVals = { 332 | "ptype": parmTypeName, 333 | "vtype": parmVTypeName, 334 | "gl_type": parmGLTypeName, 335 | "order": id, 336 | "label": parmFullName, 337 | "style": "Float", 338 | "typesize": 1, 339 | } 340 | 341 | # Array Type 342 | if parmFullName.endswith("]"): 343 | parmKeysVals["ptype"] = "chop" 344 | parmKeysVals["label"] = parmFinalName 345 | parmKeysVals["array"] = 1 346 | arraySamples = int(parmFullName.split("[")[1].split("]")[0]) 347 | parmKeysVals["arraysamples"] = arraySamples 348 | parmKeysVals["style"] = "CHOP" 349 | 350 | else: 351 | parmKeysVals["array"] = 0 352 | parmKeysVals["arraysamples"] = None 353 | parmKeysVals["arraytype"] = None 354 | 355 | # Vector Types 356 | if "vec" in parmTypeName: 357 | parmKeysVals["typesize"] = int(parmTypeName[-1]) 358 | parmKeysVals["default"] = [ 359 | float(parmAllKeysDefault["default"]) 360 | ] * parmKeysVals["typesize"] 361 | if parmKeysVals["typesize"] == 3: 362 | parmKeysVals["style"] = "RGB" 363 | elif parmKeysVals["typesize"] == 4: 364 | parmKeysVals["style"] = "RGBA" 365 | 366 | # Int types 367 | if parmTypeName.startswith(("int", "ivec", "bvec", "uvec")): 368 | parmKeysVals["style"] = "Int" 369 | 370 | # Matrix types 371 | if parmVTypeName.startswith("mat"): 372 | parmKeysVals["style"] = "CHOP" 373 | 374 | # Atomic types 375 | if parmGLTypeName != "ac": 376 | parmKeysVals["acinitval"] = None 377 | parmKeysVals["acinittype"] = None 378 | 379 | # OVERRIDE PROPERTIES 380 | # FOR EACH SPECIFIED PROPS 381 | for currentProp in specifiedProps: 382 | # Try splitting property name and property value 383 | try: 384 | propRaw = currentProp.split("=") 385 | propName = propRaw[0] 386 | propVal = propRaw[1] 387 | isValidPropName = None 388 | 389 | except Exception: 390 | continue 391 | 392 | ## CHECK PROPERTY NAME ## 393 | # Try access current property from available properties dict 394 | try: 395 | isValidPropName = True 396 | targetProp = parmTypeAvailableProps[propName] 397 | 398 | except Exception: 399 | isValidPropName = False 400 | error = cleandoc( 401 | f""" 402 | 403 | ================================= 404 | LINE {id} -> Property Error on "{parmFullName}" 405 | ================================= 406 | The '{propName}' property does not exist on {parmTypeName.capitalize()} ParmType 407 | This property will be ignore 408 | """ 409 | ) 410 | 411 | print(error) 412 | continue 413 | 414 | ## CHECK PROPERTY VALUE ## 415 | # If specified property has a valid name 416 | if isValidPropName: 417 | if propName == "menu": 418 | propVal = [str(x) for x in literal_eval(propVal)] 419 | parmKeysVals[propName] = propVal 420 | parmKeysVals["style"] = "Menu" 421 | 422 | elif parmTypeName == "header" and propName == "label": 423 | parmFinalName = cleanName(f"h{propVal}").lower().capitalize() 424 | parmKeysVals[propName] = propVal 425 | parmKeysVals["style"] = "Header" 426 | 427 | elif propName == "default": 428 | if parmKeysVals["typesize"] > 1: 429 | try: 430 | propVal = [float(x) for x in literal_eval(propVal)] 431 | parmKeysVals[propName] = propVal 432 | except Exception: 433 | try: 434 | propVal = [float(propVal)] 435 | parmKeysVals["typesize"] 436 | parmKeysVals[propName] = propVal 437 | except Exception: 438 | propVal = [ 439 | float(parmAllKeysDefault["default"]) 440 | ] * parmKeysVals["typesize"] 441 | parmKeysVals[propName] = propVal 442 | else: 443 | try: 444 | parmKeysVals[propName] = propVal 445 | except Exception: 446 | parmKeysVals[propName] = float(parmAllKeysDefault["default"]) 447 | 448 | elif parmKeysVals["array"] == 1 and propName == "arraytype": 449 | parmKeysVals["arraytype"] = cleanName(propVal).lower() 450 | 451 | elif propName in [ 452 | "op", 453 | "top", 454 | "file", 455 | "folder", 456 | "dat", 457 | "mat", 458 | "chop", 459 | "comp", 460 | ] and parmKeysVals["style"].startswith( 461 | ( 462 | "SOP", 463 | "PanelCOMP", 464 | "Python", 465 | "TOP", 466 | "MAT", 467 | "COMP", 468 | "CHOP", 469 | "File", 470 | "Folder", 471 | "Str", 472 | "StrMenu", 473 | ) 474 | ): 475 | key = parmKeysVals["style"].lower() 476 | parmKeysVals[key] = propVal 477 | parmKeysVals["default"] = propVal 478 | 479 | elif propName == "acinitval" and propVal == "chop": 480 | parmKeysVals["style"] = "CHOP" 481 | parmKeysVals[propName] = propVal 482 | 483 | else: 484 | targetPropTypeName = targetProp["type"].__name__ 485 | 486 | try: 487 | c = locate(targetPropTypeName) 488 | propVal = c(propVal) 489 | parmKeysVals[propName] = propVal 490 | 491 | # Else display detailed error 492 | except Exception: 493 | error = cleandoc( 494 | f""" 495 | 496 | ================================= 497 | LINE {id} -> Value Error on "{parmFullName}" 498 | ================================= 499 | The '{propVal}' value on '{propName}' property is wrong type 500 | Expected type is {targetProp['type'].__name__} 501 | This value will be reset to default : {targetProp['default']} 502 | """ 503 | ) 504 | 505 | print(error) 506 | continue 507 | 508 | return {parmFinalName: parmAllKeysDefault | parmKeysVals} 509 | 510 | 511 | def getParmList(dat: DAT): 512 | # Init 513 | rawText = None 514 | rawLines = [] 515 | # DAT is a scriptDAT type (multi inputs) 516 | rawText = dat.inputs[0].text if dat.type == "script" else dat.text 517 | # Split lines and write out to result 518 | rawLines = removeEmpty(rawText.splitlines()) 519 | return setParmProps(rawLines) 520 | -------------------------------------------------------------------------------- /script/py/parse/io_parserExec.py: -------------------------------------------------------------------------------- 1 | 2 | import io_parserExt as parse 3 | from pprint import pprint 4 | 5 | def onTableChange(dat): 6 | code = dat.text 7 | analyzer = parse.GLSLCodeAnalyzer(code) 8 | variables = analyzer.Run() 9 | # [print(x) for x in variables] 10 | return 11 | -------------------------------------------------------------------------------- /script/py/preset/load.py: -------------------------------------------------------------------------------- 1 | 2 | from TDJSON import addParametersFromJSONDict 3 | from TDFunctions import getCustomPage 4 | 5 | 6 | def loadPreset(presetDict, comp=parent.Comp): 7 | feedbacksRep = comp.op("FEEDBACK_REPLICATOR") 8 | ntarUniforms = comp.op("BUILD_GLSL_CODE/UNIFORMS_AS_TEXT") 9 | ntarFunctions = comp.op("BUILD_GLSL_CODE/FUNCTIONS") 10 | ntarOutputs = comp.op("BUILD_GLSL_CODE/OUTPUTS") 11 | ntarCode = comp.op("BUILD_GLSL_CODE/CODE") 12 | 13 | page = getCustomPage(comp, "Controls") 14 | inputPage = getCustomPage(comp, "Inputs") 15 | outputPage = getCustomPage(comp, "Outputs") 16 | ioPages = [inputPage, outputPage] 17 | for iopage in ioPages: 18 | for iopar in iopage: 19 | iopar.val = iopar.default 20 | 21 | if page: 22 | page.destroy() 23 | comp.appendCustomPage("Controls") 24 | comp.sortCustomPages("Controls", "Code", "Inputs", "Outputs", "GLSL", "Globals") 25 | 26 | currentPreset = presetDict 27 | 28 | # Make old presets compatible 29 | if "codetabs" in currentPreset: 30 | uniformsCode = currentPreset["codetabs"]["inputs"] 31 | functionsCode = currentPreset["codetabs"]["function"] 32 | outputsCode = currentPreset["codetabs"]["outputs"] 33 | mainCode = currentPreset["codetabs"]["main"] 34 | 35 | # Old preset scheme 36 | else: 37 | uniformsCode = currentPreset["UNIFORMS_CODE"] 38 | functionsCode = currentPreset["FUNCTIONS_CODE"] 39 | outputsCode = currentPreset["OUTPUTS_CODE"] 40 | mainCode = currentPreset["MAIN_CODE"] 41 | 42 | if "pars" in currentPreset: 43 | parameters = currentPreset["pars"] 44 | 45 | # Old preset scheme 46 | else: 47 | parameters = currentPreset 48 | del parameters["UNIFORMS_CODE"] 49 | del parameters["FUNCTIONS_CODE"] 50 | del parameters["OUTPUTS_CODE"] 51 | del parameters["MAIN_CODE"] 52 | 53 | loadCode(ntarUniforms, uniformsCode) 54 | loadCode(ntarFunctions, functionsCode) 55 | loadCode(ntarOutputs, outputsCode) 56 | loadCode(ntarCode, mainCode) 57 | addParametersFromJSONDict(comp, parameters, setValues=True, newAtEnd=False) 58 | 59 | feedbacksRep.par.recreateall.pulse() 60 | return 61 | 62 | 63 | def loadCode(targetOP, textContent): 64 | targetOP.par.syncfile = 0 65 | targetOP.clear() 66 | targetOP.write(textContent) 67 | -------------------------------------------------------------------------------- /script/py/preset/save.py: -------------------------------------------------------------------------------- 1 | 2 | import TDJSON 3 | import TDFunctions 4 | from pathlib import Path, PurePosixPath 5 | 6 | # Par Node 7 | comp = parent.Comp 8 | 9 | 10 | def buildPreset(fromComp): 11 | 12 | # Get Pages 13 | controlsPage = TDFunctions.getCustomPage(fromComp, "Controls") 14 | inputsPage = TDFunctions.getCustomPage(fromComp, "Inputs") 15 | outputsPage = TDFunctions.getCustomPage(fromComp, "Outputs") 16 | glslPage = TDFunctions.getCustomPage(fromComp, "GLSL") 17 | globalPage = TDFunctions.getCustomPage(fromComp, "Globals") 18 | 19 | # Build Parms Settings 20 | extraKeys = [ 21 | "mode", 22 | "val", 23 | "expr", 24 | "bindExpr", 25 | "isDefault", 26 | "page", 27 | "style", 28 | "name", 29 | ] 30 | 31 | controlsParDict = TDJSON.pageToJSONDict( 32 | controlsPage, forceAttrLists=True, extraAttrs=extraKeys 33 | ) 34 | inputsParDict = TDJSON.pageToJSONDict( 35 | inputsPage, forceAttrLists=True, extraAttrs=extraKeys 36 | ) 37 | outputsParDict = TDJSON.pageToJSONDict( 38 | outputsPage, forceAttrLists=True, extraAttrs=extraKeys 39 | ) 40 | glslParDict = TDJSON.pageToJSONDict( 41 | glslPage, forceAttrLists=True, extraAttrs=extraKeys 42 | ) 43 | globalParDict = TDJSON.pageToJSONDict( 44 | globalPage, forceAttrLists=True, extraAttrs=extraKeys 45 | ) 46 | 47 | # Get Code Pages 48 | functionsText = iop.function.text 49 | uniformsText = iop.uniform.text 50 | outputsText = iop.outputs.text 51 | mainText = iop.code.text 52 | 53 | # Init dict containers 54 | dicts = { 55 | "ctrls": controlsParDict, 56 | "in": inputsParDict, 57 | "out": outputsParDict, 58 | "gl": glslParDict, 59 | "global": globalParDict, 60 | } 61 | currentPreset = {"codetabs": {}, "pars": {}} 62 | # Keys to keep for static parameters 63 | inOutSavedkeys = [ 64 | "val", 65 | "expr", 66 | "bindExpr", 67 | "mode", 68 | "name", 69 | "style", 70 | "page", 71 | "label", 72 | "size", 73 | ] 74 | 75 | for pageKey, pageDict in dicts.items(): 76 | 77 | if pageKey == "ctrls": 78 | for parmKey, parmValue in pageDict.items(): 79 | currentPreset["pars"][parmKey] = parmValue 80 | 81 | if pageKey in ["in", "out", "gl", "global"]: 82 | for parmKey, parmValue in pageDict.items(): 83 | if parmValue["isDefault"] == True and parmKey != "Glmode": 84 | continue 85 | if parmKey.startswith( 86 | ("Inputsinputfeedbacksource", "Inputsinputfeedbacksource") 87 | ): 88 | # Keep only non-default parms 89 | toRemoveKeys = [ 90 | x 91 | for x in list(parmValue.keys()) 92 | if x not in inOutSavedkeys + ["menuNames", "menuLabels"] 93 | ] 94 | 95 | else: 96 | toRemoveKeys = [ 97 | x for x in list(parmValue.keys()) if x not in inOutSavedkeys 98 | ] 99 | [parmValue.pop(x, None) for x in toRemoveKeys] 100 | currentPreset["pars"][parmKey] = parmValue 101 | 102 | # Merge Code blocks to preset 103 | currentPreset["codetabs"]["inputs"] = uniformsText 104 | currentPreset["codetabs"]["function"] = functionsText 105 | currentPreset["codetabs"]["outputs"] = outputsText 106 | currentPreset["codetabs"]["main"] = mainText 107 | 108 | return currentPreset 109 | 110 | 111 | def onSelect(info): 112 | if info["button"] != "Save": 113 | return 114 | currentPreset = buildPreset(comp) 115 | name = str(PurePosixPath(info["enteredText"]).name) 116 | path = parent.Comp.par.Codeuserpresetpath 117 | relpath = "" 118 | 119 | try: 120 | relpath = op("SAVE_TO_PATH")[1, "relpath"].val 121 | path = f"{path}/{relpath}" 122 | except: 123 | pass 124 | if "/" in str(PurePosixPath(info["enteredText"])): 125 | folder = PurePosixPath(f"{path}/{PurePosixPath(info['enteredText'])}").parent 126 | Path(str(folder)).mkdir(parents=True, exist_ok=True) 127 | path = folder 128 | 129 | with open(f"{path}/{name}.json", "w") as f: 130 | f.write(TDJSON.jsonToText(currentPreset)) 131 | -------------------------------------------------------------------------------- /script/py/replicator/feedback.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | glsln = op("glsl1") 4 | table = op("FEEDBACK_REPLICATOR/OUT_ACTIVE_INPUTS") 5 | outNodesTable = op("FEEDBACK_REPLICATOR/OUT_OUTPUT_NODES") 6 | 7 | 8 | # Functions 9 | def getOutNodes(table): 10 | outNodes = table.rows() 11 | del outNodes[0] 12 | return [op(n[1].val) for n in outNodes] 13 | 14 | 15 | def disconnectAll(node): 16 | op_ins = node.inputConnectors 17 | for i in range(len(op_ins) - 1): 18 | op_ins[i].disconnect() 19 | return 20 | 21 | 22 | # Callbacks 23 | def onRemoveReplicant(comp, replicant): 24 | glsln.inputConnectors[table.numRows - 1].disconnect() 25 | replicant.destroy() 26 | return 27 | 28 | 29 | def onReplicate(comp, allOps, newOps, template, master): 30 | outs = getOutNodes(outNodesTable) 31 | for i, c in enumerate(allOps): 32 | inputn = op(template[i + 1, 0].val) 33 | try: 34 | c.inputConnectors[0].connect(inputn.outputConnectors[0]) 35 | except Exception: 36 | pass 37 | try: 38 | c.outputConnectors[0].connect(glsln.inputConnectors[i]) 39 | except Exception: 40 | pass 41 | if template[i + 1, "type"] != "feedback": 42 | c.destroy() 43 | else: 44 | for j, node in enumerate(outs): 45 | if outNodesTable[j + 1, "out_name"] == template[i + 1, 3].val: 46 | c.par.top = node.name 47 | c.par.reset.mode = ParMode.BIND 48 | c.par.resetpulse.mode = ParMode.BIND 49 | c.par.reset.bindExpr = ( 50 | f"parent.Comp.par.Inputsinputfeedbackreset{int(i + 1)}" 51 | ) 52 | c.par.resetpulse.bindExpr = ( 53 | f"parent.Comp.par.Inputsinputfeedbackreset{int(i + 1)}" 54 | ) 55 | c.bypass = False 56 | break 57 | return 58 | -------------------------------------------------------------------------------- /script/py/replicator/input.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | glsln = op("glsl1") 4 | feedbacksRep = op("FEEDBACK_REPLICATOR") 5 | outNodesTable = op("FEEDBACK_REPLICATOR/OUT_OUTPUT_NODES") 6 | 7 | 8 | def getOutNodes(table): 9 | outNodes = table.rows() 10 | del outNodes[0] 11 | return [op(n[1].val) for n in outNodes] 12 | 13 | 14 | def onRemoveReplicant(comp, replicant): 15 | # replicant.disconnect() 16 | replicant.destroy() 17 | return 18 | 19 | 20 | def onReplicate(comp, allOps, newOps, template, master): 21 | tarOP = op.pwstorage.fetch(parent.Comp.path, {})["connections"]["inputs"] 22 | outs = getOutNodes(outNodesTable) 23 | 24 | inputs = [] 25 | for i, c in enumerate(allOps): 26 | try: 27 | inOP = ( 28 | c.inputConnectors[0] 29 | .owner.parent() 30 | .inputConnectors[i] 31 | .connections[0] 32 | .owner 33 | ) 34 | inputs.append({"name": template[i + 1, "name"].val, "inputOP": inOP}) 35 | except Exception: 36 | continue 37 | 38 | for i, c in enumerate(allOps): 39 | digit = int(template[i + 1, "name"].val[-1:]) 40 | inOP = None 41 | c.color = parent.Comp.parGroup[f"Inputscolor{digit}"] 42 | inittype = template[i + 1, "inittype"].val 43 | 44 | # Check OP type 45 | if template[i + 1, "type"].val != "feedback": 46 | n = c.changeType(inTOP) if c.type != "inTOP" else c 47 | n.par.label.expr = f"parent.Comp.par.Inputsinputname{digit}" 48 | n.par.label.mode = ParMode.EXPRESSION 49 | if template[i + 1, "type"].val == "feedback": 50 | if inittype == "direct": 51 | n = c.changeType(inTOP) 52 | n.par.label.mode = ParMode.EXPRESSION 53 | n.par.label.expr = f"parent.Comp.par.Inputsinputname{digit}" 54 | elif inittype == "builtin": 55 | n = c.changeType(selectTOP) 56 | builtinPath = template[i + 1, "initbuiltinpath"].val 57 | n.par.top.val = f"BUILTINS_INIT/{builtinPath}" 58 | elif inittype == "output": 59 | n = c.changeType(selectTOP) 60 | outputName = template[i + 1, "initoutputname"].val 61 | for j, node in enumerate(outs): 62 | if outNodesTable[j + 1, "out_name"] == outputName: 63 | n.par.top = node.name 64 | elif inittype == "custom": 65 | n = c.changeType(selectTOP) 66 | customPath = template[i + 1, "initcustompath"].val 67 | n.par.top.val = f"../{customPath}" 68 | 69 | feedbacksRep.par.recreatemissing.pulse(1, frames=4) 70 | 71 | for i, c in enumerate(newOps): 72 | # Init ids, set color and connect to out 73 | digit = int(template[i + 1, "name"].val[-1:]) 74 | index = i 75 | c.outputConnectors[0].connect(glsln.inputConnectors[index]) 76 | 77 | ## Rewiring 78 | toRewireOPS = [] 79 | for i, c in enumerate(allOps): 80 | for j in tarOP: 81 | if j["index"] == i and j["op"] != None: 82 | toRewireOPS.append({"source": c, "tar": j}) 83 | break 84 | 85 | for c in toRewireOPS: 86 | fromn = op(c["source"]) 87 | tarn = op(c["tar"]["op"]) 88 | index = int(c["tar"]["index"]) 89 | fromn.inputConnectors[0].owner.parent().inputConnectors[index].connect( 90 | tarn.outputConnectors[0] 91 | ) 92 | return 93 | -------------------------------------------------------------------------------- /script/py/replicator/output.py: -------------------------------------------------------------------------------- 1 | iName = "renderselect" 2 | iWireName = "cache" 3 | 4 | 5 | def onRemoveReplicant(comp, replicant): 6 | selectn = op(iName + str(replicant.digits)) 7 | cachen = op(iWireName + str(replicant.digits)) 8 | selectn.bypass = True 9 | #cachen.bypass = True 10 | replicant.destroy() 11 | return 12 | 13 | 14 | def onReplicate(comp, allOps, newOps, template, master): 15 | 16 | for c in allOps: 17 | selectn = op(iName + str(c.digits)) 18 | cachen = op(iWireName + str(c.digits)) 19 | selectn.bypass = False 20 | #cachen.bypass = False 21 | digit = template[c.digits, 0].val[-1:] 22 | c.color = parent.Comp.parGroup[f"Outputscolor{str(digit)}"] 23 | c.inputConnectors[0].connect(op(iName + str(c.digits)).outputConnectors[0]) 24 | c.bypass = False 25 | if template[c.digits, "hide"] == 1 and c.type != "nullTOP": 26 | c.changeType(nullTOP) 27 | elif c.type != "outTOP": 28 | n = c.changeType(outTOP) 29 | n.par.label.expr = f"parent.Comp.par.Outputsoutputname{digit}" 30 | return 31 | -------------------------------------------------------------------------------- /script/py/replicator/select.py: -------------------------------------------------------------------------------- 1 | glsln = op("glsl1") 2 | table = op("SELECT_REPLICATOR/OUT_ACTIVE_INPUTS") 3 | outNodesTable = op("SELECT_REPLICATOR/OUT_OUTPUT_NODES") 4 | 5 | # Callbacks 6 | def onRemoveReplicant(comp, replicant): 7 | replicant.destroy() 8 | return 9 | 10 | 11 | def onReplicate(comp, allOps, newOps, template, master): 12 | for i, c in enumerate(allOps): 13 | tarn = op(template[i + 1, 0].val) 14 | tarnType = tarn.type 15 | if ( 16 | template[i + 1, "type"].val != "custom" 17 | and template[i + 1, "fallback"].val == "none" 18 | ): 19 | c.destroy() 20 | elif template[i + 1, "type"].val != "custom": 21 | c.par.top.expr = ( 22 | f"op('BUILTINS_INIT/{template[i+1, 'fallback'].val}')" 23 | ) 24 | if tarnType == "in": 25 | c.outputConnectors[0].connect(tarn.inputConnectors[0]) 26 | else: 27 | c.par.top = template[i + 1, "custom"].val 28 | if tarnType == "in": 29 | c.outputConnectors[0].connect(tarn.inputConnectors[0]) 30 | return 31 | -------------------------------------------------------------------------------- /script/py/storage/on_code_change.py: -------------------------------------------------------------------------------- 1 | 2 | from PRESET_UTILS import buildPreset 3 | from EXPORTER import setContainer 4 | 5 | 6 | def initStorage(): 7 | try: 8 | op.pwstorage.valid 9 | except Exception: 10 | parent = op("/").create(baseCOMP, "storage") 11 | tarn = parent.create(baseCOMP, "Pixel_Wrangle") 12 | tarn.par.opshortcut = "pwstorage" 13 | 14 | 15 | def onTableChange(dat): 16 | comp = parent.Comp 17 | opid = comp.id 18 | oppath = comp.path 19 | inputs = op.pwstorage.fetch(oppath)["connections"]["inputs"] 20 | outputs = op.pwstorage.fetch(oppath)["connections"]["outputs"] 21 | initStorage() 22 | lastState = buildPreset(comp) 23 | op.pwstorage.store( 24 | oppath, 25 | { 26 | "opid": opid, 27 | "lastState": lastState, 28 | "connections": { 29 | "inputs": inputs, 30 | "outputs": outputs 31 | }, 32 | }, 33 | ) 34 | return 35 | -------------------------------------------------------------------------------- /script/py/storage/on_path_change.py: -------------------------------------------------------------------------------- 1 | 2 | from PRESET_UTILS import buildPreset 3 | from TDJSON import opToJSONOp 4 | 5 | 6 | def onPathChange(changeOp): 7 | oldStorage = op.pwstorage.storage 8 | comp = parent.Comp 9 | curOPID = changeOp.id 10 | curOPPATH = changeOp.path 11 | inputs = [ 12 | { 13 | "index": x.index, 14 | "op": x.connections[0].owner.path if len(x.connections) >= 1 else None, 15 | } 16 | for x in comp.inputConnectors 17 | ] 18 | outputs = [ 19 | { 20 | "index": x.index, 21 | "op": x.connections[0].owner.path if len(x.connections) >= 1 else None, 22 | } 23 | for x in comp.outputConnectors 24 | ] 25 | for k, v in oldStorage.items(): 26 | if int(v["opid"]) == curOPID: 27 | op.pwstorage.unstore(k) 28 | lastState = buildPreset(comp) 29 | op.pwstorage.store( 30 | curOPPATH, 31 | { 32 | "opid": curOPID, 33 | "lastState": lastState, 34 | "connections": {"inputs": inputs, "outputs": outputs}, 35 | }, 36 | ) 37 | return 38 | 39 | 40 | def onWireChange(changeOp): 41 | onPathChange(changeOp) 42 | -------------------------------------------------------------------------------- /script/py/tdcomponents_resolve.py: -------------------------------------------------------------------------------- 1 | from _stubs import * 2 | import os.path 3 | 4 | def file(path): 5 | return path if os.path.exists(path) else '' 6 | 7 | def modpath(*oppaths, checkprefix=None): 8 | for o in ops(*oppaths): 9 | if o.isDAT and o.isText and o.text and (not checkprefix or o.text.startswith(checkprefix)): 10 | return o.path 11 | return '' 12 | -------------------------------------------------------------------------------- /script/py/time/play-pause.py: -------------------------------------------------------------------------------- 1 | target = op(f"{parent.Comp}/glsl1") 2 | 3 | 4 | def onValueChange(par, prev): 5 | 6 | seconds = absTime.seconds 7 | frames = absTime.frame 8 | step = absTime.stepSeconds 9 | 10 | if par.eval() == 0: 11 | target.par.value0x = seconds 12 | target.par.value0y = frames 13 | target.par.value0z = step 14 | return 15 | else: 16 | target.par.value0x.expr = "absTime.seconds" 17 | target.par.value0y.expr = "absTime.frame" 18 | target.par.value0z.expr = "absTime.stepSeconds" 19 | 20 | return 21 | -------------------------------------------------------------------------------- /script/py/time/timedep.py: -------------------------------------------------------------------------------- 1 | target = op(f"{parent.Comp}/glsl1") 2 | 3 | 4 | def onValueChange(par, prev): 5 | if par.eval() == 0: 6 | try: 7 | op("time").destroy() 8 | except Exception: 9 | pass 10 | 11 | elif op("time") is None: 12 | setTimeOP() 13 | return 14 | 15 | def setTimeOP(): 16 | timen = parent().create(timeCOMP, "time") 17 | timen.par.play.bindExpr = "parent.Comp.par.Inputsplay" 18 | timen.par.rate.expr = "cookRate()" 19 | timen.par.start.bindExpr = "op('/local/time').par.start" 20 | timen.par.end.bindExpr = "op('/local/time').par.end" 21 | timen.par.rangelimit.bindExpr = "op('/local/time').par.rangelimit" 22 | timen.par.rangestart.bindExpr = "op('/local/time').par.rangestart" 23 | timen.par.rangeend.bindExpr = "op('/local/time').par.rangeend" 24 | timen.par.resetframe.bindExpr = "op('/local/time').par.resetframe" 25 | timen.par.signature1.bindExpr = "op('/local/time').par.signature1" 26 | timen.par.signature2.bindExpr = "op('/local/time').par.signature2" 27 | timen.par.tempo.bindExpr = "op('/local/time').par.tempo" 28 | timen.par.independent = True 29 | -------------------------------------------------------------------------------- /script/py/update/on_select_instance.py: -------------------------------------------------------------------------------- 1 | from TDJSON import opToJSONOp, addParametersFromJSONOp 2 | from LOAD_PRESET import loadPreset 3 | def onTableChange(dat): 4 | from LOAD_PRESET import loadPreset 5 | preset = '' 6 | path = dat[0, 0].val 7 | isInstance = parent.Comp.op('UI/HEADER/GET_IS_INSTANCE')[0, 0].val 8 | isFollow = parent.Comp.op('UI/HEADER/btn_follow_selection').par.Value0.val == 1 9 | if isInstance == 'True' and isFollow : 10 | tarn = op(path) 11 | preset = tarn.storage['preset'] 12 | curPar = opToJSONOp(tarn, extraAttrs=['val', 'expr', 'bindExpr'], forceAttrLists=True, includeCustomPages=True, includeBuiltInPages=False) 13 | loadPreset(preset['lastExportState']) 14 | addParametersFromJSONOp(parent.Comp, curPar, setValues=True, newAtEnd=False, fixParNames=True) 15 | 16 | -------------------------------------------------------------------------------- /script/py/utils/os/open_folder.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import subprocess 4 | 5 | def open_file(path): 6 | if platform.system() == "Windows": 7 | os.startfile(path) 8 | elif platform.system() == "Darwin": 9 | subprocess.Popen(["open", path]) 10 | else: 11 | subprocess.Popen(["xdg-open", path]) 12 | 13 | def onPulse(par): 14 | path = parent.Comp.par.Codelibrarypath.eval() 15 | open_file(path) 16 | return -------------------------------------------------------------------------------- /script/py/utils/parm/utils.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miu-lab/Pixel-Wrangle/9e2bcf66596f5e4e74086ada198be9b125267c8b/script/py/utils/parm/utils.py -------------------------------------------------------------------------------- /script/py/vscode/on_destroy.py: -------------------------------------------------------------------------------- 1 | 2 | from utils import * 3 | 4 | 5 | def onDestroy(): 6 | for source in sourcesn: 7 | source.par.syncfile = 0 8 | source.par.file = "" 9 | return 10 | -------------------------------------------------------------------------------- /script/py/vscode/on_open.py: -------------------------------------------------------------------------------- 1 | 2 | from utils import * 3 | 4 | tempFolder = path.normpath(f"{homePath}\\.cache") 5 | targetFolder = path.join( 6 | tempFolder, 7 | project.name.split(".")[0], 8 | project.name.split(".")[1], 9 | str(f"{nComp.name}_{nComp.id}"), 10 | ) 11 | projectname = project.name.split(".")[0] 12 | targetFolder = path.join( 13 | tempFolder, 14 | f"{projectname}_{getID(project.folder)}", 15 | project.name.split(".")[1], 16 | str(f"{nComp.name}_{nComp.id}"), 17 | ) 18 | try: 19 | makedirs(targetFolder) 20 | except Exception: 21 | pass 22 | 23 | for i, source in enumerate(sourcesn): 24 | ext = source.par.extension 25 | filePath = f"{targetFolder}/{fileNames[i]}.{ext}" 26 | with open(filePath, "w") as f: 27 | f.write(source.text) 28 | source.par.file = filePath 29 | source.par.syncfile = 1 30 | 31 | # Open new Visual Studio Code Instance with a brand new environments located in your Pixel-Wrangle USER folder 32 | openVSCode(targetFolder) 33 | 34 | # Get configs paths 35 | extjson = Path(f"{str(extensionsPath)}/extensions.json") 36 | settingsjson = Path(f"{str(userDataPath)}/User/settings.json") 37 | keybindingsjson = Path(f"{str(userDataPath)}/User/keybindings.json") 38 | 39 | 40 | # If environment does not exist, initialize with Pixel-Wrangle default settings, keybindings, extensions 41 | if extjson.exists() == True: 42 | isEmpty = False 43 | 44 | with open(str(extjson), "r") as f: 45 | text = f.read() 46 | if text == "[]": 47 | isEmpty = True 48 | # Init extensions 49 | if isEmpty: 50 | original = str(Path(f"{str(pwPath)}/vscode/extensions.json")) 51 | target = str(Path(f"{str(extensionsPath)}/extensions.json")) 52 | copyfile(original, target) 53 | 54 | else: 55 | extensionsPath.mkdir(parents=True, exist_ok=True) 56 | original = str(Path(f"{str(pwPath)}/vscode/extensions.json")) 57 | Path(f"{str(targetFolder)}/.vscode").mkdir(parents=True, exist_ok=True) 58 | target = str(Path(f"{str(targetFolder)}/.vscode/extensions.json")) 59 | copyfile(original, target) 60 | 61 | if settingsjson.exists() != True: 62 | # Init settingstt 63 | Path(f"{str(userDataPath)}/User").mkdir(parents=True, exist_ok=True) 64 | original = str(Path(f"{str(pwPath)}/vscode/settings.json")) 65 | target = str(Path(str(settingsjson))) 66 | copyfile(original, target) 67 | 68 | if keybindingsjson.exists() != True: 69 | # Init keybindings 70 | Path(f"{str(userDataPath)}/User").mkdir(parents=True, exist_ok=True) 71 | original = str(Path(f"{str(pwPath)}/vscode/keybindings.json")) 72 | target = str(Path(str(keybindingsjson))) 73 | copyfile(original, target) 74 | -------------------------------------------------------------------------------- /script/py/vscode/on_project_save.py: -------------------------------------------------------------------------------- 1 | 2 | from utils import * 3 | 4 | 5 | def onProjectPostSave(): 6 | from utils import vscodeInstance 7 | 8 | tempFolder = path.normpath(f"{homePath}\\.cache") 9 | 10 | projectname = project.name.split(".")[0] 11 | projectVersion = project.name.split(".")[1] 12 | 13 | try: 14 | projectVersion = int(projectVersion) 15 | except Exception: 16 | projectVersion = 0 17 | 18 | targetFolder = path.join( 19 | tempFolder, 20 | f"{projectname}_{getID(project.folder)}", 21 | str(projectVersion), 22 | str(f"{nComp.name}_{nComp.id}"), 23 | ) 24 | 25 | # Update VSCode process targetting the new folder if vscode window is already open, else pass 26 | try: 27 | if vscodeInstance.poll() == 0: 28 | vscodeInstance.kill() 29 | else: 30 | vscodeInstance.kill() 31 | vscodeInstance = openVSCode(targetFolder) 32 | except Exception: 33 | pass 34 | 35 | try: 36 | makedirs(targetFolder) 37 | except Exception: 38 | pass 39 | 40 | for i, source in enumerate(sourcesn): 41 | ext = source.par.extension 42 | filePath = f"{targetFolder}/{fileNames[i]}.{ext}" 43 | source.par.syncfile = 0 44 | with open(filePath, "w") as f: 45 | f.write(source.text) 46 | source.par.file = filePath 47 | source.par.syncfile = 1 48 | return 49 | 50 | 51 | def onCreate(): 52 | onProjectPostSave() 53 | 54 | 55 | def onStart(): 56 | onProjectPostSave() 57 | -------------------------------------------------------------------------------- /script/py/vscode/utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from subprocess import Popen 3 | from shutil import copyfile 4 | from os import path, makedirs 5 | from pprint import pprint 6 | 7 | # from stackoverflow.com/questions/22974499/generate-id-from-string-in-python 8 | def getID(string: str, last_idx: int = 6) -> str: 9 | import hashlib 10 | 11 | m = hashlib.md5() 12 | string = string.encode("utf-8") 13 | m.update(string) 14 | unique_name: str = str(int(m.hexdigest(), 16))[:last_idx] 15 | return unique_name 16 | 17 | homePath = Path(parent.Comp.par.Codeuserpath.eval()) 18 | pwPath = Path(parent.Comp.par.Codelibrarypath.eval()) 19 | envPath = '.vscode' 20 | userDataFolder = 'userdata' 21 | extensionsFolder = 'extensions' 22 | userDataPath = homePath.joinpath(envPath).joinpath(userDataFolder) 23 | extensionsPath = homePath.joinpath(envPath).joinpath(extensionsFolder) 24 | 25 | try: 26 | userDataPath.mkdir() 27 | except Exception: 28 | pass 29 | 30 | try: 31 | extensionsPath.mkdir() 32 | except Exception: 33 | pass 34 | 35 | nComp = parent.Comp 36 | 37 | glCodeContainer = op(f"{parent.Comp.path}/BUILD_GLSL_CODE") 38 | glPath = glCodeContainer.path 39 | 40 | Inputsn = op(f"{glPath}/UNIFORMS_AS_TEXT") 41 | Outputsn = op(f"{glPath}/OUTPUTS") 42 | Mainn = op(f"{glPath}/CODE") 43 | Functionsn = op(f"{glPath}/FUNCTIONS") 44 | 45 | sourcesn = [Inputsn, Outputsn, Mainn, Functionsn] 46 | fileNames = ["inputs", "outputs", "main", "functions" ] 47 | userdata = userDataPath 48 | extensions = extensionsPath 49 | vscodeenvcmd = "--user-data-dir" 50 | vscodeextcmd = "--extensions-dir" 51 | vscode = nComp.par.Codeexternaleditorpath.val 52 | vscodeInstance = '' 53 | vscode = nComp.par.Codeexternaleditorpath.val 54 | vscodeInstance = "" 55 | 56 | 57 | def openVSCode(targetFolder): 58 | global vscodeInstance 59 | vscodeInstance = Popen( 60 | [vscode, vscodeenvcmd, userdata, vscodeextcmd, extensions, targetFolder] 61 | ) 62 | return vscodeInstance 63 | -------------------------------------------------------------------------------- /vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "enkia.tokyo-night", 4 | "raczzalan.webgl-glsl-editor", 5 | "slevesque.shader", 6 | "pkief.material-icon-theme" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /vscode/keybindings.json: -------------------------------------------------------------------------------- 1 | // Merge Keybindings from grafikart -> https://grafikart.fr/tutoriels/vscode-settings-2096 2 | [ 3 | // Etend la sélection 4 | { 5 | "key": "ctrl+d", 6 | "command": "editor.action.smartSelect.expand", 7 | "when": "editorTextFocus" 8 | }, 9 | // Trouve toutes les occurences du mot sélectionné 10 | { 11 | "key": "shift+ctrl+d", 12 | "command": "editor.action.selectHighlights", 13 | "when": "editorFocus" 14 | }, 15 | // Ouvre un fichier 16 | { 17 | "key": "ctrl+o", 18 | "command": "workbench.action.quickOpen" 19 | }, 20 | // Crée un nouveau fichier 21 | { 22 | "key": "ctrl+n", 23 | "command": "explorer.newFile" 24 | }, 25 | // Commandes 26 | { 27 | "key": "ctrl+p", 28 | "command": "workbench.action.showCommands" 29 | }, 30 | // Masque / Affiche l'explorateur de fichier 31 | { 32 | "key": "alt+1", 33 | "command": "workbench.view.explorer" 34 | }, 35 | { 36 | "key": "alt+1", 37 | "command": "workbench.action.toggleSidebarVisibility", 38 | "when": "explorerViewletVisible" 39 | }, 40 | // Masque / Affiche la vue git 41 | { 42 | "key": "ctrl+k", 43 | "command": "workbench.view.scm" 44 | }, 45 | { 46 | "key": "ctrl+k", 47 | "command": "workbench.action.toggleSidebarVisibility", 48 | "when": "view.workbench.scm.visible" 49 | }, 50 | { 51 | "key": "ctrl+k", 52 | "command": "-workbench.action.terminal.clear", 53 | "when": "terminalFocus" 54 | }, 55 | // Masque / Affiche le terminal 56 | { 57 | "key": "ctrl+t", 58 | "command": "workbench.action.terminal.toggleTerminal" 59 | }, 60 | // Nettoie le terminal 61 | { 62 | "key": "ctrl+l", 63 | "command": "workbench.action.terminal.clear", 64 | "when": "terminalFocus" 65 | }, 66 | // Ouvre le fichier courant dans l'explorer de l'OS 67 | { 68 | "key": "shift+ctrl+o", 69 | "command": "revealFileInOS" 70 | }, 71 | // Navigation par symbole 72 | { 73 | "key": "ctrl+r", 74 | "command": "workbench.action.gotoSymbol" 75 | }, 76 | // Reformatte le document 77 | { 78 | "key": "alt+ctrl+l", 79 | "command": "editor.action.formatDocument" 80 | }, 81 | { 82 | "key": "ctrl+alt+b", 83 | "command": "workbench.action.tasks.build", 84 | "when": "taskCommandsRegistered" 85 | }, 86 | { 87 | "key": "ctrl+shift+b", 88 | "command": "-workbench.action.tasks.build", 89 | "when": "taskCommandsRegistered" 90 | }, 91 | { 92 | "key": "ctrl+shift+b", 93 | "command": "workbench.action.toggleActivityBarVisibility" 94 | } 95 | ] 96 | -------------------------------------------------------------------------------- /vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "window.menuBarVisibility": "compact", 3 | "workbench.startupEditor": "none", // On ne veut pas une page d'accueil chargée 4 | "editor.minimap.enabled": false, 5 | "editor.cursorSmoothCaretAnimation": true, 6 | "editor.cursorWidth": 3, 7 | "breadcrumbs.enabled": false, 8 | 9 | // -- Sidebar 10 | "workbench.tree.indent": 15, // Indente plus pour plus de clarté dans la sidebar 11 | "workbench.tree.renderIndentGuides": "always", 12 | 13 | // -- Code 14 | "editor.occurrencesHighlight": false, 15 | "editor.renderWhitespace": "trailing", // On ne veut pas laisser d'espace en fin de ligne 16 | 17 | // Thème 18 | "editor.fontFamily": "'JetBrains Mono', 'Fira Code', 'Operator Mono Lig', monospace", 19 | "editor.fontLigatures": true, 20 | "editor.fontSize": 16, 21 | "editor.lineHeight": 28, 22 | "workbench.colorTheme": "Tokyo Night", 23 | "workbench.iconTheme": "material-icon-theme", 24 | "workbench.colorCustomizations": { 25 | "[Tokyo Night]": { 26 | "editor.selectionBackground": "#3D59A1", 27 | "editor.selectionHighlightBackground": "#3D59A1" 28 | } 29 | }, 30 | "workbench.statusBar.visible": false, 31 | 32 | // Ergonomie 33 | "editor.wordWrap": "on", 34 | "editor.suggest.insertMode": "replace", 35 | "editor.acceptSuggestionOnCommitCharacter": false, 36 | "editor.formatOnSave": false, 37 | "editor.formatOnPaste": false, 38 | "editor.linkedEditing": true, 39 | "explorer.autoReveal": false, 40 | "explorer.confirmDragAndDrop": false, 41 | "workbench.editor.enablePreview": false, 42 | "emmet.triggerExpansionOnTab": true, 43 | 44 | // Fichiers 45 | "files.autoSave": "afterDelay", 46 | "files.defaultLanguage": "markdown", 47 | "files.exclude": { 48 | "**/.idea": true 49 | }, 50 | // Languages 51 | "javascript.preferences.importModuleSpecifierEnding": "js", 52 | "typescript.preferences.importModuleSpecifierEnding": "js", 53 | 54 | // Extensions 55 | "editor.unicodeHighlight.nonBasicASCII": false, 56 | "shadered.executablePath": "C:\\Program Files\\SHADERed\\SHADERed.exe", 57 | 58 | // GLSL Extension 59 | "webgl-glsl-editor.codeInjection": true, 60 | "webgl-glsl-editor.codeInjectionSource": [ 61 | "#extension GL_EXT_nonuniform_qualifier : enable", 62 | "#extension GL_GOOGLE_include_directive : enable", 63 | "struct TDTexInfo{ vec4 res; vec4 depth; };", 64 | "uniform sampler2D sTD2DInputs[1];", 65 | "uniform TD2DInfoBlock { TDTexInfo uTD2DInfos[1]; };", 66 | "uniform TDOutputInfoBlock { TDTexInfo uTDOutputInfo; };", 67 | "uniform int uTDPass;", 68 | "uniform int uTDCurrentDepth;", 69 | "uniform sampler2D sTDNoiseMap;", 70 | "uniform sampler1D sTDSineLookup;", 71 | "uniform sampler2D sTDWhite2D;", 72 | "uniform sampler3D sTDWhite3D;", 73 | "uniform sampler2DArray sTDWhite2DArray;", 74 | "uniform samplerCube sTDWhiteCube;", 75 | "vec4 TDDither(vec4 color);", 76 | "float TDPerlinNoise(vec2 v);", 77 | "float TDPerlinNoise(vec3 v);", 78 | "float TDPerlinNoise(vec4 v);", 79 | "float TDSimplexNoise(vec2 v);", 80 | "float TDSimplexNoise(vec3 v);", 81 | "float TDSimplexNoise(vec4 v);", 82 | "vec3 TDHSVToRGB(vec3 c);", 83 | "vec3 TDRGBToHSV(vec3 c);", 84 | "float TDSineLookup(float v);", 85 | "mat3 TDRotateToVector(vec3 forward, vec3 to);", 86 | "mat3 TDRotateOnAxis(float radians, vec3 axis);", 87 | "mat3 TDRotateX(float radians);", 88 | "mat3 TDRotateY(float radians);", 89 | "mat3 TDRotateZ(float radians);", 90 | "mat3 TDScale(float x, float y, float z);", 91 | "mat4 TDTranslate(float x, float y, float z);", 92 | "mat3 TDCreateTBNMatrix(vec3 N, vec3 T, float handedness);", 93 | "vec3 TDEquirectangularToCubeMap(vec2 mapCoord);", 94 | "vec2 TDCubeMapToEquirectangular(vec3 envMapCoord);", 95 | "vec2 TDCubeMapToEquirectangular(vec3 envMapCoord, out float mipMapBias);", 96 | "vec2 TDTexGenSphere(vec3 envMapCoord);", 97 | "vec4 TDOutputSwizzle(vec4 v);", 98 | "uvec4 TDOutputSwizzle(uvec4 v);", 99 | "in vec3 vUV;", 100 | "vec4 MousePos;", 101 | "vec3 MouseClicks;" 102 | ], 103 | "[glsl]": { 104 | "editor.defaultFormatter": "raczzalan.webgl-glsl-editor" 105 | }, 106 | "glsl-canvas.useCompactFormatter": false, 107 | "explorer.confirmDelete": false, 108 | "security.workspace.trust.enabled": false 109 | } 110 | --------------------------------------------------------------------------------