LLMSkillHub
mirroredCopied from dylanroscover/Embody under MIT. dylanroscover has not claimed this page, and is not affiliated with LLM SkillHub.

dylanroscover / td-api-reference

MUST READ before writing TD Python via execute_python, set_dat_content, or edit_dat_content, and before any heavy build. Parameters, storage, operators, referencing, threading, cook model, heavy-build safety caps.

0
SKILL.md
14.3 KB

<!-- Generated by Embody/Envoy - Do not remove this comment - sha:1517fd24f8397a92 -->

TouchDesigner Python API Reference

Verify unfamiliar TD behavior before asserting it: get_docs('<Operator or Class>') reads the offline help mirror first, the wiki (https://docs.derivative.ca: OP Class, COMP Class, Par Class, Extensions, Cook) second, and describe_op_type gives an operator type's real parameter names. Assumptions about TD's Python API are frequently wrong. Two companions in this skill's folder hold the long tail: references/background-work.md (the Web Client DAT and Thread Manager code) and references/heavy-build-safety.md (crash causes and caps). Read them before any fetch, thread or heavy build; without a skills folder, get_guidance(topic='td-api-reference/background-work').

Parameter Access Patterns

value = op('geo1').par.tx.eval()   # CORRECT: the current runtime value
value = op('geo1').par.tx.val      # WRONG: constant-mode value only
op('geo1').par.tx = 5              # set; .val = 5 also silently switches the mode to CONSTANT
op('geo1').par.xord = 'trs'        # menus take the name or the index
me.par.tx.eval().hex()             # cast AFTER eval(); me.par.tx.hex() is wrong

Creating Custom Parameters

All append* methods return a ParGroup (tuple-like), never a single Par: index with [0].

page = comp.appendCustomPage('Controls')
p = page.appendFloat('Speed', label='Speed')[0]
p.default = 0.5; p.val = p.default          # .default never sets the value
p.normMin = 0; p.normMax = 2                # slider range
p.min = 0; p.clampMin = True                # hard clamp
p.help = "Playback speed multiplier."       # ALWAYS set help
p.startSection = True                       # separator above
# appendInt / appendToggle / appendStr / appendMenu (EMPTY menu: set .menuNames/.menuLabels)
# appendPulse / appendRGB ('Color' -> Colorr/g/b) / appendXYZ ('Pos' -> Pos1/2/3) / appendOP / appendFile

Names start uppercase, then lowercase letters and digits, no underscores. Get-or-create (never a blind append*), callbacks, styles, ranges, help text: /parameter-design. Docs: https://docs.derivative.ca/Custom_Parameters

op() vs opex()

op('/missing') returns None silently; opex('/missing') raises tdError with a clear message. Use op() only when None is an acceptable result. ops('noise*') returns a list (wildcards). opex('level1').asType(levelTOP, checkType=True) is the typed, runtime-asserted one-liner for a known type.

Operator Referencing Patterns

Absolute paths (op('/project1/...')) are always wrong, in code, expressions AND parameter values. Take the highest rung that resolves from where the code runs:

RungPatternUse whenPrerequisite
1self.ownerCompAnywhere inside an extension; capture it once in __init__none
2parent.CompNameInside the COMP but outside the extension: callback DATs, parameter expressionsthe COMP sets par.parentshortcut
3op('sibling'), op('./child'), op('../up_one')The target is in the same network or nearbynone
3biop.Name / ipar.NameAn internal op or parameter the COMP publishes for its own descendantsInternal OP shortcuts on the Common page
4op.CompNameA project-wide singleton no parent shortcut can reachthe COMP sets par.opshortcut
  • parent.CompName searches UP by shortcut name and survives renesting; parent(2) encodes the depth and breaks the moment anything moves. Set par.parentshortcut when you create a reusable COMP.
  • The bare global parent() / parent(N) is banned inside an extension class body: use self.ownerComp. Exemptions: a COMP's own callback DAT (where parent() IS the owner), generic tooling inside a COMP it did not author, parameter expressions on replicated or cloned widgets. some_op.parent() is documented OP-class API and untouched.
  • iop.Name names one internal operator, ipar.Name a parameter-holding COMP; inside-only, and an operator-valued internal par needs op(ipar.X.Opname). Reserve them for stable internal addresses.
  • op.CompName is globally unique: assigning a name in use steals it from the previous holder. Use it for singleton services (op.Embody.Log(...)), otherwise prefer parent.CompName.
  • Always verify a reference resolves from the calling context; a reference that returns None (or finds the wrong operator) is a latent bug.
  • After ANY rename (operator, method or parameter) sweep every live surface, not just DAT text on disk: DAT text inside the .toe (findChildren(type=DAT) and read .text), par.expr, par.bindExpr, constant-mode OP-reference values (par.val), and run() strings composed at call time (issue #94: an agent swept DAT content only and left stale expressions and binds behind). findChildren(depth=N) matches EXACTLY depth N, a silent all-clear for anything shallower; walk with no depth argument or maxDepth=N.

Type Hints

Annotate every def (arguments AND return) and let a checker read them; Python ignores them at runtime. TD's classes are the types (COMP, DAT, OP, Par, constantCHOP, ...); put from __future__ import annotations at the top of a module DAT. x: str = None declares a str that is None: write Optional[str] = None. A callback returns -> None; a query that can miss returns -> Optional[COMP]. The hint is the soft check; asType(..., checkType=True) is the runtime assertion. Check softly, in CI, with a baseline (pyright basic, failing only on NEW errors).

Everyday Essentials

  • debug('value is', x) prints with the source DAT and line number; print() does not.
  • Never call op(), parent() or touch TD objects at module level: module code runs at import, before the network is ready. Defer to methods.
  • A DAT named json shadows Python's json: TD searches DATs by name before sys.path.
  • mod.utils.func() re-resolves the DAT every call; in a loop cache it (m = mod.utils). In a parameter expression import is unavailable, mod.utils.f() works. mod('utils') resolves relative to the current op, never an absolute path.
  • Expressions that read a promoted attribute guard with parent().X if parent().extensionsReady else 0.
  • changeType() returns the NEW op (capture it); copyOPs([list]) preserves wires, COMP.copy() does not; addError()/addWarning() work only in cook callbacks (use addScriptError() from extension code).

onInitTD and TDXN Import Timing

Inside a TDXN-strategy COMP, onInitTD fires BEFORE the import that rebuilds the children (project open around frame 60, every Ctrl+S strip/restore, every manual reimport with clear_first=True), so anything it creates, sets or stores inside the COMP is wiped. Defer network-dependent setup with run('args[0].postInit()', self, delayFrames=5), keep it idempotent (it runs several times), null-check children (op('child') is None during the strip window), and keep durable state OUTSIDE the boundary or in store() on the COMP.

Parameter, Storage, or Dependency?

User-facing state is a custom parameter; hidden durable bookkeeping is storage (lives on the COMP, survives an extension reinit, wiped when the COMP's contents are replaced by a tox reload or TDXN reconstruction); a derived value that must recook its readers is a tdu.Dependency (dies with the extension instance on every source save, rebuilt in __init__). Publish once and let readers pull; never push values into consumers. The lifetime table, what actually notifies dependents, and the storage mechanics are measured in /parameter-design and its references/state-lifetimes.md.

op('base1').store('count', 42)                # write THROUGH store(); comp.storage[k] = v notifies nobody
val = op('base1').fetch('count', 0)           # no default -> a miss RAISES; fetch() searches UP the parents
op('base1').unstore('count')                  # glob-matched: never a computed key
op('base1').storeStartupValue('version', 1)   # a separate dict restored on load

dep = tdu.Dependency(0)
dep.val = 5            # CORRECT: recooks readers. dep = 5 destroys the Dependency
dep.val.append(4); dep.modified()             # in-place mutation needs modified()
current = dep.peekVal  # read without subscribing

tdu.clamp, tdu.remap, tdu.rand(seed), tdu.base('noise3') -> 'noise', tdu.digits, tdu.validName, tdu.match, tdu.expand('A[1-3]'), tdu.tryExcept(expr, fallback).

DAT, CHOP and TOP Access

  • DAT cells are strings; n[1,2] + 1 auto-casts, n[1,2].val + 1 raises. dat.text strips multi-line cells (dat.csv keeps them); dat.jsonObject parses directly; dat.write() APPENDS; dat.module imports it.
  • CHOP: op('noise1')['chan1'] by name (no wildcard), .chans('tx*') for patterns, ch.eval(), ch[10], ch.evalFrame(30), numpyArray() shaped (numChans, numSamples).
  • TOP coordinates: TOP.sample(x, y), GLSL gl_FragCoord, UVs and Crop/Transform parameters put (0, 0) at the BOTTOM-LEFT with Y up. TOP.numpyArray() follows the same y-up order: row 0 is y = 0, the bottom of the image (shape [height, width, channels]; measured 2025.33230 against sample() on a vertical ramp). PIL/OpenCV images and panel screen coordinates are top-down, so np.flipud converts between the two. sample() downloads the whole texture: never in a loop. numpyArray() returns linear-light floats, not the sRGB bytes a file decode gives: compare same-domain only.
  • POPs: pop.numPoints(delayed=True) and bounds(delayed=True) are non-blocking; pop.points('P') downloads from the GPU. fileinPOP (meshes) and pointfileinPOP (point clouds, splats) are distinct operators.
  • run("me.cook(force=True)", fromOP=op('base1'), delayFrames=1), run(fn, arg, delayMilliSeconds=500), run("...", endFrame=True); resolve ops through fromOP or shortcuts, never absolute paths.

Cook Model Gotchas

  • cook(force=True) does not advance a feedback loop within a frame: evolution needs real frames with the chain demanded (run(..., delayFrames=1) or an Execute DAT onFrameStart), never a for loop.
  • A Movie File In reload (par.file, reloadpulse) lands only across a real frame advance, and ops downstream in the same forced pass can still serve the pre-reload texture. Verify content at the point of capture and let each reload settle (/movie-export).
  • A CHOP Execute DAT is a per-frame demand source: its watched CHOP cooks every frame (measured 2025.33230). An Execute DAT's framestart toggle is OFF by default, so a frame driver that was never enabled demands nothing.
  • Animate cheaply: make a heavy generator static (no time reference, cooks once) and put the motion in a cheap downstream op that animates the SAMPLING. Confirm with cookedThisFrame.

Background and Long-Running Work

Ironclad rule: a worker thread touches NO TD object, and a read counts as a write. No op()/opex(), no Par (not even .eval()), no DAT/CHOP/SOP/TOP content, no storage, no tdu.Dependency, no debug()/print(), and never run()/td.run() from a worker: it does not raise, the scheduled code even runs, and the crash lands later far from the call site (Derivative-confirmed 2026-08-17). A worker gets pure Python, tdu math utilities, values resolved on the main thread and passed in, queue/threading primitives, td.isMainThread(), and the Thread Manager's InfoQueue/SafeLogger; it hands plain data back for a main-thread callback to apply.

Match the rung to the problem; threading is the LAST resort:

  1. Prototype a fetch synchronously once to prove URL, auth and parse; never ship it.
  2. Fast, TD-only, no I/O: inline. Any network, disk or subprocess call is never this rung.
  3. Fetch data with a native operator: Web Client DAT (request(url, 'GET', timeout=8000) returns at once; onResponse runs on the MAIN thread), WebSocket DAT, Web Server DAT, OSC, File In / Folder DAT. Parse with a JSON DAT, bridge with DAT to CHOP.
  4. Long TD-touching work: chunk with run(delayFrames=N); run() controls WHEN, not HOW MUCH.
  5. Blocking pure-Python work: the Thread Manager (Palette Thread Manager Client preferred), a target with zero TD access, results applied in main-thread hooks. Never EnqueueTask() from a worker.
  6. Long-lived loop or server: ThreadManager standalone=True or a top-level threading.Thread with zero TD access draining a queue.Queue from a main-thread callback armed BEFORE the worker starts.

Trigger a fetch from a Pulse parameter or a Timer CHOP (cycle on, limit off), never a sleep loop, a self-rescheduling run() poller or an auto-fetch on open. Gate with primary evidence: fps and droppedFrames held (get_project_performance) AND the result actually arrived. requests has no default timeout and blocks the frame: timeout=(2, 8), and only ever inside a worker. Code patterns: references/background-work.md.

Heavy-Build Safety

Load references/heavy-build-safety.md (crash causes with their warning metrics and mitigations) before render chains, feedback loops, instancing, large geometry, GLSL, high-resolution TOPs or many operators at once; the gating protocol and stop conditions in performance.md always apply. The caps in one breath: TOPs <= 1920x1080 in 8/16-bit fixed formats unless asked; feedback loops bounded (fixed resolution, decay below 1, reset wired, bypassed while wiring, terminated in a Null); heavy sources bypassed while wiring; GPU instancing or POPs over Copy SOP, counts ramped with metrics (CPU particles ~10k max); Time Slicing on and small buffers for CHOPs; constant-bounded GLSL loops with TD_NUM_* index guards; short non-blocking execute_python calls with no sleep, no TOP.sample() in loops, and large builds chunked across frames. allowCooking = False is COMP-only; gate other operators with bypass.

Pre-Installed Packages and Types

numpy and cv2 are documented as bundled; requests, yaml, cryptography, attrs are commonly present (verify in your build). Auto-imported stdlib: math, re, sys, collections, enum, inspect, traceback, warnings. TD parameters auto-cast in expressions but stay TD objects: convert with int(), float(), str() before handing them to standard Python; repr() reveals the real type.