dylanroscover / create-operator
MUST READ before creating or moving any operator (create_op, copy_op, set_op_position, or execute_python builds). Required verification, canonical positioning recipe, and error-checking steps.
<!-- Generated by Embody/Envoy - Do not remove this comment - sha:8f0ca97bbdf8c00f -->
Create Operator Workflow
Follow these steps every time you create operators via MCP:
Choose the correct parent network -- ASSOCIATE with where the user already works. Inconsistent placement is the #1 scoping bug (
/one run,/project1the next). The fix is to anchor on the user's own structure, which is stable across runs -- not on a transient pane:- NEVER create under
/localor/local/*-- volatile storage, not saved with the.toe. - If the user named a target, use it.
- Default home = the container that holds the
EmbodyCOMP.execute_pythonwithresult = op.Embody.parent().path. Embody in/project1-> build in/project1; Embody at the root/-> build at/. It returns the SAME home every run -- this is the consistency mechanism, and it is the level the user chose by placing Embody there. - Override only for deliberate navigation. If the user has actively opened a specific content network to work in, build there instead (
ui.panes.current.owner.path) -- UNLESS that pane is sitting at the bare root/(a non-deliberate default, e.g. right after the project opens), in which case ignore it and use the Embody home above. Never treat bare/as a content home. - Discover, never guess. Confirm the real container names with
query_networkon/-- never hardcode/project1(it may be renamed, and there may be more than one). - Keep one task's COMPs together under the SAME parent, grouped in one container / annotation. Don't split related COMPs across levels.
- NEVER create under
- Discover the target network:
query_networkon the target parent to confirm it exists and see existing operators - Scan existing layout: Use
get_network_layouton the parent COMP. Note each operator'snodeX,nodeY,nodeWidth, andnodeHeight- operators vary in size (100-300+ units wide) - Plan positions BEFORE creating: Batch-compute grid-aligned positions for ALL operators you intend to create. Signal flow is left-to-right: inputs on the left, outputs on the right. Supporting operators (DATs feeding a TOP, CHOPs feeding parameters) go to the left of or below the operator they feed. Snap all coordinates to the 200-unit grid. See the Positioning Rules section below.
- Create each operator:
create_opwith the desired type and name - Position each operator:
set_op_positionto place it at the pre-computed grid position. Auto-placement is NOT acceptable - it produces messy, unreadable networks. You MUST explicitly position every operator you create. Moving a host viaset_op_positioncarries its docked companions along (re-hugged below the new spot, reported asdocks_moved), so position the host FIRST and any deliberately-placed dock after. - Docked companions (callback/shader/info DATs):
create_op,copy_op, andset_op_positionauto-hug every docked op in a tight row ~30 units below its host (docks_placed/docks_movedin the result) - do not re-plan grid slots for them. Ops created insideexecute_pythonget NO such placement at create time (Envoy only auto-hugs badly scattered docks after the call, with aLAYOUT WARNING); if you create dock-spawning ops (GLSL TOP/MAT, execute DATs, OSC in/out) in a script, place their docks yourself per the Docked companion layout formula below. - Connect:
connect_opsto wire inputs/outputs. Wires must flow left-to-right (positive X). If a wire would go backward, the downstream op is misplaced - reposition it. - Set OP-reference parameters with relative paths: If the operator has parameters referencing other operators (Camera, Geometry, Lights, TOP, CHOP, etc.), use sibling names (
cam) or relative paths (../shared/lut) - NEVER absolute paths (/project1/scene/cam). Seeparameters.mdsection OP-Reference Parameter Values. - Verify layout: Call
get_network_layoutagain. Confirm no overlaps, grid alignment is intact, signal flows left-to-right, and every entry carryingdockedTosits in a tight row just below its named host (docked ops are the one exception to 200-grid spacing - they hug). - Verify errors:
get_op_errorswithrecurse=trueto check for errors and warnings. Fix all errors before considering the task complete - Visual verification: For any renderable result (a TOP chain or a render), capture the output TOP with
capture_topand confirm it actually renders (not black) and matches intent; for a 3D render, confirm a camera, a light, and geometry display/render flags are present. For anything but a trivial op, load the/visual-aestheticsskill to judge composition/value/color/contrast.
Operator Type Preferences
- Prefer POPs over SOPs for geometry and particle work. POPs (Point Operators) are GPU-accelerated and significantly more performant than SOPs (Surface operators). Only use SOPs when POP equivalents don't exist or when CPU-side geometry manipulation is specifically required.
Geometry COMP: delete the default torus
A freshly created geometryCOMP ships with a default torus1 SOP inside, render flag ON. The moment you add your OWN geometry to that COMP -- a SOP chain, a POP chain terminating in a nullPOP, or imported geometry -- delete torus1 (or turn OFF its render flag). It is the RENDER flag (purple) that selects a SOP for the Render TOP, so leaving it on draws BOTH: your geometry AND a phantom torus. It is easy to miss: adding your own SOP auto-clears the torus's DISPLAY flag (blue, exclusive per viewer) so the SOP / geometry viewer looks correct, while its RENDER flag (non-exclusive) stays on and keeps drawing it in the render output. Applies to EVERY geometryCOMP you populate, not just POP builds. (TDXN import already strips these auto-created defaults, so this bites only live create_op builds -- which is why it shows up in some projects and not others.)
Positioning Rules (canonical layout recipe)
- Every operator MUST be explicitly positioned on the 200-unit grid. Do NOT rely on auto-placement.
Spacing is
size + gap, BOTH axes, ALWAYS -- never a fixed step. This is the #1 overlap bug: stepping by a constant (e.g.nodeY - 100,nodeX + 300) while the tiles are bigger than the step (a 134-tall node stepped by 100 overlaps by 34). The step MUST be computed from the ACTUAL node extent on that axis plus a gap, then snapped up to the grid:- Horizontal:
next_x = prev_nodeX + prev_nodeWidth + gap(gap >= 200), rounded up to the next 200 multiple. - Vertical:
next_y = prev_nodeY - (prev_nodeHeight + gap), rounded so the step clears the tile. For a stack of same-size tiles:step = ceil((maxNodeHeight + gap) / 200) * 200and place tileiaty = -i * step. A 134-tall stack ->step = 200(66 gap); a 260-tall stack ->step = 400. Bigger nodes => bigger spacing, mechanically.
- Horizontal:
- The grid is the SNAP target, not the step size -- when a node is taller/wider than 200, the step is 400/600/... (the next multiple that clears
size + gap), NEVER a flat 200. 400 units vertical minimum between parallel chains or annotation groups (widen when tiles are tall). Y increases upward; new rows go downward. - Use actual dimensions, not assumptions. COMPs (~160+ wide) and panel widgets (often 130-160 wide x 130+ tall) are much bigger than a bare TOP (~120 sq) -- a step that clears one overlaps the other.
- Signal flow is left-to-right; wires must flow forward (positive X) -- never a backward "S" wire. Every operator must have a higher
nodeXthan each operator feeding it. The classic mistake: placing a source at the SAMEnodeXas its destination (directly above or below) -- the source's output then sits right of the dest's input and the wire loops back. Place each source so its right edge (nodeX + nodeWidth) is left of the destination'snodeX. If a wire bends backward, move the downstream op right or the source left. - Branches split vertically, each continuing left-to-right; minimize crossings. Same row = same stage in a chain (same X); same column = same function across parallel chains (same Y).
- New operators extend a group to the RIGHT (
rightmost_nodeX + rightmost_nodeWidth + 200, snapped); a parallel chain goes DOWN (lowest Y in group - 400). Don't fill a gap mid-row of a finished chain. - Batch-compute ALL positions before placing anything; after placing,
get_network_layoutto verify.
Related operators stay close
Operators that reference each other belong near each other, even with no wire or dock: a MAT beside the Geometry COMP it shades; a camera and light(s) near their Render TOP; a CHOP/DAT that drives parameters by reference near the op it drives; any COMP named in an op() call, expression, or material slot near the op that references it. Rule of thumb: if op A names op B, a reader should see both without scrolling.
Panel-COMP widgets stack too
Buttons/fields/containers inside an align-driven panel (Container/Button laid out verttb/horizlr) render by their align + order pars, so their nodeX/nodeY are cosmetic to the UI -- BUT they still get network tiles, and a verttb container full of 134-tall buttons stepped by 100 overlaps in the editor exactly like data-flow ops do. Apply the same size + gap stepping to panel-widget tiles (stack by panel order); it never affects the rendered panel. This is the one layout class the LAYOUT WARNING lint does NOT police -- it is on you to tidy.
Docked companion layout formula
Hosts that dock: execute/callback DATs (chopExecuteDAT, datExecuteDAT, panelExecuteDAT, parameterExecuteDAT, executeDAT); input DATs (keyboardinDAT, mouseinDAT, oscinDAT/oscoutDAT); and GLSL ops -- a glslTOP/glslmultiTOP docks a pixel DAT, a compute DAT, AND an info DAT (the multi variant also docks a vertex DAT); a glslMAT docks a vertex DAT, a pixel DAT, AND an info DAT (no compute DAT); a glslPOP docks a compute DAT and an info DAT.
Given host bottom-left (sx,sy), size (sw,sh), and N docks (use max dw,dh across docks). Docks HUG the host -- the one deliberate exception to 200-grid spacing:
- Row Y:
row_y = sy - dh - 30-- a tight ~30-unit gap below the host's bottom edge, NOT a full grid step. - Slot step:
step = dw + 20-- one compact cluster under the host. - Center the row: dock
iatnodeX = (sx + sw/2) + (i - (N-1)/2) * step - dw/2, filling per this table; overflow stacks into a second tight row (row_y - dh - 30) rather than widening:
| N | Pattern |
|---|---|
| 1 | [C] |
| 2 | [C, R] |
| 3 | [L, C, R] |
| 4 | [L, C, R, R2] |
| 5 | [L2, L, C, R, R2] |
If another op occupies a dock's slot, move that other op -- docked ops take priority, are never threaded around obstacles, never overlap, never stranded. For execute_python builds: query [d.path for d in op('PATH').docked], reposition EVERY dock per the formula, then get_network_layout to confirm. A glslTOP built inside execute_python is the classic trap.
Annotations and complexity
- Every operator inside exactly one annotation; no orphans; annotations never overlap (>= 400 units between edges); expand the annotation after every addition. Title names the FUNCTION, not the implementation ("Audio Mixing", not "CHOP chain 2").
- MUST invoke
/manage-annotationsbeforecreate_annotationorset_annotation--nodeX/nodeYis the bottom-left corner. - 4-5 annotation groups in one network: consider baseCOMPs (containerCOMPs for UI). 15-20 operators in one group: consider encapsulating into a COMP that replaces the group, moving the annotation title to the COMP name/label.
Anti-patterns
- Leaving
execute_python-created ops at (0, 0) / overlapping -- Envoy emits aLAYOUT WARNING, but position + verify regardless. - Fixed offsets like
nodeX + 300withoutnodeWidth-- the #1 cause of overlaps. - TD's
COMP.layout()-- overlapping, unreadable results. - Creating operators without updating the enclosing annotation;
set_op_positioninto an unverified slot (it has no overlap detection). - Leaving docked callback/info DATs at their auto-spawn position after a script build.
Key Reminders
- NEVER skip positioning - auto-placement produces unreadable networks
- NEVER place an operator on top of another operator - always scan first
- Never rely on
layout()for production networks - New operators go near related operators, not at origin
- Docked companions hug their host (~30 units below, never a full grid step away). The MCP tools enforce this; after any
execute_pythonbuild, check everydockedToentry inget_network_layoutyourself - For current network location:
execute_pythonwithresult = ui.panes.current.owner.path - Placing a COMP: anchor on where the user put Embody (
op.Embody.parent().path) for a consistent home; override only when the user has deliberately navigated into a content network (never bare/); discover, never guess,/project1; keep one task's COMPs in one home - New
geometryCOMP: delete its defaulttorus1the moment you add your own geometry, or it renders a phantom torus behind your scene