diff --git a/skills/creative/touchdesigner-mcp/SKILL.md b/skills/creative/touchdesigner-mcp/SKILL.md index a81e29e6dc..7deab319da 100644 --- a/skills/creative/touchdesigner-mcp/SKILL.md +++ b/skills/creative/touchdesigner-mcp/SKILL.md @@ -343,6 +343,11 @@ See `references/network-patterns.md` for complete build scripts + shader code. | `references/midi-osc.md` | MIDI/OSC controllers, TouchOSC, multi-machine sync | | `references/particles.md` | POPs and legacy particleSOP — emission, forces, collisions | | `references/projection-mapping.md` | Multi-window output, corner pin, mesh warp, edge blending | +| `references/external-data.md` | HTTP, WebSocket, MQTT, Serial, TCP, webserverDAT | +| `references/panel-ui.md` | Custom params, panel COMPs, button/slider/field, panelExecuteDAT | +| `references/replicator.md` | replicatorCOMP — data-driven cloning, layouts, callbacks | +| `references/dat-scripting.md` | Execute DAT family — chop/dat/parameter/panel/op/executeDAT | +| `references/3d-scene.md` | Lighting rigs, shadows, IBL/cubemaps, multi-camera, PBR | | `scripts/setup.sh` | Automated setup script | --- diff --git a/skills/creative/touchdesigner-mcp/references/3d-scene.md b/skills/creative/touchdesigner-mcp/references/3d-scene.md new file mode 100644 index 0000000000..ff54a3fb02 --- /dev/null +++ b/skills/creative/touchdesigner-mcp/references/3d-scene.md @@ -0,0 +1,275 @@ +# 3D Scene Reference + +Lighting rigs, shadows, IBL/cubemaps, multi-camera, and PBR materials. For wireframe rendering and feedback TOPs see `operator-tips.md`. For instancing geometry see `geometry-comp.md`. For shader code see `glsl.md`. + +--- + +## Anatomy of a 3D Scene + +``` +[Geometry COMP] ← contains SOPs (the shapes) +[Material] ← Phong/PBR/GLSL/Constant MAT +[Light COMPs] ← point/directional/spot/area/environment +[Camera COMP] ← view position, FOV + │ + ▼ + [Render TOP] ← combines geo + lights + camera into a 2D image + │ + ▼ + [post-FX chain] ← bloomTOP, glsl shaders, etc. + │ + ▼ + [windowCOMP] ← actual display +``` + +Render TOP is the heart. It takes an explicit `geometry` path, an explicit `camera` path, and lights via the lights table or an envlight reference. + +--- + +## Minimal Scene + +```python +# Geometry +geo = root.create(geometryCOMP, 'scene_geo') +sphere = geo.create(sphereSOP, 'shape') +sphere.par.rad = 1.0; sphere.par.rows = 64; sphere.par.cols = 64 + +# Material — start with PBR +mat = root.create(pbrMAT, 'mat') +mat.par.basecolorr = 0.7; mat.par.basecolorg = 0.7; mat.par.basecolorb = 0.7 +mat.par.metallic = 0.0 +mat.par.roughness = 0.4 + +geo.par.material = mat.path + +# Camera +cam = root.create(cameraCOMP, 'cam1') +cam.par.tx = 0; cam.par.ty = 0; cam.par.tz = 4 +cam.par.fov = 45 +cam.par.near = 0.1; cam.par.far = 100 + +# Key light +key = root.create(lightCOMP, 'key_light') +key.par.lighttype = 'point' +key.par.tx = 3; key.par.ty = 3; key.par.tz = 3 +key.par.dimmer = 1.5 + +# Render +render = root.create(renderTOP, 'render1') +render.par.outputresolution = 'custom' +render.par.resolutionw = 1920; render.par.resolutionh = 1080 +render.par.camera = cam.path +render.par.geometry = geo.path +render.par.lights = key.path # single light path; for multi, see below +render.par.bgcolorr = 0; render.par.bgcolorg = 0; render.par.bgcolorb = 0 +``` + +For multiple lights, leave `par.lights` blank — Render TOP scans the network for all `lightCOMP` and `envlightCOMP` ops by default. To restrict to specific lights, set `par.lights = '/project1/key_light /project1/fill_light'` (space-separated paths). + +--- + +## Light Types + +| Type | What | Common params | +|---|---|---| +| `point` | Omnidirectional, falls off with distance | `dimmer`, `coneangle` (n/a), `attenuation` | +| `directional` | Parallel rays, infinite distance (sun) | `dimmer`, light's rotation only matters | +| `spot` | Cone, falls off with distance + angle | `coneangle`, `conedelta`, `dimmer` | +| `cone` | Like spot but harder edge | same | +| `area` | Rectangular soft light source | `sizex`, `sizey` | + +For all: `colorr`, `colorg`, `colorb`, `tx/ty/tz`, `rx/ry/rz`, `dimmer`. + +### Three-Point Lighting (Studio Setup) + +```python +# Key — main light, ~45° front +key = root.create(lightCOMP, 'key') +key.par.lighttype = 'point' +key.par.tx = 4; key.par.ty = 3; key.par.tz = 4 +key.par.dimmer = 1.5 +key.par.colorr = 1.0; key.par.colorg = 0.95; key.par.colorb = 0.85 + +# Fill — softer, opposite side +fill = root.create(lightCOMP, 'fill') +fill.par.lighttype = 'area' +fill.par.tx = -4; fill.par.ty = 2; fill.par.tz = 3 +fill.par.dimmer = 0.5 +fill.par.colorr = 0.7; fill.par.colorg = 0.8; fill.par.colorb = 1.0 +fill.par.sizex = 4; fill.par.sizey = 4 + +# Rim/back — outline from behind +rim = root.create(lightCOMP, 'rim') +rim.par.lighttype = 'spot' +rim.par.tx = 0; rim.par.ty = 4; rim.par.tz = -4 +rim.par.coneangle = 30 +rim.par.dimmer = 1.0 + +# Optional: ambient lift to prevent pure-black shadows +amb = root.create(ambientlightCOMP, 'ambient') +amb.par.dimmer = 0.15 +``` + +--- + +## Shadows + +Spot and directional lights cast shadows when `par.shadowtype != 'none'`. + +```python +key.par.shadowtype = 'softshadow' # 'none' | 'hardshadow' | 'softshadow' +key.par.shadowsize = 1024 # shadow map resolution +key.par.shadowsoftness = 0.02 # softshadow only +``` + +**Tips:** +- Soft shadows are GPU-expensive. Start with `shadowsize = 1024` and only go higher (2048/4096) if shadow edges look pixelated at your resolution. +- Set the spot light's `near`/`far` to JUST contain the scene. Wider range = wasted shadow map precision. +- Multiple shadow-casting lights compound cost. Limit to 1-2 in real-time work; pre-bake the rest into the materials. + +--- + +## Image-Based Lighting (IBL) / Environment Light + +For realistic PBR materials you need a cubemap for reflections. + +```python +# Environment light from an HDR +env = root.create(envlightCOMP, 'env') +env.par.envmap = '/project1/cube_in' # path to a TOP that produces a cubemap +env.par.envlightmap = ... # diffuse irradiance map (often same as envmap) +env.par.dimmer = 1.0 + +# Cubemap source — option A: built-in cubeTOP from 6 faces +cube = root.create(cubeTOP, 'cube_in') +# (assign 6 face TOPs) + +# Option B: HDR equirectangular → cubemap conversion +# Use a moviefileinTOP loading .hdr or .exr, then projectTOP type='cubemapfromequirect' +hdr = root.create(moviefileinTOP, 'hdr_src') +hdr.par.file = '/path/to/environment.hdr' + +proj = root.create(projectTOP, 'cube_proj') +proj.par.projecttype = 'cubemapfromequirect' +proj.inputConnectors[0].connect(hdr) +``` + +PBR materials sample the environment automatically when `envlightCOMP` is in the scene. Verify param names with `td_get_par_info(op_type='envlightCOMP')` — TD versions vary. + +--- + +## PBR Material Setup + +```python +mat = root.create(pbrMAT, 'pbr_metal') +mat.par.basecolorr = 0.95; mat.par.basecolorg = 0.65; mat.par.basecolorb = 0.4 +mat.par.metallic = 1.0 +mat.par.roughness = 0.25 +mat.par.specularlevel = 0.5 +mat.par.emitcolorr = 0; mat.par.emitcolorg = 0; mat.par.emitcolorb = 0 + +# Texture maps +mat.par.basecolormap = '/project1/textures/albedo' # TOP path +mat.par.metallicroughnessmap = '/project1/textures/mr' # G=roughness, B=metallic (glTF convention) +mat.par.normalmap = '/project1/textures/normal' +mat.par.emitmap = '/project1/textures/emit' +mat.par.occlusionmap = '/project1/textures/ao' +``` + +**Material idioms:** + +| Look | metallic | roughness | basecolor | +|---|---|---|---| +| Brushed steel | 1.0 | 0.4 | (0.7, 0.7, 0.7) | +| Polished gold | 1.0 | 0.1 | (1.0, 0.85, 0.4) | +| Plastic | 0.0 | 0.5 | mid-saturated | +| Rubber | 0.0 | 0.9 | dark | +| Glass | 0.0 | 0.05 | (1, 1, 1), low alpha + transmission | +| Glowing emitter | 0.0 | 1.0 | dark, high `emitcolor` | + +For glass/transmission, recent TD versions support `transmission` in PBR; older versions need glslMAT. + +--- + +## Multi-Camera Setups + +For comparison views, instant replay, multi-screen mapping, etc. + +```python +# Camera A — main scene +cam_a = root.create(cameraCOMP, 'cam_main') +cam_a.par.tz = 5 + +# Camera B — orbiting top-down +cam_b = root.create(cameraCOMP, 'cam_top') +cam_b.par.ty = 6; cam_b.par.rx = -90 + +# Render each via separate Render TOPs +render_a = root.create(renderTOP, 'render_main') +render_a.par.camera = cam_a.path +render_a.par.geometry = geo.path + +render_b = root.create(renderTOP, 'render_top') +render_b.par.camera = cam_b.path +render_b.par.geometry = geo.path +``` + +Composite both with a `multiplyTOP`/`compositeTOP` for picture-in-picture, or route to separate `windowCOMP`s for multi-display. + +### Camera animation + +Drive camera params via expressions (orbit), animationCOMP (waypoint), or LFO (oscillation): + +```python +# Orbiting camera +cam_a.par.tx.mode = ParMode.EXPRESSION +cam_a.par.tx.expr = "cos(absTime.seconds * 0.3) * 6" +cam_a.par.tz.mode = ParMode.EXPRESSION +cam_a.par.tz.expr = "sin(absTime.seconds * 0.3) * 6" +cam_a.par.lookat = '/project1/scene_geo' # auto-aim at target +``` + +`par.lookat` is the simplest "always look at target" mechanism. + +### Depth of field + +PBR + Render TOP supports DOF when `par.dof = 'on'`. + +```python +render.par.dof = 'on' +render.par.focusdistance = 5.0 +render.par.aperture = 0.05 # blur strength +render.par.bokehshape = 'hexagon' +``` + +DOF is GPU-heavy. Render at lower res then upscale for performance. + +--- + +## Common Pitfalls + +1. **Render TOP shows black** — most common cause: no light. Even with PBR you need at least one `lightCOMP` or `envlightCOMP`. Add an `ambientlightCOMP` at low dimmer as a safety net. +2. **Material doesn't appear** — `geo.par.material` must be a string PATH, not the material op itself. Use `mat.path`, not `mat`. +3. **Lights ignored** — by default Render TOP picks up ALL `lightCOMP`s in the network. If you have leftover lights from another scene, they leak in. Set `par.lights` explicitly. +4. **PBR looks flat** — without an `envlightCOMP` providing reflections, PBR materials look like Phong. Add one even if you don't have an HDR (use a `constantTOP` cubemap as fallback). +5. **Shadow acne / striping** — increase `par.shadowbias` slightly. Tune per-light. +6. **Camera inside geometry** — if `cam.par.tz` is INSIDE a sphere, you see the inside (or nothing if backface culled). Move the camera further out. +7. **Light range too small** — point lights have implicit attenuation. Far-away geometry receives little light. Increase `par.dimmer` or move lights closer. +8. **Multiple cameras conflict** — one render TOP = one camera. Don't try to share. Use multiple render TOPs. +9. **Wrong handedness** — TD is right-handed Y-up. Imported assets from Z-up apps (Blender, Maya in Z-up) need a 90° X rotation on the geo COMP. +10. **Cooking budget** — PBR + IBL + shadows + DOF at 1080p60 is fine on modern GPUs but 4K + 4 lights + soft shadows + DOF will tank. Profile via `td_get_perf` and downgrade settings before adding more. + +--- + +## Quick Recipes + +| Goal | Recipe | +|---|---| +| Studio portrait | 3-point rig (key + fill + rim) + ambient + PBR mat + DOF | +| Outdoor daylight | One directional `lightCOMP` (sun) + envlight (sky HDR) + soft shadows | +| Dramatic / film noir | Single spot light from upper side, hard shadows, deep ambient = 0.05 | +| Abstract / dreamy | Multiple area lights at low dimmer, no shadows, `bloomTOP` post | +| Product render | Three-point + IBL + neutral PBR + `bgcolorr=g=b=1` (white seamless) | +| Game-style | Phong MAT + 1-2 lights + no IBL + flat ambient (cheap, stylized) | +| Wireframe + solid | Two render TOPs (one with wireframeMAT, one with PBR), composite via `addTOP` | +| Orbiting camera | `par.lookat` + expressions on tx/tz using sin/cos | diff --git a/skills/creative/touchdesigner-mcp/references/dat-scripting.md b/skills/creative/touchdesigner-mcp/references/dat-scripting.md new file mode 100644 index 0000000000..e18b277490 --- /dev/null +++ b/skills/creative/touchdesigner-mcp/references/dat-scripting.md @@ -0,0 +1,352 @@ +# DAT-Based Scripting Reference + +TD's event/callback model — Python that runs in response to network events. The full set of "Execute DATs" plus their idiomatic patterns. + +For arbitrary Python execution (not callback-based), see `python-api.md`. For the MCP's `td_execute_python` tool, see `mcp-tools.md`. + +--- + +## The Execute DAT Family + +Every type watches one kind of event source and fires Python on changes. + +| DAT | Watches | Use for | +|---|---|---| +| `chopExecuteDAT` | A CHOP's channel values | Audio triggers, threshold callbacks, state machines on numeric input | +| `datExecuteDAT` | A DAT's content (table cells, text) | Reacting to data updates from APIs, parsing webDAT responses | +| `parameterExecuteDAT` | A parameter's value or pulse | Reacting to user-changed params, custom pulse buttons | +| `panelExecuteDAT` | A panel COMP's interaction | Button clicks, slider drags, field commits | +| `opExecuteDAT` | Operator lifecycle | New operator created, deleted, name changed | +| `executeDAT` | Project lifecycle, frame events | Run-once setup, per-frame logic, save/load hooks | + +All have a docked DAT with predefined callback functions. You only fill in the bodies of the ones you care about. + +--- + +## chopExecuteDAT — Numeric Triggers + +```python +ce = root.create(chopExecuteDAT, 'kick_handler') +ce.par.chop = '/project1/audio/out_kick' # source CHOP +ce.par.offtoon = True # fire when channel rises above 0 +ce.par.ontooff = False +ce.par.whileon = False +ce.par.valuechange = False +``` + +In the docked callback DAT: + +```python +def offToOn(channel, sampleIndex, val, prev): + """Channel went from 0 to non-zero. Classic beat trigger.""" + op('/project1/strobe').par.flash.pulse() + op('/project1/scene').par.index = (op('/project1/scene').par.index + 1) % 8 + return + +def onToOff(channel, sampleIndex, val, prev): + """Channel went from non-zero to 0.""" + return + +def whileOn(channel, sampleIndex, val, prev): + """Fires every frame while channel is non-zero. Use sparingly.""" + return + +def valueChange(channel, sampleIndex, val, prev): + """Fires every frame the value changes (continuous). Heavy.""" + return +``` + +`channel` is a `Channel` object — `.name`, `.owner`, `.vals[]`. Use `channel.name == 'chan1'` to filter. + +**Threshold-based custom triggers:** wire the source CHOP through a `triggerCHOP` first to get clean 0/1 pulses, then watch with `offtoon`. + +--- + +## datExecuteDAT — Table/Text Changes + +```python +de = root.create(datExecuteDAT, 'api_response') +de.par.dat = '/project1/api/web1' # source DAT +de.par.tablechange = True # any cell change +de.par.cellchange = False +de.par.rowchange = False +de.par.colchange = False +``` + +```python +def onTableChange(dat): + """Whole table changed (including text DAT content updates).""" + if dat.numRows == 0: + return + # If it's a webDAT response, parse JSON + import json + try: + data = json.loads(dat.text) + except json.JSONDecodeError: + debug(f'Bad JSON: {dat.text[:100]}') + return + # Write to a CHOP + op('/project1/api_value').par.value0 = float(data.get('count', 0)) + return + +def onCellChange(dat, cells, prev): + """Specific cells changed.""" + for cell in cells: + # cell.row, cell.col, cell.val + pass + return +``` + +`debug()` prints to the textport — readable via `td_read_textport`. + +--- + +## parameterExecuteDAT — Param Changes & Pulse + +```python +pe = root.create(parameterExecuteDAT, 'comp_params') +pe.par.op = '/project1/my_component' # COMP whose params to watch +pe.par.parameters = '*' # or specific names like 'Intensity Reset' +pe.par.valuechange = True +pe.par.pulse = True +``` + +```python +def onValueChange(par, prev): + """par is a Par object. par.name, par.eval(), par.owner.""" + if par.name == 'Intensity': + op('/project1/bloom').par.threshold = par.eval() + return + +def onPulse(par): + """Pulse param was triggered.""" + if par.name == 'Reset': + op('/project1/scene').par.index = 0 + op('/project1/audio_player').par.cuepoint = 0 + op('/project1/audio_player').par.cuepulse.pulse() + return + +def onExpressionChange(par, val, prev): + """User changed the expression on a param.""" + return + +def onExportChange(par, val, prev): + """Export source changed.""" + return + +def onModeChange(par, val, prev): + """Param mode changed (CONSTANT / EXPRESSION / EXPORT / etc).""" + return +``` + +--- + +## panelExecuteDAT — UI Events + +For interactive control surfaces. See `panel-ui.md` for the full panel COMP context. + +```python +pe = root.create(panelExecuteDAT, 'btn_handler') +pe.par.panel = '/project1/play_btn' +pe.par.click = True # mouse click events +pe.par.value = True # state changes (toggle) +pe.par.lockedchange = False +``` + +```python +def onOffToOn(panelValue): + """Panel value rose to 1 (button pressed, slider crossed threshold).""" + op('/project1/scene_timer').par.start.pulse() + return + +def onOnToOff(panelValue): + """Panel value dropped to 0.""" + return + +def onValueChange(panelValue): + """Continuous: every frame the value changes.""" + val = panelValue.eval() + op('/project1/master').par.opacity = val + return + +def onClick(panelValue): + """Discrete click event, fires once per click.""" + return +``` + +`panelValue` is a `Par` object on the panel COMP. + +--- + +## opExecuteDAT — Operator Lifecycle + +Watches creation/deletion/renaming of operators in a parent COMP. + +```python +oe = root.create(opExecuteDAT, 'lifecycle') +oe.par.op = '/project1' +oe.par.create = True +oe.par.destroy = True +oe.par.namechange = True +oe.par.flagchange = False +``` + +```python +def onCreate(opCreated): + """A new operator was created. Useful for auto-applying conventions.""" + if opCreated.OPType == 'glslTOP': + # Always wrap with a null + n = opCreated.parent().create(nullTOP, opCreated.name + '_out') + n.inputConnectors[0].connect(opCreated) + return + +def onDestroy(opDestroyed): + """Operator was deleted. opDestroyed.path is still valid for one frame.""" + return + +def onNameChange(opChanged): + """Operator was renamed.""" + return +``` + +Useful for dev-time scaffolding (auto-create downstream nullTOPs, auto-name conventions). Disable in production projects to avoid surprise side effects. + +--- + +## executeDAT — Project Lifecycle & Per-Frame + +The catch-all. Gets you hooks into project start, save, load, frame-start, frame-end. + +```python +exec_dat = root.create(executeDAT, 'lifecycle') +exec_dat.par.start = True +exec_dat.par.create = True +exec_dat.par.framestart = True +exec_dat.par.frameend = False +``` + +```python +def onStart(): + """Project just started cooking. Run once.""" + op('/project1/scene').par.index = 0 + debug('Project started') + return + +def onCreate(): + """Component was just created (only fires for component executeDATs, not project root).""" + return + +def onFrameStart(frame): + """Per-frame, BEFORE network cooks. Heavy logic here = bottleneck.""" + return + +def onFrameEnd(frame): + """Per-frame, AFTER network cooks. Use for capture, recording, post-network logic.""" + return + +def onPlayStateChange(playing): + """Project play/pause toggled.""" + return + +def onProjectPreSave(): + """Right before saving the .toe file.""" + return + +def onProjectPostSave(): + return +``` + +Heavy per-frame logic in `onFrameStart` is one of the top performance regressions in TD projects. Use CHOPs for per-frame computation, scripts for events. + +--- + +## Pattern: Triggering an Animation Sequence on Beat + +```python +# Source: a kick trigger CHOP +# Goal: on each kick, run a 1.5s scale pulse + color flash + +# Setup (create once) +animator = root.create(timerCHOP, 'pulse_anim') +animator.par.length = 1.5 +animator.par.cycle = False + +# Param expressions on visual targets: +op('logo').par.sx.expr = "1.0 + (1 - op('pulse_anim')['timer_fraction']) * 0.3" +op('logo').par.sx.mode = ParMode.EXPRESSION +op('logo').par.sy.expr = "1.0 + (1 - op('pulse_anim')['timer_fraction']) * 0.3" +op('logo').par.sy.mode = ParMode.EXPRESSION + +# In a chopExecuteDAT watching the kick CHOP: +def offToOn(channel, sampleIndex, val, prev): + op('pulse_anim').par.start.pulse() + return +``` + +--- + +## Pattern: Live Editing a CHOP from API Data + +```python +# webDAT polls an API every 5 seconds +# datExecuteDAT parses the response and writes to a constantCHOP + +def onTableChange(dat): + import json + try: + data = json.loads(dat.text) + except: + return + target = op('/project1/external_state') + target.par.name0 = 'temperature' + target.par.value0 = float(data['temp_c']) + target.par.name1 = 'humidity' + target.par.value1 = float(data['humidity']) + return +``` + +Visuals just reference `op('external_state')['temperature']` — they update live. + +--- + +## Pattern: Self-Cleaning Network + +```python +# An opExecuteDAT watching for orphaned helper ops, deleting them after their parent disappears + +def onDestroy(opDestroyed): + parent_name = opDestroyed.name + helper = op(f'/project1/{parent_name}_helper') + if helper: + helper.destroy() + return +``` + +--- + +## Pitfalls + +1. **Callbacks crash silently** — exceptions print to the textport but don't show up in the UI. Always `td_clear_textport` before debugging, then `td_read_textport` after. +2. **`debug()` vs `print()`** — both write to textport, but `debug()` includes the file/line of the calling DAT. Prefer `debug()` for scripts. +3. **`val` is the new value, `prev` is old** — easy to swap. Always: `def offToOn(channel, sampleIndex, val, prev)`. Check parameter order in TD docs if confused. +4. **`whileOn` and `valueChange` are per-frame** — heavy. Avoid unless absolutely needed. Drive via expressions instead. +5. **Callbacks don't run during cooking-paused state** — if the parent COMP has `allowCooking=False`, callbacks freeze. Useful for "disable me" toggles. +6. **`par` vs `panelValue`** — parameterExecuteDAT gives `par` (a Par object), panelExecuteDAT gives `panelValue` (also a Par-like object). Both have `.name` and `.eval()` but their context differs. +7. **`opExecuteDAT` fires for itself** — when you create an opExecuteDAT, it can fire `onCreate` for itself if `par.create=True` and parent matches. Filter by `if opCreated == me: return`. +8. **Reload behavior** — when reloading an extension (`td_reinit_extension`), all callback DATs reset their internal state. Module-level vars are lost. Persist state in tableDATs or the docked DAT itself, not in module globals. +9. **Cooking dependencies** — if a callback writes to an op that's upstream of the callback's source, you get a cooking loop. TD warns about it but doesn't always block. Keep dataflow one-directional. +10. **Active flag** — every Execute DAT has `par.active`. False = silent. Easy to toggle for testing without deleting wiring. + +--- + +## Quick Recipes + +| Goal | Setup | +|---|---| +| Beat trigger | `chopExecuteDAT.par.offtoon=True` watching a `triggerCHOP` | +| API response handler | `datExecuteDAT.par.tablechange=True` watching a `webDAT` | +| Custom button → action | `parameterExecuteDAT.par.pulse=True` watching a custom pulse param | +| Slider → continuous param | `panelExecuteDAT.par.value=True` watching a `sliderCOMP` | +| Run-once setup | `executeDAT.par.start=True` with logic in `onStart()` | +| Per-frame metrics | `executeDAT.par.frameend=True` recording values to a CHOP | +| Auto-name new ops | `opExecuteDAT.par.create=True` enforcing naming conventions | diff --git a/skills/creative/touchdesigner-mcp/references/external-data.md b/skills/creative/touchdesigner-mcp/references/external-data.md new file mode 100644 index 0000000000..ca99435212 --- /dev/null +++ b/skills/creative/touchdesigner-mcp/references/external-data.md @@ -0,0 +1,322 @@ +# External Data Reference + +Network and device I/O — HTTP requests, WebSockets, MQTT, Serial, TCP, UDP. For MIDI/OSC specifically see `midi-osc.md`. + +Common production needs: +- API polling / webhook ingestion +- Real-time data streams (sensors, market data, chat) +- IoT device control (Arduino, ESP32, smart lights) +- Inter-application messaging +- Hosting a tiny TD-side HTTP server for remote control + +--- + +## Web DAT — HTTP Requests + +```python +web = root.create(webDAT, 'api_call') +web.par.url = 'https://api.example.com/v1/status' +web.par.fetchmethod = 'get' # 'get' | 'post' | 'put' | 'delete' +web.par.format = 'auto' # 'auto' | 'text' | 'json' +web.par.timeout = 5.0 +``` + +**Triggering a request:** + +`webDAT` does NOT auto-fetch on cook. Trigger explicitly: + +```python +web.par.fetch.pulse() +``` + +Or via expression on a CHOP value-change (chopExecuteDAT — see `dat-scripting.md`). + +**Authentication headers:** + +Use `webclientDAT` (more flexible) or set `webDAT` headers via the headers DAT: + +```python +web_headers = root.create(tableDAT, 'headers') +web_headers.appendRow(['Authorization', 'Bearer YOUR_TOKEN']) +web_headers.appendRow(['Accept', 'application/json']) +web.par.headers = web_headers.path +``` + +**Parsing JSON response:** + +```python +import json + +def onTableChange(dat): + response = dat.text # raw response body + data = json.loads(response) + # Update a tableDAT or store in a constantCHOP for downstream use + op('/project1/api_status').par.value0 = data['count'] + return +``` + +Wire this in a `datExecuteDAT` watching the webDAT. + +**Polling pattern:** + +```python +# timerCHOP fires every N seconds +timer = root.create(timerCHOP, 'poll_timer') +timer.par.length = 5.0 +timer.par.cycle = True + +# chopExecuteDAT on the timer's 'cycles' channel pulses the webDAT +def offToOn(channel, sampleIndex, val, prev): + op('/project1/api_call').par.fetch.pulse() + return +``` + +--- + +## Web Client DAT — More Robust HTTP + +`webclientDAT` is the modern replacement for `webDAT` — supports streaming responses, chunked transfer, custom auth. + +```python +client = root.create(webclientDAT, 'api') +client.par.method = 'POST' +client.par.url = 'https://api.example.com/events' +client.par.uploadtype = 'json' +client.par.uploaddata = '{"event": "scene_change", "scene": 3}' +client.par.request.pulse() +``` + +Output goes to its child `webclient1_response` DAT. Use a `datExecuteDAT` to react. + +--- + +## Web Server DAT — TD as HTTP Server + +Hosts a tiny HTTP server inside TD. Useful for: +- Status/health endpoints +- Remote control from a phone or another machine +- Webhook receivers from external services + +```python +server = root.create(webserverDAT, 'control_server') +server.par.port = 8080 +server.par.active = True + +# Define handler in the docked callback DAT +``` + +In the auto-created `webserver1_callbacks` DAT: + +```python +def onHTTPRequest(webServerDAT, request, response): + path = request['uri'] + if path == '/status': + response['statusCode'] = 200 + response['data'] = '{"fps": 60, "scene": "active"}' + elif path == '/scene': + idx = int(request['args'].get('index', 0)) + op('/project1/scene_switch').par.index = idx + response['statusCode'] = 200 + response['data'] = 'OK' + else: + response['statusCode'] = 404 + response['data'] = 'Not Found' + return response +``` + +Test from terminal: `curl http://localhost:8080/status`. + +**Security:** No auth by default. Bind to localhost only or add a token check in the callback. Never expose to the public internet without auth. + +--- + +## WebSocket DAT — Bidirectional Real-Time + +For low-latency bidirectional streams (chat, live data feeds, controllers). + +### Client + +```python +ws = root.create(websocketDAT, 'ws_client') +ws.par.netaddress = 'wss://api.example.com/socket' +ws.par.active = True +``` + +In the docked callbacks DAT: + +```python +def onConnect(dat): + dat.sendText('{"action": "subscribe", "channel": "ticks"}') + return + +def onReceiveText(dat, rowIndex, message): + # message is a string; parse JSON, dispatch to ops + import json + data = json.loads(message) + op('/project1/price_chop').par.value0 = data['price'] + return + +def onDisconnect(dat): + # Optionally schedule a reconnect + return +``` + +### Server + +```python +ws = root.create(websocketDAT, 'ws_server') +ws.par.mode = 'server' +ws.par.port = 9001 +ws.par.active = True +``` + +Same callback structure with an additional `clientID` arg. + +--- + +## MQTT — Pub/Sub for IoT + +```python +mqtt = root.create(mqttClientDAT, 'iot') +mqtt.par.brokeraddress = 'broker.hivemq.com' +mqtt.par.brokerport = 1883 +mqtt.par.clientid = 'td_install_01' +mqtt.par.connect.pulse() + +# Subscribe in callbacks DAT: +def onConnect(dat): + dat.subscribe('home/lights/+', qos=1) + return + +def onReceive(dat, topic, payload, qos, retained, dup): + # payload is bytes — decode if JSON + msg = payload.decode('utf-8') + # Dispatch by topic + return + +# Publish from anywhere: +op('iot').publish('show/scene', 'sunset', qos=0, retain=False) +``` + +For Mosquitto / HiveMQ self-hosted brokers use the same setup with `tcp://192.168.x.x` and your local port. + +--- + +## Serial DAT — Arduino, USB Devices + +```python +serial = root.create(serialDAT, 'arduino') +serial.par.port = '/dev/cu.usbmodem14101' # macOS — check Arduino IDE +# Windows: 'COM3', 'COM4', etc. +serial.par.baudrate = 115200 +serial.par.active = True +``` + +In callbacks: + +```python +def onReceive(dat, rowIndex, line): + # Each newline-terminated line from Arduino arrives here + parts = line.split(',') + op('/project1/sensors').par.value0 = float(parts[0]) + op('/project1/sensors').par.value1 = float(parts[1]) + return +``` + +Send to Arduino: +```python +op('arduino').send('LED_ON\n') +``` + +--- + +## TCP/IP DAT — Custom Protocols + +For talking to non-HTTP servers (game servers, custom protocols, legacy systems). + +```python +tcp = root.create(tcpipDAT, 'show_control') +tcp.par.netaddress = '192.168.1.50' +tcp.par.port = 7000 +tcp.par.protocol = 'tcp' # 'tcp' | 'udp' +tcp.par.active = True +``` + +Send / receive via callbacks similar to websocketDAT. + +For UDP-only (fire-and-forget, no connection), use `udpoutDAT` + `udpinDAT` — simpler but unreliable across networks. + +--- + +## Common Patterns + +### REST API → Visual + +``` +timerCHOP (5s loop) + → chopExecuteDAT (pulse webDAT.par.fetch on cycle) + → webDAT (returns JSON) + → datExecuteDAT (parse, write to constantCHOP) + → CHOP drives glsl uniform → visuals +``` + +### Webhook receiver + +``` +webserverDAT (port 8080, /webhook endpoint) + → callback writes to a tableDAT log + triggers a scene change +``` + +### Real-time stock/crypto ticker + +``` +websocketDAT (subscribe to feed) + → onReceiveText callback parses JSON + → writes to constantCHOP + → drives bar chart / typography animation +``` + +### IoT-controlled installation + +``` +MQTT → callback dispatches by topic + → /lights/main → constantCHOP drives lighting render + → /audio/volume → mathCHOP for master fader +``` + +### Two-way phone control + +``` +WebSocket server in TD + → simple HTML page on phone connects, sends slider values + → callback writes to ops + → TD pushes status back via dat.sendText() to phone UI +``` + +--- + +## Pitfalls + +1. **`webDAT` doesn't auto-fetch** — must explicitly pulse `par.fetch`. Easy to forget. +2. **Blocking on slow APIs** — `webDAT` runs on the cook thread. A 30s API call freezes TD for 30s. Use `webclientDAT` (async) for anything potentially slow. +3. **WebSocket reconnection** — TD does NOT auto-reconnect on disconnect. Implement backoff in `onDisconnect`. +4. **Serial port permissions on macOS** — TD needs Full Disk Access OR the port needs to be unlocked via `sudo chmod 666 /dev/cu.usbmodem...` per session. +5. **MQTT broker connection state** — `mqttClientDAT` may show `connected=true` but messages don't flow if QoS is wrong or topic ACL blocks. Check broker logs. +6. **JSON parse errors crash callbacks silently** — wrap parses in try/except and log to textport. Otherwise the callback just stops firing. +7. **Firewall on Windows** — first time `webserverDAT` binds, Windows pops a firewall dialog. Approve it or the server is unreachable. +8. **CORS** — `webserverDAT` doesn't add CORS headers by default. If serving a webapp from a different origin, add `Access-Control-Allow-Origin: *` in the response. +9. **Polling vs push** — polling burns API quota. Always prefer WebSocket / webhook / MQTT for high-frequency data. +10. **Floating-point parsing** — sensor data over Serial often comes as strings. `float()` will crash on `'\n'` or `'NaN'`. Validate before converting. + +--- + +## Quick Recipes + +| Goal | Op chain | +|---|---| +| Periodic API fetch | `timerCHOP` → `chopExecuteDAT` pulses → `webDAT` → `datExecuteDAT` parses | +| Webhook receiver | `webserverDAT` (port + path), callback writes to ops | +| Real-time stream | `websocketDAT` client → onReceiveText → CHOP/DAT | +| Arduino sensor → visual | `serialDAT` → callback → `constantCHOP` → expression on visual op | +| TD ↔ phone control | `websocketDAT` server + simple HTML page on phone | +| MQTT IoT integration | `mqttClientDAT` subscribe → callback dispatches by topic | diff --git a/skills/creative/touchdesigner-mcp/references/panel-ui.md b/skills/creative/touchdesigner-mcp/references/panel-ui.md new file mode 100644 index 0000000000..bec68e33cf --- /dev/null +++ b/skills/creative/touchdesigner-mcp/references/panel-ui.md @@ -0,0 +1,281 @@ +# Panel & UI Reference + +Interactive control surfaces inside TouchDesigner — buttons, sliders, fields, custom parameter pages, panel callbacks. For HUD overlays (rendered text on visuals) see `layout-compositor.md`. + +Use cases: +- VJ control rack (master fader, scene buttons, FX toggles) +- Installation operator console +- Self-contained TOX components with their own parameter UIs +- Phone-style touch interfaces displayed on a tablet + +--- + +## Two Layers of UI + +| Layer | What it is | Use for | +|---|---|---| +| **Custom Parameters** | Params on any COMP, edited like built-in TD params | Configurable components, presets, "settings" panels | +| **Panel COMPs** | Visible widgets (button, slider, field) inside a containerCOMP | Interactive control surfaces, real-time UIs | + +Combine both: build a containerCOMP with panel widgets that read/write custom parameters on a parent component. + +--- + +## Custom Parameters + +Add user-editable params to any COMP. Params persist with the COMP, drive expressions, and survive save/reload. + +```python +# Add a custom page to a baseCOMP +comp = op('/project1/my_component') +page = comp.appendCustomPage('Controls') + +# Add typed params +page.appendFloat('Intensity', label='Intensity')[0] # returns a Par +page.appendInt('Count', label='Count')[0] +page.appendToggle('Enabled', label='Enabled')[0] +page.appendMenu('Mode', menuNames=['off', 'soft', 'hard'], menuLabels=['Off', 'Soft', 'Hard'])[0] +page.appendStr('Title', label='Title')[0] +page.appendRGB('Color', label='Color') # returns 3 pars +page.appendXY('Offset', label='Offset') # returns 2 pars +page.appendPulse('Reset', label='Reset')[0] +page.appendFile('TextureFile', label='Texture')[0] +``` + +**Read/write from anywhere:** + +```python +val = op('/project1/my_component').par.Intensity.eval() +op('/project1/my_component').par.Intensity = 0.7 +``` + +**Drive other params via expression:** + +```python +op('bloom1').par.threshold.mode = ParMode.EXPRESSION +op('bloom1').par.threshold.expr = "op('/project1/my_component').par.Intensity" +``` + +**Pulse handler (Reset button):** + +Use a `parameterExecuteDAT` watching the COMP's pulse params. See `dat-scripting.md`. + +--- + +## Panel COMPs — The Widgets + +Each is a COMP that renders as a clickable/draggable widget inside a `containerCOMP`. + +| Type | Type Name | Use | +|---|---|---| +| Button | `buttonCOMP` | Click action — momentary or toggle | +| Slider | `sliderCOMP` | Drag to set 0-1 value (1D or 2D) | +| Field | `fieldCOMP` | Text input | +| Container | `containerCOMP` | Layout + visual styling, holds children | +| Select | `selectCOMP` | Reference and display content from another COMP | +| List | `listCOMP` | Scrollable list with row callbacks | + +### Button + +```python +btn = root.create(buttonCOMP, 'play_btn') +btn.par.w = 120; btn.par.h = 40 +btn.par.buttontype = 'momentary' # 'momentary' | 'toggleup' | 'togglepress' | 'radio' +btn.par.bgcolorr = 0.1; btn.par.bgcolorg = 0.1; btn.par.bgcolorb = 0.1 +btn.par.text = 'Play' + +# Read state +state = btn.panel.state # 1 when active +``` + +### Slider + +```python +sld = root.create(sliderCOMP, 'master_fader') +sld.par.w = 60; sld.par.h = 300 +sld.par.style = 'vertical' # 'vertical' | 'horizontal' | 'xy' +sld.par.value0min = 0.0 +sld.par.value0max = 1.0 + +# Drive a parameter via expression (always-on, no callback needed) +op('/project1/master_level').par.opacity.mode = ParMode.EXPRESSION +op('/project1/master_level').par.opacity.expr = "op('master_fader').panel.u" +``` + +`panel.u` and `panel.v` give the 0-1 normalized values. For 2D sliders both are populated. + +### Field (Text Input) + +```python +fld = root.create(fieldCOMP, 'scene_name') +fld.par.w = 200; fld.par.h = 30 +fld.par.fieldtype = 'string' # 'string' | 'integer' | 'float' + +# Read current text +text = fld.panel.field # the text content +``` + +### List + +For scrollable lists with selectable rows, use the docked `list1_callbacks` DAT to handle row interactions. Set up cells via the `list_definition` table DAT. + +--- + +## Container COMP — Layout & Styling + +`containerCOMP` is the primary parent for grouping widgets and arranging layouts. + +```python +panel = root.create(containerCOMP, 'control_panel') +panel.par.w = 400; panel.par.h = 600 +panel.par.bgcolorr = 0.05 +panel.par.bgcolorg = 0.05 +panel.par.bgcolorb = 0.05 +panel.par.bgalpha = 1.0 + +# Layout child panels in vertical stack +panel.par.align = 'lefttoright' # 'lefttoright' | 'toptobottom' | etc. +``` + +Children are positioned automatically based on `par.align`. For absolute positioning use `par.align = 'fillresize'` and set each child's `par.x` / `par.y`. + +### Layout Strategies + +| `par.align` | Behavior | +|---|---| +| `lefttoright` | Children stacked horizontally | +| `toptobottom` | Children stacked vertically | +| `righttoleft` / `bottomtotop` | Reversed stacks | +| `fillresize` | Children sized to fill, manual positioning | +| `top` / `bottom` / `left` / `right` | Fixed positioning | + +For complex grids: nest containers — vertical container holding horizontal containers. + +--- + +## Panel Callbacks — Reacting to Events + +`panelExecuteDAT` watches a panel and fires Python callbacks on user interaction. + +```python +pe = root.create(panelExecuteDAT, 'btn_handler') +pe.par.panel = '/project1/play_btn' +pe.par.click = True # respond to clicks +pe.par.value = True # respond to value changes +``` + +In its docked DAT: + +```python +def onOffToOn(panelValue): + # Click pressed + op('/project1/scene_timer').par.start.pulse() + return + +def onOnToOff(panelValue): + # Click released + return + +def onValueChange(panelValue): + # Slider drag, field change, etc. + new_val = panelValue.eval() + op('/project1/master').par.opacity = new_val + return +``` + +For pulse params on custom-parameter pages, use a `parameterExecuteDAT` instead. + +--- + +## Building a Complete VJ Control Panel + +End-to-end pattern: + +```python +# 1. Top-level container +panel = root.create(containerCOMP, 'vj_control') +panel.par.w = 800; panel.par.h = 200 +panel.par.align = 'lefttoright' + +# 2. Master fader column +master_col = panel.create(containerCOMP, 'master') +master_col.par.w = 120; master_col.par.h = 200 +master_col.par.align = 'toptobottom' + +master_label = master_col.create(textTOP, 'lbl') +master_label.par.text = 'MASTER' + +master_sld = master_col.create(sliderCOMP, 'fader') +master_sld.par.w = 60; master_sld.par.h = 150 +master_sld.par.style = 'vertical' + +# 3. Scene buttons row +scene_col = panel.create(containerCOMP, 'scenes') +scene_col.par.w = 400; scene_col.par.h = 200 +scene_col.par.align = 'lefttoright' +for i in range(8): + b = scene_col.create(buttonCOMP, f'scene_{i+1}') + b.par.w = 50; b.par.h = 50 + b.par.text = str(i+1) + b.par.buttontype = 'radio' # only one active at a time + +# 4. FX toggle column +fx_col = panel.create(containerCOMP, 'fx') +fx_col.par.w = 280; fx_col.par.h = 200 +fx_col.par.align = 'toptobottom' +for fx in ['Bloom', 'CRT', 'Glitch', 'Strobe']: + t = fx_col.create(buttonCOMP, fx.lower()) + t.par.w = 220; t.par.h = 35 + t.par.text = fx + t.par.buttontype = 'toggleup' + +# 5. Display in a window +win = root.create(windowCOMP, 'control_win') +win.par.winop = panel.path +win.par.winw = 800; win.par.winh = 200 +win.par.borders = True +win.par.winopen.pulse() +``` + +Then wire panel values to ops via expressions or panelExecuteDATs. + +--- + +## Showing the Panel — Window or Embedded + +| Approach | When | +|---|---| +| `windowCOMP` pointing at panel | Standalone control surface, separate display | +| Render the containerCOMP via `renderTOP` | Composite UI over visuals (HUD-style) | +| Use a `panelCOMP` directly inside a network editor pane | Designer/dev preview only — panel is fully interactive | + +For a touch-screen tablet, use a `windowCOMP` on a second display routed to the tablet's HDMI input. + +--- + +## Pitfalls + +1. **Panel won't respond to clicks** — likely `par.disabled = True` or the parent container has `par.disableinputs = True`. Check the panel hierarchy. +2. **Slider value not updating** — `panel.u/v` reads the visual position. If you set `par.value0` directly, the visual lags. Use `par.value0` AS the source of truth and let the slider follow. +3. **Custom param won't appear** — must call `appendCustomPage` first, then append params. Pages with no params don't show. +4. **Custom param disappears on reload** — params added via Python at runtime persist only if the COMP is saved AFTER. Use a `tox` save (`comp.save('mycomp.tox')`) or commit via `td_execute_python` then save the project. +5. **Event callback fires twice** — both `onOffToOn` and `onValueChange` may fire on a single button press. Pick one to handle the action; don't double-trigger. +6. **Pulse params need `.pulse()`** — setting `par.X = True` on a pulse param does nothing. Always use `.pulse()`. +7. **Field text doesn't commit until Tab/Enter** — fields don't fire callbacks while typing. Use `par.committemode = 'all'` to fire on every keystroke (heavy). +8. **`par.text` vs panel content** — `buttonCOMP.par.text` is the LABEL on the button. The button's STATE is `panel.state` (0/1). Don't confuse them. +9. **Touch input on macOS** — multi-touch via direct touch panels works but TD's gesture handling is rudimentary. For complex multi-touch (pinch/rotate), use TouchOSC on a tablet instead. +10. **Layout doesn't update** — changing `par.align` requires the container to re-cook. Touch a child or pulse the container to trigger. + +--- + +## Quick Recipes + +| Goal | Setup | +|---|---| +| Master fader | `sliderCOMP` (vertical) → expression on `level.par.opacity` | +| Scene picker | 8 `buttonCOMP` (radio) → `selectCHOP` on their state → drive `switchTOP.par.index` | +| FX toggle | `buttonCOMP` (toggleup) → expression on `bypass` of an FX op | +| Numeric input | `fieldCOMP` (float) → expression on target par | +| Component settings | Custom params on the component COMP, panel widgets inside drive them | +| Touch tablet UI | `containerCOMP` with widgets → `windowCOMP` to second display | +| Status display | `textTOP` rendered into the panel via `selectCOMP` | diff --git a/skills/creative/touchdesigner-mcp/references/replicator.md b/skills/creative/touchdesigner-mcp/references/replicator.md new file mode 100644 index 0000000000..5b9cd3da3d --- /dev/null +++ b/skills/creative/touchdesigner-mcp/references/replicator.md @@ -0,0 +1,198 @@ +# Replicator COMP Reference + +The `replicatorCOMP` clones a template operator N times, driven by a table of data. The fundamental TD pattern for data-driven networks: button grids, scene rosters, dynamic UI, parameter panels per-channel. + +For visual instancing (per-pixel/per-render copies), see `geometry-comp.md`. Replicator builds NETWORK NODES; instancing builds RENDER COPIES. Different layer. + +--- + +## Concept + +``` +[Template OP] [Data tableDAT] + │ │ + └─────→ replicatorCOMP ←───────┘ + │ + ▼ + [N clones], one per data row + Each clone gets per-row params +``` + +Edit the template once → all clones inherit. Edit the table → clones add/remove dynamically. Push parameter overrides per-row. + +--- + +## Minimal Setup + +```python +# 1. Make a template (the thing to clone) +template = root.create(buttonCOMP, 'btn_template') +template.par.w = 80; template.par.h = 80 +template.par.text = 'X' +template.par.bgcolorr = 0.2 + +# 2. Make a data table (one row per clone) +data = root.create(tableDAT, 'scene_data') +data.appendRow(['name', 'color_r', 'color_g', 'color_b']) +data.appendRow(['Sunset', 1.0, 0.4, 0.0]) +data.appendRow(['Midnight', 0.0, 0.1, 0.4]) +data.appendRow(['Storm', 0.3, 0.3, 0.5]) +data.appendRow(['Forest', 0.0, 0.5, 0.2]) + +# 3. Replicator — points at template + data +rep = root.create(replicatorCOMP, 'scene_buttons') +rep.par.template = template.path +rep.par.opfromdat = data.path +rep.par.namefromdatname = 'name' # use 'name' column for clone names +rep.par.incrementalnumbering = False +``` + +After cooking, the replicator creates 4 child COMPs named `Sunset`, `Midnight`, `Storm`, `Forest` (one per non-header row), each cloned from `btn_template`. + +--- + +## Per-Row Parameter Overrides + +The replicator's docked `replicator1_callbacks` DAT lets you customize each clone: + +```python +def onReplicate(comp, allOps, newOps, template, master): + """Called once per replicate cycle. newOps is the list of just-created clones.""" + data = op('scene_data') + for i, clone in enumerate(newOps): + row = i + 1 # +1 to skip header + clone.par.text = data[row, 'name'].val + clone.par.bgcolorr = float(data[row, 'color_r'].val) + clone.par.bgcolorg = float(data[row, 'color_g'].val) + clone.par.bgcolorb = float(data[row, 'color_b'].val) + return +``` + +Or use parameter expressions referencing `digits` (the per-clone index, available as a built-in expression token inside the cloned subtree): + +```python +# Inside the template, set a param expression like: +# par.value0.expr = "op('../scene_data')[me.digits + 1, 'value']" +``` + +`me.digits` resolves to the row index of the current clone. This is the cleanest way for static reference patterns — no callback needed. + +--- + +## Layout: Buttons in a Grid + +Drop the replicator inside a `containerCOMP` with auto-layout: + +```python +panel = root.create(containerCOMP, 'scene_panel') +panel.par.w = 400; panel.par.h = 100 +panel.par.align = 'lefttoright' + +# Move the replicator inside +rep.parent = panel.path # or create rep as a child of panel directly +``` + +Each clone is a child of the replicator (which itself is a child of the panel). The panel auto-arranges everything. + +For a 2D grid, set `par.align = 'fillresize'` on the container and override `par.x` / `par.y` per clone in the callback based on row/col index. + +--- + +## Updating Without Rebuilding + +When the data table changes, the replicator regenerates the clones. By default it destroys and recreates everything. To preserve state, set: + +```python +rep.par.recreatemissing = True # only add/remove changed rows +rep.par.recreateallonchange = False +``` + +This pattern is essential for live-edit scenarios (designer adjusts table, network keeps running). + +For incremental data ingestion (e.g., from a `webDAT` polling an API), have a `datExecuteDAT` watch the response, parse, write to the data table, and the replicator self-updates. + +--- + +## Common Patterns + +### Scene Roster (Data → Buttons + Logic) + +```python +# Data per scene: name, file path, audio track, BPM +scene_data.appendRow(['name', 'file', 'audio', 'bpm']) +scene_data.appendRow(['Intro', '/scenes/intro.tox', '/audio/intro.wav', 110]) +scene_data.appendRow(['Main', '/scenes/main.tox', '/audio/main.wav', 128]) + +# Replicator clones a buttonCOMP per scene +# Each button's onClick callback loads the corresponding tox + cues audio +``` + +### Dynamic Parameter Panel + +For a list of audio bands, generate a fader strip per band: + +```python +# Data: band names (sub, low, mid, hi-mid, high, air) +# Template: containerCOMP with label + sliderCOMP +# Replicator clones N strips +# Each slider's value is read at /audio_eq/{band_name}/fader +``` + +### Procedural Visual Network + +Build a multi-channel visual network from a config file: + +```python +# Data: which TOPs to chain, per "scene" +# Template: a baseCOMP with placeholder children +# Replicator builds one baseCOMP per scene; each scene contains a custom chain +# Switch between scenes via switchTOP.par.index driven by panel +``` + +### Per-Channel CHOP Display + +Visualize each channel of a multi-channel CHOP separately: + +```python +# Data table: one row per channel (auto-extracted via choptodatDAT) +# Template: a small chopVis COMP showing one channel +# Replicator generates N visualizers stacked vertically +``` + +--- + +## Replicator vs. Pure Python Loop + +| Approach | When to use | +|---|---| +| **replicatorCOMP** | The set of clones changes (add/remove rows live). Visual editor expectations. Pattern is reusable across projects. | +| **Python loop** (in `td_execute_python`) | One-shot generation. Static set. Simpler logic, no template overhead. Faster to write. | + +If you'll only ever build the network once, prefer a Python loop with `td_execute_python`. The replicator earns its weight when data is live. + +--- + +## Pitfalls + +1. **Header row** — `tableDAT` rows are 0-indexed. If you have a header, your first data row is index 1. Off-by-one bugs are common in callbacks. +2. **`namefromdatname` column missing** — replicator silently uses `digits` (numeric suffix) names. Buttons end up named `1`, `2`, `3` instead of meaningful names. Set `par.namefromdatname` explicitly. +3. **Template lives in network** — the template OP is itself a real network node. Don't connect things downstream of it directly; connect to the clones (or use a `nullCOMP` between). +4. **Recreate-on-change wipes state** — toggles, slider positions, and uncached data inside clones are lost on each regeneration. Use `recreatemissing` to preserve. +5. **`onReplicate` doesn't fire on edit** — only fires when the clone set changes. Editing a value WITHIN an existing row doesn't re-trigger. Use `parameterExecuteDAT` or expressions for per-cell live updates. +6. **Custom params on clones** — pages added in the template propagate. Pages added in `onReplicate` don't survive the next regeneration. Always add custom pages on the template, not the clone. +7. **Cooking storms** — adding many rows fast triggers many clone events. Bundle adds via Python and call `data.cook(force=True)` once at the end. +8. **`me.digits` outside replicator children** — `me.digits` only resolves inside an op that's a descendant of the replicator. Don't reference it in unrelated networks. +9. **Cross-clone references** — referencing a sibling clone via relative path works from inside a clone (`op('../OtherClone/x')`), but breaks if names change. Prefer absolute paths via the data table. + +--- + +## Quick Recipes + +| Goal | Setup | +|---|---| +| 8-button scene picker | `tableDAT` (8 rows) + `buttonCOMP` template + `replicatorCOMP` | +| Per-band EQ strip panel | `tableDAT` (band names) + container template (label + slider) + replicator | +| Data-driven visual scenes | `tableDAT` (scene config) + `baseCOMP` template (visual chain) + replicator | +| Live-updating clone set | Same as above + `par.recreatemissing = True` | +| Per-row colored UI | Data table with color cols, `onReplicate` callback sets per-clone colors | +| List from API response | `webDAT` → `datExecuteDAT` parses JSON → writes to data table → replicator updates |