@dpantani/tdmcp 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -549,7 +549,7 @@ function registerAllResources(server, ctx) {
549
549
  }
550
550
 
551
551
  // src/tools/layer1/applyPostProcessing.ts
552
- import { z as z6 } from "zod";
552
+ import { z as z7 } from "zod";
553
553
 
554
554
  // src/feedback/errorChecker.ts
555
555
  async function checkErrors(client, path, recursive = true) {
@@ -624,11 +624,49 @@ async function connectNodesViaBridge(client, sourcePath, targetPath, sourceOutpu
624
624
  }
625
625
  const src = JSON.stringify(sourcePath);
626
626
  const dst = JSON.stringify(targetPath);
627
- const python = `op(${dst}).inputConnectors[${targetInput}].connect(op(${src}).outputConnectors[${sourceOutput}])`;
627
+ const python = [
628
+ `__s = op(${src}); __d = op(${dst})`,
629
+ `if __s is None or __d is None: raise LookupError('connect: source or target not found (%s -> %s)' % (${src}, ${dst}))`,
630
+ `if __s.parent() is None or __d.parent() is None or __s.parent().path != __d.parent().path: raise ValueError('connect: cannot wire across containers (%s -> %s); use a Select/In OP to bring an operator across networks' % (${src}, ${dst}))`,
631
+ `__d.inputConnectors[${targetInput}].connect(__s.outputConnectors[${sourceOutput}])`
632
+ ].join("\n");
628
633
  await client.executePythonScript(python, false);
629
634
  return { method: "python" };
630
635
  }
631
636
 
637
+ // src/tools/layer2/createControlPanel.ts
638
+ import { z as z6 } from "zod";
639
+
640
+ // src/tools/pythonReport.ts
641
+ function buildPayloadScript(template, payload) {
642
+ const b64 = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
643
+ return template.replace("__PAYLOAD_B64__", b64);
644
+ }
645
+ function parsePythonReport(stdout) {
646
+ if (!stdout) throw new TdApiError("The TouchDesigner script returned no output.");
647
+ const lines = stdout.split("\n");
648
+ for (let i = lines.length - 1; i >= 0; i--) {
649
+ const line = lines[i]?.trim();
650
+ if (!line) continue;
651
+ if (line.startsWith("{") && line.endsWith("}")) {
652
+ try {
653
+ return JSON.parse(line);
654
+ } catch {
655
+ }
656
+ }
657
+ break;
658
+ }
659
+ const start = stdout.indexOf("{");
660
+ const end = stdout.lastIndexOf("}");
661
+ if (start >= 0 && end > start) {
662
+ try {
663
+ return JSON.parse(stdout.slice(start, end + 1));
664
+ } catch {
665
+ }
666
+ }
667
+ throw new TdApiError(`Could not parse the TouchDesigner script result: ${stdout.slice(0, 200)}`);
668
+ }
669
+
632
670
  // src/tools/result.ts
633
671
  function textResult(text) {
634
672
  return { content: [{ type: "text", text }] };
@@ -664,6 +702,254 @@ async function guardTd(fn, onOk) {
664
702
  }
665
703
  }
666
704
 
705
+ // src/tools/layer2/createControlPanel.ts
706
+ var controlSchema = z6.object({
707
+ name: z6.string().describe(
708
+ "Control label; also sanitized into a valid TD custom-parameter name (e.g. 'blur amount' \u2192 'Bluramount')."
709
+ ),
710
+ type: z6.enum(["float", "int", "toggle", "menu", "rgb", "pulse", "string"]).default("float").describe(
711
+ "Widget kind: float/int sliders, a toggle, a dropdown menu, an RGB swatch, a momentary pulse, or a text field."
712
+ ),
713
+ label: z6.string().optional().describe("Display label (defaults to `name`)."),
714
+ min: z6.coerce.number().optional().describe("Slider lower bound (float/int) \u2014 also hard-clamped."),
715
+ max: z6.coerce.number().optional().describe("Slider upper bound (float/int) \u2014 also hard-clamped."),
716
+ default: z6.union([z6.number(), z6.boolean(), z6.string()]).optional().describe("Initial value."),
717
+ menu_items: z6.array(z6.string()).optional().describe("Options for a 'menu' control."),
718
+ bind_to: z6.array(z6.string()).optional().describe(
719
+ "Parameters this control should drive, each written as 'nodePath.parName' (e.g. '/project1/sys/blur1.size'). Each target is switched to expression mode so moving the control moves the parameter live. Not supported for 'rgb'/'pulse'."
720
+ )
721
+ });
722
+ var createControlPanelSchema = z6.object({
723
+ comp_path: z6.string().default("/project1").describe(
724
+ "COMP that will receive the custom parameters \u2014 usually a generated system's container."
725
+ ),
726
+ page: z6.string().default("Controls").describe("Name of the custom-parameter page to add the controls to."),
727
+ controls: z6.array(controlSchema).min(1).describe("The controls (knobs/sliders/toggles/menus) to expose.")
728
+ });
729
+ var PANEL_SCRIPT = `
730
+ import json, base64, traceback
731
+ _payload = json.loads(base64.b64decode("__PAYLOAD_B64__").decode("utf-8"))
732
+ _comp = op(_payload["comp"]); _page_name = _payload["page"]; _controls = _payload["controls"]
733
+ report = {"comp": _payload["comp"], "page": _page_name, "created": [], "bound": [], "warnings": []}
734
+
735
+ def _parname(s):
736
+ s = "".join(ch for ch in s if ch.isalnum())
737
+ if not s:
738
+ s = "Par"
739
+ if not s[0].isalpha():
740
+ s = "P" + s
741
+ # TD custom-parameter names must be a leading uppercase letter followed by lowercase
742
+ # letters/digits only, so lowercase the tail \u2014 otherwise camelCase input like "CamZoom"
743
+ # keeps its internal capital and TouchDesigner rejects the name.
744
+ return s[0].upper() + s[1:].lower()
745
+
746
+ if _comp is None:
747
+ report["fatal"] = "COMP not found: " + _payload["comp"]
748
+ elif not hasattr(_comp, "appendCustomPage"):
749
+ report["fatal"] = _payload["comp"] + " is not a COMP, so it cannot hold custom parameters."
750
+ else:
751
+ _page = None
752
+ for _pg in _comp.customPages:
753
+ if _pg.name == _page_name:
754
+ _page = _pg
755
+ break
756
+ if _page is None:
757
+ _page = _comp.appendCustomPage(_page_name)
758
+ for _spec in _controls:
759
+ try:
760
+ _name = _parname(_spec["name"]); _typ = _spec.get("type", "float"); _label = _spec.get("label") or _spec["name"]
761
+ if _typ == "float":
762
+ _pg = _page.appendFloat(_name, label=_label)
763
+ elif _typ == "int":
764
+ _pg = _page.appendInt(_name, label=_label)
765
+ elif _typ == "toggle":
766
+ _pg = _page.appendToggle(_name, label=_label)
767
+ elif _typ == "menu":
768
+ _pg = _page.appendMenu(_name, label=_label)
769
+ elif _typ == "rgb":
770
+ _pg = _page.appendRGB(_name, label=_label)
771
+ elif _typ == "pulse":
772
+ _pg = _page.appendPulse(_name, label=_label)
773
+ elif _typ in ("string", "str"):
774
+ _pg = _page.appendStr(_name, label=_label)
775
+ else:
776
+ report["warnings"].append("Unknown control type '%s' for '%s'." % (_typ, _spec["name"]))
777
+ continue
778
+ _p0 = _pg[0]; _dflt = _spec.get("default", None)
779
+ if _typ in ("float", "int"):
780
+ _mn = _spec.get("min", None); _mx = _spec.get("max", None)
781
+ if _mn is not None:
782
+ _p0.normMin = _mn; _p0.min = _mn; _p0.clampMin = True
783
+ if _mx is not None:
784
+ _p0.normMax = _mx; _p0.max = _mx; _p0.clampMax = True
785
+ if _dflt is not None:
786
+ _p0.default = _dflt; _p0.val = _dflt
787
+ elif _typ == "toggle":
788
+ if _dflt is not None:
789
+ _p0.default = bool(_dflt); _p0.val = bool(_dflt)
790
+ elif _typ == "menu":
791
+ _items = _spec.get("menu_items") or []
792
+ if _items:
793
+ _names = [str(x) for x in _items]
794
+ _p0.menuNames = _names; _p0.menuLabels = _names
795
+ if _dflt is not None and str(_dflt) in [str(x) for x in _items]:
796
+ _p0.default = str(_dflt); _p0.val = str(_dflt)
797
+ elif _typ in ("string", "str"):
798
+ if _dflt is not None:
799
+ _p0.default = str(_dflt); _p0.val = str(_dflt)
800
+ report["created"].append({"control": _spec["name"], "name": _name, "type": _typ, "pars": [pp.name for pp in _pg], "value": _p0.eval()})
801
+ _binds = _spec.get("bind_to") or []
802
+ if _binds and _typ in ("rgb", "pulse"):
803
+ report["warnings"].append("bind_to ignored for '%s' (a %s control cannot drive a single parameter)." % (_spec["name"], _typ))
804
+ _binds = []
805
+ for _t in _binds:
806
+ try:
807
+ _dot = _t.rfind(".")
808
+ if _dot <= 0:
809
+ report["warnings"].append("Invalid bind target '%s' (expected 'nodePath.parName')." % _t)
810
+ continue
811
+ _np = _t[:_dot]; _pn = _t[_dot + 1:]; _tn = op(_np)
812
+ if _tn is None:
813
+ report["warnings"].append("Bind target node not found: %s" % _np)
814
+ continue
815
+ _tp = getattr(_tn.par, _pn, None)
816
+ if _tp is None:
817
+ report["warnings"].append("Bind target parameter not found: %s.%s" % (_np, _pn))
818
+ continue
819
+ _PM = type(_tp.mode)
820
+ _tp.expr = "op(%s).par.%s" % (repr(_payload["comp"]), _name)
821
+ _tp.mode = _PM.EXPRESSION
822
+ report["bound"].append({"control": _name, "target": _np + "." + _pn})
823
+ except Exception:
824
+ report["warnings"].append("Failed to bind '%s' to '%s': %s" % (_name, _t, traceback.format_exc().splitlines()[-1]))
825
+ except Exception:
826
+ report["warnings"].append("Failed to create control '%s': %s" % (_spec.get("name", "?"), traceback.format_exc().splitlines()[-1]))
827
+ print(json.dumps(report))
828
+ `;
829
+ function buildPanelScript(payload) {
830
+ return buildPayloadScript(PANEL_SCRIPT, payload);
831
+ }
832
+ function parseReport(stdout) {
833
+ return parsePythonReport(stdout);
834
+ }
835
+ async function createControlPanelImpl(ctx, args) {
836
+ return guardTd(
837
+ async () => {
838
+ const script = buildPanelScript({
839
+ comp: args.comp_path,
840
+ page: args.page,
841
+ controls: args.controls
842
+ });
843
+ const exec = await ctx.client.executePythonScript(script, true);
844
+ return parseReport(exec.stdout);
845
+ },
846
+ (report) => {
847
+ if (report.fatal) {
848
+ return jsonResult(`Could not build control panel: ${report.fatal}`, report);
849
+ }
850
+ const summary = `Added ${report.created.length} control(s) on page "${report.page}" of ${report.comp}, ${report.bound.length} bound to live parameter(s)${report.warnings.length ? `, ${report.warnings.length} warning(s)` : ""}.`;
851
+ return jsonResult(summary, report);
852
+ }
853
+ );
854
+ }
855
+ var registerCreateControlPanel = (server, ctx) => {
856
+ server.registerTool(
857
+ "create_control_panel",
858
+ {
859
+ title: "Create control panel",
860
+ description: "Expose live controls on a COMP: append custom parameters (sliders, toggles, menus, RGB, pulse) and bind them to node parameters so the artist can drive a generated system in real time. Point `comp_path` at a system container and list the controls; use each control's `bind_to` to wire it to one or more 'nodePath.parName' targets.",
861
+ inputSchema: createControlPanelSchema.shape,
862
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
863
+ },
864
+ (args) => createControlPanelImpl(ctx, args)
865
+ );
866
+ };
867
+
868
+ // src/tools/layout.ts
869
+ var X_STEP = 200;
870
+ var Y_STEP = 140;
871
+ var SIBLING_GAP = 80;
872
+ function pushTo(map, key, value) {
873
+ const existing = map.get(key);
874
+ if (existing) existing.push(value);
875
+ else map.set(key, [value]);
876
+ }
877
+ function parentOf(path) {
878
+ const i = path.lastIndexOf("/");
879
+ return i <= 0 ? "/" : path.slice(0, i);
880
+ }
881
+ function layerByLongestPath(nodes, preds) {
882
+ const memo = /* @__PURE__ */ new Map();
883
+ const visiting = /* @__PURE__ */ new Set();
884
+ const depth = (node) => {
885
+ const cached2 = memo.get(node);
886
+ if (cached2 !== void 0) return cached2;
887
+ if (visiting.has(node)) return 0;
888
+ visiting.add(node);
889
+ let best = 0;
890
+ for (const pred of preds.get(node) ?? []) best = Math.max(best, depth(pred) + 1);
891
+ visiting.delete(node);
892
+ memo.set(node, best);
893
+ return best;
894
+ };
895
+ const layers = /* @__PURE__ */ new Map();
896
+ for (const node of nodes) layers.set(node, depth(node));
897
+ return layers;
898
+ }
899
+ function computeDataflowLayout(nodes, edges) {
900
+ const set = new Set(nodes);
901
+ const preds = /* @__PURE__ */ new Map();
902
+ for (const { from, to } of edges) {
903
+ if (from === to || !set.has(from) || !set.has(to)) continue;
904
+ pushTo(preds, to, from);
905
+ }
906
+ const layerOf = layerByLongestPath(nodes, preds);
907
+ const byLayer = /* @__PURE__ */ new Map();
908
+ for (const node of nodes) pushTo(byLayer, layerOf.get(node) ?? 0, node);
909
+ const positions = {};
910
+ for (const [layer, members] of byLayer) {
911
+ const center = (members.length - 1) / 2;
912
+ members.forEach((node, i) => {
913
+ positions[node] = [layer * X_STEP, (center - i) * Y_STEP];
914
+ });
915
+ }
916
+ return positions;
917
+ }
918
+ function computeLayoutByParent(nodes, edges) {
919
+ const groups = /* @__PURE__ */ new Map();
920
+ for (const node of nodes) pushTo(groups, parentOf(node), node);
921
+ const merged = {};
922
+ for (const members of groups.values()) {
923
+ Object.assign(merged, computeDataflowLayout(members, edges));
924
+ }
925
+ return merged;
926
+ }
927
+ function layoutScript(positions) {
928
+ return [
929
+ `_pos = ${JSON.stringify(positions)}`,
930
+ "for _p, _xy in _pos.items():",
931
+ " _n = op(_p)",
932
+ " if _n is not None:",
933
+ " _n.nodeX = _xy[0]",
934
+ " _n.nodeY = _xy[1]"
935
+ ].join("\n");
936
+ }
937
+ function placeBelowSiblingsScript(parentPath, nodePath) {
938
+ const q11 = JSON.stringify;
939
+ return [
940
+ `_parent = op(${q11(parentPath)})`,
941
+ `_new = op(${q11(nodePath)})`,
942
+ "if _parent is not None and _new is not None:",
943
+ " _sibs = [c for c in _parent.children if c is not _new]",
944
+ " if _sibs:",
945
+ " _new.nodeX = min(c.nodeX for c in _sibs)",
946
+ ` _new.nodeY = min(c.nodeY for c in _sibs) - _new.nodeHeight - ${SIBLING_GAP}`,
947
+ " else:",
948
+ " _new.nodeX = 0",
949
+ " _new.nodeY = 0"
950
+ ].join("\n");
951
+ }
952
+
667
953
  // src/tools/layer1/orchestration.ts
668
954
  var q = (value) => JSON.stringify(value);
669
955
  var UNIFORM_KINDS = {
@@ -707,6 +993,9 @@ var NetworkBuilder = class {
707
993
  warnings = [];
708
994
  nameToPath = /* @__PURE__ */ new Map();
709
995
  pathToType = /* @__PURE__ */ new Map();
996
+ // Intended data-flow wires, recorded even when the physical connect fails or is
997
+ // satisfied via a source parameter, so auto-layout still reflects the flow.
998
+ edges = [];
710
999
  async add(type, name, parameters, parentPath) {
711
1000
  const ref = await this.ctx.client.createNode({
712
1001
  parent_path: parentPath ?? this.containerPath,
@@ -729,6 +1018,7 @@ for _c in list(_g.children):
729
1018
  return this.nameToPath.get(name);
730
1019
  }
731
1020
  async connect(fromPath, toPath, fromOutput = 0, toInput = 0) {
1021
+ this.edges.push({ from: fromPath, to: toPath });
732
1022
  const targetType = this.pathToType.get(toPath);
733
1023
  const param = targetType ? converterSourceParam(targetType) : void 0;
734
1024
  if (param) {
@@ -755,6 +1045,19 @@ for _c in list(_g.children):
755
1045
  this.warnings.push(`Python step failed: ${friendlyTdError(err)}`);
756
1046
  }
757
1047
  }
1048
+ /** Arranges every created node left→right along the recorded data flow. */
1049
+ async layout() {
1050
+ const positions = computeLayoutByParent(
1051
+ this.created.map((c) => c.path),
1052
+ this.edges
1053
+ );
1054
+ if (Object.keys(positions).length === 0) return;
1055
+ try {
1056
+ await this.ctx.client.executePythonScript(layoutScript(positions), false);
1057
+ } catch (err) {
1058
+ this.warnings.push(`Auto-layout skipped: ${friendlyTdError(err)}`);
1059
+ }
1060
+ }
758
1061
  };
759
1062
  async function createSystemContainer(ctx, parentPath, name) {
760
1063
  const container = await ctx.client.createNode({
@@ -762,6 +1065,14 @@ async function createSystemContainer(ctx, parentPath, name) {
762
1065
  type: "baseCOMP",
763
1066
  name
764
1067
  });
1068
+ try {
1069
+ await ctx.client.executePythonScript(
1070
+ placeBelowSiblingsScript(parentPath, container.path),
1071
+ false
1072
+ );
1073
+ } catch (err) {
1074
+ ctx.logger.debug("container placement skipped", { err: String(err) });
1075
+ }
765
1076
  return new NetworkBuilder(ctx, container.path);
766
1077
  }
767
1078
  async function buildFromRecipe(ctx, recipe, parentPath) {
@@ -864,11 +1175,37 @@ _n.display = True`
864
1175
  }
865
1176
  const outNode = recipe.nodes.find((n) => /^out/i.test(n.name)) ?? recipe.nodes[recipe.nodes.length - 1];
866
1177
  const outputPath = outNode ? builder.pathOf(outNode.name) : void 0;
867
- return { builder, outputPath };
1178
+ const controls = recipe.controls.map((control) => ({
1179
+ ...control,
1180
+ bind_to: control.bind_to?.map((target) => {
1181
+ const dot = target.lastIndexOf(".");
1182
+ if (dot <= 0) return target;
1183
+ const path = builder.pathOf(target.slice(0, dot));
1184
+ return path ? `${path}.${target.slice(dot + 1)}` : target;
1185
+ })
1186
+ }));
1187
+ return { builder, outputPath, controls };
1188
+ }
1189
+ async function exposeControls(ctx, compPath, controls) {
1190
+ try {
1191
+ const script = buildPanelScript({ comp: compPath, page: "Controls", controls });
1192
+ const exec = await ctx.client.executePythonScript(script, true);
1193
+ return parsePythonReport(exec.stdout);
1194
+ } catch (err) {
1195
+ return { created: [], bound: [], warnings: [`Control panel skipped: ${friendlyTdError(err)}`] };
1196
+ }
868
1197
  }
869
1198
  async function finalize(ctx, options) {
870
1199
  const { builder } = options;
1200
+ await builder.layout();
871
1201
  const warnings = [...builder.warnings];
1202
+ let controlsSummary;
1203
+ if (options.controls?.length) {
1204
+ const result = await exposeControls(ctx, builder.containerPath, options.controls);
1205
+ warnings.push(...result.warnings);
1206
+ if (result.fatal) warnings.push(`Control panel skipped: ${result.fatal}`);
1207
+ else controlsSummary = { added: result.created.map((c) => c.name), bound: result.bound.length };
1208
+ }
872
1209
  let errors = [];
873
1210
  try {
874
1211
  const report = await checkErrors(ctx.client, builder.containerPath);
@@ -894,6 +1231,7 @@ async function finalize(ctx, options) {
894
1231
  created: builder.created.map((c) => c.path),
895
1232
  output: options.outputPath,
896
1233
  recipe: options.recipeId,
1234
+ controls: controlsSummary,
897
1235
  errors,
898
1236
  warnings,
899
1237
  ...options.extra
@@ -917,7 +1255,13 @@ ${JSON.stringify(data, null, 2)}
917
1255
  // src/tools/layer1/applyPostProcessing.ts
918
1256
  var q2 = (value) => JSON.stringify(value);
919
1257
  var DIRECT_EFFECTS = {
920
- bloom: { type: "bloomTOP" },
1258
+ // bloomTOP defaults bloom everything above 0.01 and adds it back, which blows bright
1259
+ // sources out to solid white. Raise the threshold so only highlights bloom, and soften the
1260
+ // intensity for a tasteful glow that preserves the underlying image.
1261
+ bloom: {
1262
+ type: "bloomTOP",
1263
+ parameters: { bloomthreshold: 0.8, bloomintensity: 0.6, bloomfill: 0.5 }
1264
+ },
921
1265
  blur: { type: "blurTOP", parameters: { size: 4 } },
922
1266
  edge_detect: { type: "edgeTOP" },
923
1267
  sharpen: { type: "sharpenTOP" },
@@ -978,10 +1322,10 @@ var EFFECTS = [
978
1322
  "rgb_split",
979
1323
  "scanlines"
980
1324
  ];
981
- var applyPostProcessingSchema = z6.object({
982
- source_path: z6.string().describe("Path of the TOP to post-process."),
983
- effects: z6.array(z6.enum(EFFECTS)).min(1).describe("Effects to apply in order."),
984
- parent_path: z6.string().default("/project1")
1325
+ var applyPostProcessingSchema = z7.object({
1326
+ source_path: z7.string().describe("Path of the TOP to post-process."),
1327
+ effects: z7.array(z7.enum(EFFECTS)).min(1).describe("Effects to apply in order."),
1328
+ parent_path: z7.string().default("/project1")
985
1329
  });
986
1330
  async function addGlslEffect(builder, name, fragment) {
987
1331
  const glsl = await builder.add("glslTOP", name);
@@ -1041,27 +1385,81 @@ var registerApplyPostProcessing = (server, ctx) => {
1041
1385
  };
1042
1386
 
1043
1387
  // src/tools/layer1/createAudioReactive.ts
1044
- import { z as z7 } from "zod";
1388
+ import { z as z8 } from "zod";
1045
1389
  var q3 = (value) => JSON.stringify(value);
1046
1390
  var AUDIO_SPECTRUM_SHADER = `out vec4 fragColor;
1047
1391
  void main(){
1048
1392
  vec2 uv = vUV.st;
1049
- float amp = texture(sTD2DInputs[0], vec2(uv.x, 0.5)).r;
1393
+ // Audio Spectrum CHOP magnitudes are tiny (~0.01\u20130.1); scale into the [0,1] bar
1394
+ // range so realistic input renders visible bars instead of a near-black frame.
1395
+ float amp = texture(sTD2DInputs[0], vec2(uv.x, 0.5)).r * 20.0;
1050
1396
  float bar = step(uv.y, clamp(amp, 0.0, 1.0));
1051
1397
  vec3 col = mix(vec3(0.02, 0.0, 0.08), vec3(0.1, 0.8, 1.0), uv.x) * bar;
1052
1398
  fragColor = TDOutputSwizzle(vec4(col, 1.0));
1053
1399
  }
1054
1400
  `;
1055
- var createAudioReactiveSchema = z7.object({
1056
- audio_source: z7.enum(["microphone", "file", "device_in", "existing_chop"]).default("microphone"),
1057
- audio_file_path: z7.string().optional().describe("Audio file path (audio_source='file')."),
1058
- existing_chop_path: z7.string().optional().describe("Existing CHOP path (audio_source='existing_chop')."),
1059
- visual_style: z7.enum(["geometric", "particle", "feedback", "glsl", "instancing"]),
1060
- frequency_bands: z7.number().int().positive().default(8).describe(
1401
+ var STYLE_SHADERS = {
1402
+ // Radial spectrum bars emanating from the centre by angle.
1403
+ geometric: `out vec4 fragColor;
1404
+ void main(){
1405
+ vec2 pos = vUV.st - 0.5;
1406
+ float ang = atan(pos.y, pos.x) * 0.159155 + 0.5;
1407
+ float rad = length(pos) * 2.0;
1408
+ float amp = texture(sTD2DInputs[0], vec2(ang, 0.5)).r * 20.0;
1409
+ float bar = step(rad, clamp(amp, 0.0, 1.0));
1410
+ vec3 col = mix(vec3(0.05, 0.0, 0.15), vec3(0.2, 0.9, 1.0), ang) * bar;
1411
+ fragColor = TDOutputSwizzle(vec4(col, 1.0));
1412
+ }
1413
+ `,
1414
+ // A grid of dots whose size tracks each column's spectrum bin.
1415
+ particle: `out vec4 fragColor;
1416
+ void main(){
1417
+ vec2 uv = vUV.st;
1418
+ vec2 cell = fract(uv * 16.0) - 0.5;
1419
+ float colx = floor(uv.x * 16.0) / 16.0;
1420
+ float amp = texture(sTD2DInputs[0], vec2(colx, 0.5)).r * 20.0;
1421
+ float dot = smoothstep(0.45 * clamp(amp, 0.05, 1.0), 0.0, length(cell));
1422
+ vec3 col = dot * mix(vec3(0.1, 0.4, 1.0), vec3(1.0, 0.3, 0.6), uv.y);
1423
+ fragColor = TDOutputSwizzle(vec4(col, 1.0));
1424
+ }
1425
+ `,
1426
+ // Concentric rings warped by the spectrum — a tunnel/echo look.
1427
+ feedback: `out vec4 fragColor;
1428
+ void main(){
1429
+ vec2 pos = vUV.st - 0.5;
1430
+ float rad = length(pos);
1431
+ float ang = atan(pos.y, pos.x);
1432
+ float amp = texture(sTD2DInputs[0], vec2(rad, 0.5)).r * 20.0;
1433
+ float rings = 0.5 + 0.5 * sin(rad * 40.0 - amp * 12.0 + ang * 3.0);
1434
+ vec3 col = rings * mix(vec3(0.6, 0.1, 0.8), vec3(0.1, 0.8, 0.9), rad * 2.0);
1435
+ fragColor = TDOutputSwizzle(vec4(col, 1.0));
1436
+ }
1437
+ `,
1438
+ // An LED matrix: columns light up to a height set by their spectrum bin.
1439
+ instancing: `out vec4 fragColor;
1440
+ void main(){
1441
+ vec2 uv = vUV.st;
1442
+ vec2 cell = floor(uv * vec2(16.0, 8.0));
1443
+ vec2 fr = fract(uv * vec2(16.0, 8.0));
1444
+ float amp = texture(sTD2DInputs[0], vec2(cell.x / 16.0, 0.5)).r * 20.0;
1445
+ float lit = step((cell.y + 0.5) / 8.0, clamp(amp, 0.0, 1.0));
1446
+ float pad = step(0.1, fr.x) * step(fr.x, 0.9) * step(0.1, fr.y) * step(fr.y, 0.9);
1447
+ vec3 col = lit * pad * mix(vec3(0.0, 1.0, 0.4), vec3(1.0, 0.8, 0.0), cell.y / 8.0);
1448
+ fragColor = TDOutputSwizzle(vec4(col, 1.0));
1449
+ }
1450
+ `
1451
+ };
1452
+ var createAudioReactiveSchema = z8.object({
1453
+ audio_source: z8.enum(["microphone", "file", "device_in", "existing_chop"]).default("microphone"),
1454
+ audio_file_path: z8.string().optional().describe("Audio file path (audio_source='file')."),
1455
+ existing_chop_path: z8.string().optional().describe("Existing CHOP path (audio_source='existing_chop')."),
1456
+ visual_style: z8.enum(["geometric", "particle", "feedback", "glsl", "instancing"]),
1457
+ frequency_bands: z8.coerce.number().int().positive().default(8).describe(
1061
1458
  "Spectrum resolution: sets the Audio Spectrum CHOP output length (TouchDesigner clamps it to 128\u20134096 bins). Higher = finer spectrum."
1062
1459
  ),
1063
- beat_detection: z7.boolean().default(true),
1064
- parent_path: z7.string().default("/project1")
1460
+ beat_detection: z8.boolean().default(true),
1461
+ expose_controls: z8.boolean().default(true).describe("Expose a live 'Sensitivity' knob (how strongly the audio drives the visual)."),
1462
+ parent_path: z8.string().default("/project1")
1065
1463
  });
1066
1464
  async function buildAudioSource(builder, args) {
1067
1465
  if (args.audio_source === "existing_chop" && args.existing_chop_path) {
@@ -1093,40 +1491,38 @@ async function createAudioReactiveImpl(ctx, args) {
1093
1491
  }
1094
1492
  const audioTex = await builder.add("choptoTOP", "audio_tex");
1095
1493
  await builder.connect(spectrum, audioTex);
1096
- let visual;
1097
- if (args.visual_style === "glsl") {
1098
- visual = await builder.add("glslTOP", "visual", {
1099
- outputresolution: "custom",
1100
- resolutionw: 1280,
1101
- resolutionh: 720,
1102
- format: "rgba8fixed"
1103
- });
1104
- const frag = await builder.add("textDAT", "visual_frag");
1105
- await builder.python(
1106
- `op(${q3(frag)}).text = ${q3(AUDIO_SPECTRUM_SHADER)}
1494
+ const sensitivity = await builder.add("levelTOP", "sensitivity", { brightness1: 1 });
1495
+ await builder.connect(audioTex, sensitivity);
1496
+ const shader = args.visual_style === "glsl" ? AUDIO_SPECTRUM_SHADER : STYLE_SHADERS[args.visual_style] ?? AUDIO_SPECTRUM_SHADER;
1497
+ const visual = await builder.add("glslTOP", "visual", {
1498
+ outputresolution: "custom",
1499
+ resolutionw: 1280,
1500
+ resolutionh: 720,
1501
+ format: "rgba8fixed"
1502
+ });
1503
+ const frag = await builder.add("textDAT", "visual_frag");
1504
+ await builder.python(
1505
+ `op(${q3(frag)}).text = ${q3(shader)}
1107
1506
  op(${q3(visual)}).par.pixeldat = op(${q3(frag)}).name`
1108
- );
1109
- await builder.connect(audioTex, visual);
1110
- } else {
1111
- visual = await builder.add("circleTOP", "visual", { radius: 0.3 });
1112
- await builder.python(
1113
- `c = op(${q3(visual)})
1114
- for p in ('radiusx', 'radiusy'):
1115
- try:
1116
- par = getattr(c.par, p)
1117
- par.expr = "0.2 + op('level')['chan1'] * 0.6"
1118
- except Exception: pass`
1119
- );
1120
- builder.warnings.push(
1121
- `Visual style "${args.visual_style}" is approximated: a circle driven by the audio level. Refine the mapping for production.`
1122
- );
1123
- }
1507
+ );
1508
+ await builder.connect(sensitivity, visual);
1124
1509
  const out = await builder.add("nullTOP", "out1");
1125
1510
  await builder.connect(visual, out);
1511
+ const controls = args.expose_controls ? [
1512
+ {
1513
+ name: "Sensitivity",
1514
+ type: "float",
1515
+ min: 0,
1516
+ max: 4,
1517
+ default: 1,
1518
+ bind_to: [`${sensitivity}.brightness1`]
1519
+ }
1520
+ ] : [];
1126
1521
  return finalize(ctx, {
1127
1522
  summary: `Created an audio-reactive system (source: ${args.audio_source}, style: ${args.visual_style}, ${args.frequency_bands} bands).`,
1128
1523
  builder,
1129
1524
  outputPath: out,
1525
+ controls,
1130
1526
  extra: {
1131
1527
  audio_source: args.audio_source,
1132
1528
  visual_style: args.visual_style,
@@ -1140,7 +1536,7 @@ var registerCreateAudioReactive = (server, ctx) => {
1140
1536
  "create_audio_reactive",
1141
1537
  {
1142
1538
  title: "Create audio-reactive visual",
1143
- description: "Build an audio analysis chain (spectrum + level + optional beat) and a visual driven by it. The 'glsl' style renders a spectrum visualization; other styles are approximated.",
1539
+ description: "Build an audio analysis chain (spectrum + level + optional beat) and a spectrum visual driven by it. Each visual_style renders the spectrum its own way: glsl=horizontal bars, geometric=radial bars, particle=dot field, feedback=ring tunnel, instancing=LED grid.",
1144
1540
  inputSchema: createAudioReactiveSchema.shape,
1145
1541
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
1146
1542
  },
@@ -1149,7 +1545,7 @@ var registerCreateAudioReactive = (server, ctx) => {
1149
1545
  };
1150
1546
 
1151
1547
  // src/tools/layer1/createDataVisualization.ts
1152
- import { z as z8 } from "zod";
1548
+ import { z as z9 } from "zod";
1153
1549
  var q4 = (value) => JSON.stringify(value);
1154
1550
  var BARS_SHADER = `out vec4 fragColor;
1155
1551
  void main(){
@@ -1165,10 +1561,11 @@ var SOURCE_TYPE = {
1165
1561
  file: "fileinDAT",
1166
1562
  chop: "constantCHOP"
1167
1563
  };
1168
- var createDataVisualizationSchema = z8.object({
1169
- data_source: z8.enum(["table", "file", "chop"]).default("table"),
1170
- chart_style: z8.enum(["bars", "graph", "points"]).default("bars"),
1171
- parent_path: z8.string().default("/project1")
1564
+ var createDataVisualizationSchema = z9.object({
1565
+ data_source: z9.enum(["table", "file", "chop"]).default("table"),
1566
+ chart_style: z9.enum(["bars", "graph", "points"]).default("bars"),
1567
+ expose_controls: z9.boolean().default(true).describe("Expose a live 'Scale' knob that amplifies the data values feeding the chart."),
1568
+ parent_path: z9.string().default("/project1")
1172
1569
  });
1173
1570
  async function createDataVisualizationImpl(ctx, args) {
1174
1571
  return runBuild(async () => {
@@ -1197,7 +1594,9 @@ for v in ${JSON.stringify(rows)}:
1197
1594
  }
1198
1595
  const tex = await builder.add("choptoTOP", "data_tex");
1199
1596
  await builder.connect(chop, tex);
1200
- let visual = tex;
1597
+ const scale = await builder.add("levelTOP", "scale", { brightness1: 1 });
1598
+ await builder.connect(tex, scale);
1599
+ let visual = scale;
1201
1600
  if (args.chart_style === "bars") {
1202
1601
  const glsl = await builder.add("glslTOP", "chart", {
1203
1602
  outputresolution: "custom",
@@ -1210,7 +1609,7 @@ for v in ${JSON.stringify(rows)}:
1210
1609
  `op(${q4(frag)}).text = ${q4(BARS_SHADER)}
1211
1610
  op(${q4(glsl)}).par.pixeldat = op(${q4(frag)}).name`
1212
1611
  );
1213
- await builder.connect(tex, glsl);
1612
+ await builder.connect(scale, glsl);
1214
1613
  visual = glsl;
1215
1614
  } else {
1216
1615
  builder.warnings.push(
@@ -1222,10 +1621,21 @@ op(${q4(glsl)}).par.pixeldat = op(${q4(frag)}).name`
1222
1621
  builder.warnings.push(
1223
1622
  "Wire your real data into the 'data' node \u2014 a placeholder source was created."
1224
1623
  );
1624
+ const controls = args.expose_controls ? [
1625
+ {
1626
+ name: "Scale",
1627
+ type: "float",
1628
+ min: 0,
1629
+ max: 4,
1630
+ default: 1,
1631
+ bind_to: [`${scale}.brightness1`]
1632
+ }
1633
+ ] : [];
1225
1634
  return finalize(ctx, {
1226
1635
  summary: `Created a data visualization (source: ${args.data_source}, style: ${args.chart_style}).`,
1227
1636
  builder,
1228
1637
  outputPath: out,
1638
+ controls,
1229
1639
  extra: { data_source: args.data_source, chart_style: args.chart_style }
1230
1640
  });
1231
1641
  });
@@ -1244,7 +1654,7 @@ var registerCreateDataVisualization = (server, ctx) => {
1244
1654
  };
1245
1655
 
1246
1656
  // src/tools/layer1/createFeedbackNetwork.ts
1247
- import { z as z9 } from "zod";
1657
+ import { z as z10 } from "zod";
1248
1658
  var q5 = (value) => JSON.stringify(value);
1249
1659
  function colorizeShader(colors) {
1250
1660
  const toVec3 = (hex) => {
@@ -1265,6 +1675,14 @@ void main(){
1265
1675
  }
1266
1676
  `;
1267
1677
  }
1678
+ var SEED_GLSL = `out vec4 fragColor;
1679
+ void main(){
1680
+ vec2 uv = vUV.st;
1681
+ float v = sin(uv.x * 12.0) + sin(uv.y * 12.0) + sin((uv.x + uv.y) * 8.0);
1682
+ vec3 col = 0.5 + 0.5 * cos(vec3(0.0, 2.0, 4.0) + v);
1683
+ fragColor = TDOutputSwizzle(vec4(col, 1.0));
1684
+ }
1685
+ `;
1268
1686
  var SEED_TYPES = {
1269
1687
  noise: "noiseTOP",
1270
1688
  shape: "circleTOP",
@@ -1284,10 +1702,10 @@ var TRANSFORM_TYPES = {
1284
1702
  tile: "tileTOP",
1285
1703
  luma_blur: "lumablurTOP"
1286
1704
  };
1287
- var createFeedbackNetworkSchema = z9.object({
1288
- seed_type: z9.enum(["noise", "shape", "image", "video", "webcam", "glsl"]).default("noise"),
1289
- transformations: z9.array(
1290
- z9.enum([
1705
+ var createFeedbackNetworkSchema = z10.object({
1706
+ seed_type: z10.enum(["noise", "shape", "image", "video", "webcam", "glsl"]).default("noise"),
1707
+ transformations: z10.array(
1708
+ z10.enum([
1291
1709
  "blur",
1292
1710
  "displace",
1293
1711
  "edge",
@@ -1299,9 +1717,10 @@ var createFeedbackNetworkSchema = z9.object({
1299
1717
  "luma_blur"
1300
1718
  ])
1301
1719
  ).default(["blur", "displace", "level"]),
1302
- feedback_gain: z9.number().min(0).max(1).default(0.95),
1303
- colors: z9.array(z9.string()).max(2).optional().describe("Up to two hex colors to colorize the otherwise-grayscale output."),
1304
- parent_path: z9.string().default("/project1")
1720
+ feedback_gain: z10.coerce.number().min(0).max(1).default(0.95),
1721
+ colors: z10.array(z10.string()).max(2).optional().describe("Up to two hex colors to colorize the otherwise-grayscale output."),
1722
+ expose_controls: z10.boolean().default(true).describe("Expose a live 'Feedback' knob on the system container, bound to the loop's decay."),
1723
+ parent_path: z10.string().default("/project1")
1305
1724
  });
1306
1725
  async function createFeedbackNetworkImpl(ctx, args) {
1307
1726
  return runBuild(async () => {
@@ -1309,6 +1728,13 @@ async function createFeedbackNetworkImpl(ctx, args) {
1309
1728
  const seed = await builder.add(SEED_TYPES[args.seed_type], "seed", {
1310
1729
  ...args.seed_type === "noise" ? { monochrome: 1, period: 4 } : {}
1311
1730
  });
1731
+ if (args.seed_type === "glsl") {
1732
+ const seedFrag = await builder.add("textDAT", "seed_frag");
1733
+ await builder.python(
1734
+ `op(${q5(seedFrag)}).text = ${q5(SEED_GLSL)}
1735
+ op(${q5(seed)}).par.pixeldat = op(${q5(seedFrag)}).name`
1736
+ );
1737
+ }
1312
1738
  const feedback = await builder.add("feedbackTOP", "feedback1");
1313
1739
  const comp = await builder.add("compositeTOP", "comp1");
1314
1740
  await builder.setParams(comp, { operand: "maximum" });
@@ -1325,7 +1751,7 @@ async function createFeedbackNetworkImpl(ctx, args) {
1325
1751
  last = node;
1326
1752
  }
1327
1753
  const gain = await builder.add("levelTOP", "gain");
1328
- await builder.setParams(gain, { gain: args.feedback_gain });
1754
+ await builder.setParams(gain, { brightness1: args.feedback_gain });
1329
1755
  await builder.connect(last, gain);
1330
1756
  let output = gain;
1331
1757
  if (args.colors && args.colors.length > 0) {
@@ -1341,10 +1767,21 @@ op(${q5(colorize)}).par.pixeldat = op(${q5(frag)}).name`
1341
1767
  const out = await builder.add("nullTOP", "out1");
1342
1768
  await builder.connect(output, out);
1343
1769
  await builder.python(`op(${q5(feedback)}).par.top = op(${q5(gain)}).name`);
1770
+ const controls = args.expose_controls ? [
1771
+ {
1772
+ name: "Feedback",
1773
+ type: "float",
1774
+ min: 0,
1775
+ max: 1,
1776
+ default: args.feedback_gain,
1777
+ bind_to: [`${gain}.brightness1`]
1778
+ }
1779
+ ] : [];
1344
1780
  return finalize(ctx, {
1345
1781
  summary: `Created a feedback network (seed: ${args.seed_type}, gain: ${args.feedback_gain}, ${args.transformations.length} transform(s)).`,
1346
1782
  builder,
1347
1783
  outputPath: out,
1784
+ controls,
1348
1785
  extra: { seed_type: args.seed_type, transformations: args.transformations }
1349
1786
  });
1350
1787
  });
@@ -1363,7 +1800,7 @@ var registerCreateFeedbackNetwork = (server, ctx) => {
1363
1800
  };
1364
1801
 
1365
1802
  // src/tools/layer1/createGenerativeArt.ts
1366
- import { z as z10 } from "zod";
1803
+ import { z as z11 } from "zod";
1367
1804
  var q6 = (value) => JSON.stringify(value);
1368
1805
  var DEFAULT_PLASMA = `out vec4 fragColor;
1369
1806
  uniform float uTime;
@@ -1375,12 +1812,66 @@ void main(){
1375
1812
  fragColor = TDOutputSwizzle(vec4(col, 1.0));
1376
1813
  }
1377
1814
  `;
1815
+ var VORONOI_SHADER = `out vec4 fragColor;
1816
+ uniform float uTime;
1817
+ vec2 hash2(vec2 p){ return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453); }
1818
+ vec2 cellNoise(vec2 p){
1819
+ vec2 ip=floor(p); vec2 fp=fract(p);
1820
+ float d1=8.0; float d2=8.0;
1821
+ for(int yy=-1; yy<=1; yy++){
1822
+ for(int xx=-1; xx<=1; xx++){
1823
+ vec2 nb=vec2(float(xx),float(yy));
1824
+ vec2 pt=hash2(ip+nb);
1825
+ pt=0.5+0.5*sin(uTime*0.5+6.2831*pt);
1826
+ vec2 diff=nb+pt-fp; float dd=length(diff);
1827
+ if(dd<d1){ d2=d1; d1=dd; } else if(dd<d2){ d2=dd; }
1828
+ }
1829
+ }
1830
+ return vec2(d1,d2);
1831
+ }
1832
+ void main(){
1833
+ vec2 uv=vUV.st*8.0;
1834
+ vec2 fc=cellNoise(uv);
1835
+ float edge=smoothstep(0.0,0.08,fc.y-fc.x);
1836
+ float cell=fc.x;
1837
+ vec3 col=mix(vec3(0.02),vec3(0.5+0.5*cell,0.7,0.9),edge);
1838
+ fragColor=TDOutputSwizzle(vec4(col,1.0));
1839
+ }
1840
+ `;
1841
+ var FBM_SHADER = `out vec4 fragColor;
1842
+ uniform float uTime;
1843
+ float hashF(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453); }
1844
+ float vnoise(vec2 p){
1845
+ vec2 ip=floor(p); vec2 fp=fract(p);
1846
+ vec2 uu=fp*fp*(3.0-2.0*fp);
1847
+ float na=hashF(ip+vec2(0.0,0.0));
1848
+ float nb=hashF(ip+vec2(1.0,0.0));
1849
+ float nc=hashF(ip+vec2(0.0,1.0));
1850
+ float nd=hashF(ip+vec2(1.0,1.0));
1851
+ return mix(mix(na,nb,uu.x),mix(nc,nd,uu.x),uu.y);
1852
+ }
1853
+ float fbm(vec2 p){
1854
+ float acc=0.0; float amp=0.5;
1855
+ for(int oc=0; oc<6; oc++){ acc+=amp*vnoise(p); p*=2.0; amp*=0.5; }
1856
+ return acc;
1857
+ }
1858
+ void main(){
1859
+ vec2 uv=vUV.st*3.0;
1860
+ float val=fbm(uv+vec2(uTime*0.1, uTime*0.05));
1861
+ vec3 col=mix(vec3(0.02,0.05,0.15), vec3(0.95,0.6,0.25), val);
1862
+ fragColor=TDOutputSwizzle(vec4(col,1.0));
1863
+ }
1864
+ `;
1865
+ var TECHNIQUE_SHADERS = {
1866
+ voronoi: VORONOI_SHADER,
1867
+ fractal: FBM_SHADER
1868
+ };
1378
1869
  var RECIPE_FOR = /* @__PURE__ */ new Map([
1379
1870
  ["reaction_diffusion", "reaction_diffusion"],
1380
1871
  ["noise_landscape", "noise_landscape"]
1381
1872
  ]);
1382
- var createGenerativeArtSchema = z10.object({
1383
- technique: z10.enum([
1873
+ var createGenerativeArtSchema = z11.object({
1874
+ technique: z11.enum([
1384
1875
  "noise_landscape",
1385
1876
  "reaction_diffusion",
1386
1877
  "strange_attractor",
@@ -1391,18 +1882,25 @@ var createGenerativeArtSchema = z10.object({
1391
1882
  "fractal",
1392
1883
  "custom_glsl"
1393
1884
  ]),
1394
- color_palette: z10.string().optional().describe("Free-text palette hint (best-effort)."),
1395
- evolution_speed: z10.number().positive().default(1),
1396
- custom_glsl_code: z10.string().optional().describe("Fragment shader (only for technique 'custom_glsl')."),
1397
- parent_path: z10.string().default("/project1")
1885
+ color_palette: z11.string().optional().describe("Free-text palette hint (best-effort)."),
1886
+ evolution_speed: z11.coerce.number().positive().default(1),
1887
+ custom_glsl_code: z11.string().optional().describe("Fragment shader (only for technique 'custom_glsl')."),
1888
+ expose_controls: z11.boolean().default(true).describe("Expose a live 'Speed' knob (evolution speed) on the system container."),
1889
+ parent_path: z11.string().default("/project1")
1398
1890
  });
1399
- async function buildGlslGenerative(ctx, parentPath, name, fragment) {
1891
+ async function buildGlslGenerative(ctx, parentPath, name, fragment, speed = 1) {
1400
1892
  const builder = await createSystemContainer(ctx, parentPath, name);
1401
1893
  const glsl = await builder.add("glslTOP", "glsl1");
1402
1894
  const frag = await builder.add("textDAT", "glsl1_frag");
1403
1895
  await builder.python(
1404
1896
  `op(${q6(frag)}).text = ${q6(fragment)}
1405
1897
  op(${q6(glsl)}).par.pixeldat = op(${q6(frag)}).name`
1898
+ );
1899
+ await builder.python(
1900
+ `_g = op(${q6(glsl)})
1901
+ _g.seq.vec.numBlocks = max(_g.seq.vec.numBlocks, 1)
1902
+ _g.par.vec0name = 'uTime'
1903
+ _g.par.vec0valuex.expr = ${q6(`absTime.seconds * (parent().par.Speed.eval() if hasattr(parent().par, 'Speed') else ${speed})`)}`
1406
1904
  );
1407
1905
  const out = await builder.add("nullTOP", "out1");
1408
1906
  await builder.connect(glsl, out);
@@ -1410,16 +1908,22 @@ op(${q6(glsl)}).par.pixeldat = op(${q6(frag)}).name`
1410
1908
  }
1411
1909
  async function createGenerativeArtImpl(ctx, args) {
1412
1910
  return runBuild(async () => {
1911
+ const speedControls = args.expose_controls ? [{ name: "Speed", type: "float", min: 0, max: 4, default: args.evolution_speed }] : [];
1413
1912
  const recipeId = RECIPE_FOR.get(args.technique);
1414
1913
  if (recipeId) {
1415
1914
  const recipe = ctx.recipes.get(recipeId);
1416
1915
  if (recipe) {
1417
- const { builder: builder2, outputPath } = await buildFromRecipe(ctx, recipe, args.parent_path);
1916
+ const { builder: builder2, outputPath, controls } = await buildFromRecipe(
1917
+ ctx,
1918
+ recipe,
1919
+ args.parent_path
1920
+ );
1418
1921
  return finalize(ctx, {
1419
1922
  summary: `Created "${recipe.name}" generative system.`,
1420
1923
  builder: builder2,
1421
1924
  outputPath,
1422
1925
  recipeId,
1926
+ controls,
1423
1927
  extra: { technique: args.technique, color_palette: args.color_palette }
1424
1928
  });
1425
1929
  }
@@ -1430,7 +1934,8 @@ async function createGenerativeArtImpl(ctx, args) {
1430
1934
  ctx,
1431
1935
  args.parent_path,
1432
1936
  "generative_custom_glsl",
1433
- fragment
1937
+ fragment,
1938
+ args.evolution_speed
1434
1939
  );
1435
1940
  if (!args.custom_glsl_code) {
1436
1941
  builder2.warnings.push("No custom_glsl_code provided; used a default plasma shader.");
@@ -1439,22 +1944,25 @@ async function createGenerativeArtImpl(ctx, args) {
1439
1944
  summary: "Created a custom GLSL generative system.",
1440
1945
  builder: builder2,
1441
1946
  outputPath,
1947
+ controls: speedControls,
1442
1948
  extra: { technique: args.technique }
1443
1949
  });
1444
1950
  }
1445
- const pattern = ctx.knowledge.getGlslPattern(args.technique);
1446
- if (pattern?.code?.snippet) {
1951
+ const inlineShader = TECHNIQUE_SHADERS[args.technique];
1952
+ if (inlineShader) {
1447
1953
  const { builder: builder2, outputPath } = await buildGlslGenerative(
1448
1954
  ctx,
1449
1955
  args.parent_path,
1450
1956
  `generative_${args.technique}`,
1451
- pattern.code.snippet
1957
+ inlineShader,
1958
+ args.evolution_speed
1452
1959
  );
1453
1960
  return finalize(ctx, {
1454
- summary: `Created a "${args.technique}" system from the GLSL knowledge pattern "${pattern.name}".`,
1961
+ summary: `Created a "${args.technique}" generative system (GLSL).`,
1455
1962
  builder: builder2,
1456
1963
  outputPath,
1457
- extra: { technique: args.technique, glsl_pattern: pattern.id }
1964
+ controls: speedControls,
1965
+ extra: { technique: args.technique, color_palette: args.color_palette }
1458
1966
  });
1459
1967
  }
1460
1968
  const builder = await createSystemContainer(
@@ -1469,7 +1977,7 @@ async function createGenerativeArtImpl(ctx, args) {
1469
1977
  await builder.connect(level, out);
1470
1978
  await builder.python(
1471
1979
  `p = op(${q6(noise)}).par.tz
1472
- p.expr = ${q6(`absTime.seconds * ${args.evolution_speed}`)}`
1980
+ p.expr = ${q6(`absTime.seconds * (parent().par.Speed.eval() if hasattr(parent().par, 'Speed') else ${args.evolution_speed})`)}`
1473
1981
  );
1474
1982
  builder.warnings.push(
1475
1983
  `Technique "${args.technique}" is approximated with an animated-noise generator in this version.`
@@ -1478,6 +1986,7 @@ p.expr = ${q6(`absTime.seconds * ${args.evolution_speed}`)}`
1478
1986
  summary: `Created an approximate "${args.technique}" generative system.`,
1479
1987
  builder,
1480
1988
  outputPath: out,
1989
+ controls: speedControls,
1481
1990
  extra: { technique: args.technique, evolution_speed: args.evolution_speed }
1482
1991
  });
1483
1992
  });
@@ -1496,7 +2005,7 @@ var registerCreateGenerativeArt = (server, ctx) => {
1496
2005
  };
1497
2006
 
1498
2007
  // src/tools/layer1/createParticleSystem.ts
1499
- import { z as z11 } from "zod";
2008
+ import { z as z12 } from "zod";
1500
2009
  var q7 = (value) => JSON.stringify(value);
1501
2010
  var EMITTER_SOP = {
1502
2011
  point: "addSOP",
@@ -1506,13 +2015,51 @@ var EMITTER_SOP = {
1506
2015
  mesh: "boxSOP",
1507
2016
  image: "gridSOP"
1508
2017
  };
1509
- var createParticleSystemSchema = z11.object({
1510
- emitter_shape: z11.enum(["point", "line", "circle", "sphere", "mesh", "image"]).default("point"),
1511
- particle_count: z11.number().int().positive().default(1e4),
1512
- forces: z11.array(z11.enum(["gravity", "noise", "attract", "repel", "vortex", "turbulence", "drag"])).default(["noise", "gravity"]),
1513
- render_style: z11.enum(["points", "sprites", "lines", "trails", "instanced_geo"]).default("sprites"),
1514
- lifetime: z11.number().positive().default(3),
1515
- parent_path: z11.string().default("/project1")
2018
+ var q22 = (n) => Number.isFinite(n) ? n.toString() : "0";
2019
+ function computeParticleDynamics(args) {
2020
+ const f = new Set(args.forces);
2021
+ const birth = Math.max(1, Math.round(args.particle_count / args.lifetime));
2022
+ let drag = 2;
2023
+ if (f.has("drag")) drag = 3.5;
2024
+ if (f.has("repel")) drag = 1;
2025
+ let turb = 0;
2026
+ if (f.has("noise")) turb = 1;
2027
+ if (f.has("turbulence")) turb = 1.8;
2028
+ if (f.has("attract") || f.has("repel") || f.has("vortex")) turb = Math.max(turb, 1.2);
2029
+ const gravity = f.has("gravity") ? -0.6 : 0;
2030
+ const wind = f.has("vortex") ? 0.5 : 0;
2031
+ const lifevar = Number((args.lifetime * 0.25).toFixed(3));
2032
+ return { birth, drag, turb, gravity, wind, lifevar };
2033
+ }
2034
+ function particleDynamicsPython(particlePath, args, dyn) {
2035
+ return [
2036
+ `_p = op(${q7(particlePath)})`,
2037
+ `_p.par.birth = ${dyn.birth}`,
2038
+ `_p.par.lifevar = ${q22(dyn.lifevar)}`,
2039
+ "_p.par.normals = True",
2040
+ "_p.par.jitter = True",
2041
+ `_p.par.timepreroll = ${q22(args.lifetime)}`,
2042
+ "_p.par.dodrag = True",
2043
+ `_p.par.drag = ${q22(dyn.drag)}`,
2044
+ `_p.par.turbx = ${q22(dyn.turb)}`,
2045
+ `_p.par.turby = ${q22(dyn.turb)}`,
2046
+ `_p.par.turbz = ${q22(dyn.turb)}`,
2047
+ "_p.par.period = 3.0",
2048
+ `_p.par.externaly = ${q22(dyn.gravity)}`,
2049
+ `_p.par.windx = ${q22(dyn.wind)}`,
2050
+ "_p.par.reset.pulse()"
2051
+ ].join("\n");
2052
+ }
2053
+ var createParticleSystemSchema = z12.object({
2054
+ // Default to "sphere": its varied normals give particles a radial initial velocity, so the
2055
+ // out-of-the-box system is a full cloud. ("point" has no normals and stays a thin stream.)
2056
+ emitter_shape: z12.enum(["point", "line", "circle", "sphere", "mesh", "image"]).default("sphere"),
2057
+ particle_count: z12.coerce.number().int().positive().default(1e4),
2058
+ forces: z12.array(z12.enum(["gravity", "noise", "attract", "repel", "vortex", "turbulence", "drag"])).default(["noise", "gravity"]),
2059
+ render_style: z12.enum(["points", "sprites", "lines", "trails", "instanced_geo"]).default("sprites"),
2060
+ lifetime: z12.coerce.number().positive().default(3),
2061
+ expose_controls: z12.boolean().default(true).describe("Expose live Drag / Turbulence / Gravity / Lifetime knobs on the system container."),
2062
+ parent_path: z12.string().default("/project1")
1516
2063
  });
1517
2064
  async function createParticleSystemImpl(ctx, args) {
1518
2065
  return runBuild(async () => {
@@ -1533,14 +2080,28 @@ async function createParticleSystemImpl(ctx, args) {
1533
2080
  { name: "emitter", path: emitter.path, type: emitter.type },
1534
2081
  { name: "particle", path: particle.path, type: particle.type }
1535
2082
  );
2083
+ if (args.emitter_shape === "point") {
2084
+ await builder.python(
2085
+ `_e = op(${q7(emitter.path)})
2086
+ _e.par.addpts = True
2087
+ _e.seq.point.numBlocks = max(_e.seq.point.numBlocks, 1)`
2088
+ );
2089
+ }
1536
2090
  await builder.connect(emitter.path, particle.path);
2091
+ const dynamics = computeParticleDynamics(args);
2092
+ await builder.python(particleDynamicsPython(particle.path, args, dynamics));
1537
2093
  const mat = await builder.add(
1538
2094
  args.render_style === "sprites" ? "pointspriteMAT" : "constantMAT",
1539
2095
  "mat"
1540
2096
  );
1541
- const cam = await builder.add("cameraCOMP", "cam", { tz: 5 });
2097
+ const cam = await builder.add("cameraCOMP", "cam", { tz: 6 });
1542
2098
  const light = await builder.add("lightCOMP", "light", { tx: 3, ty: 4, tz: 4 });
1543
- const render = await builder.add("renderTOP", "render");
2099
+ const render = await builder.add("renderTOP", "render", {
2100
+ bgcolorr: 0.02,
2101
+ bgcolorg: 0.02,
2102
+ bgcolorb: 0.05,
2103
+ bgcolora: 1
2104
+ });
1544
2105
  const out = await builder.add("nullTOP", "out1");
1545
2106
  await builder.connect(render, out);
1546
2107
  await builder.python(
@@ -1556,13 +2117,58 @@ async function createParticleSystemImpl(ctx, args) {
1556
2117
  `r.par.lights = ${q7(light)}`
1557
2118
  ].join("\n")
1558
2119
  );
1559
- builder.warnings.push(
1560
- `Particle forces (${args.forces.join(", ")}), exact count (${args.particle_count}) and the "${args.render_style}" render style are scaffolded; tune the particleSOP and material for production.`
2120
+ const approximated = args.forces.filter(
2121
+ (x) => x === "attract" || x === "repel" || x === "vortex"
1561
2122
  );
2123
+ if (approximated.length > 0) {
2124
+ builder.warnings.push(
2125
+ `Forces ${approximated.join(", ")} have no native Particle SOP equivalent and are approximated with turbulence.`
2126
+ );
2127
+ }
2128
+ if (args.render_style !== "points" && args.render_style !== "sprites") {
2129
+ builder.warnings.push(
2130
+ `Render style "${args.render_style}" falls back to point/sprite rendering in this version.`
2131
+ );
2132
+ }
2133
+ const controls = args.expose_controls ? [
2134
+ {
2135
+ name: "Drag",
2136
+ type: "float",
2137
+ min: 0,
2138
+ max: 8,
2139
+ default: dynamics.drag,
2140
+ bind_to: [`${particle.path}.drag`]
2141
+ },
2142
+ {
2143
+ name: "Turbulence",
2144
+ type: "float",
2145
+ min: 0,
2146
+ max: 4,
2147
+ default: dynamics.turb,
2148
+ bind_to: [`${particle.path}.turbx`, `${particle.path}.turby`, `${particle.path}.turbz`]
2149
+ },
2150
+ {
2151
+ name: "Gravity",
2152
+ type: "float",
2153
+ min: -3,
2154
+ max: 3,
2155
+ default: dynamics.gravity,
2156
+ bind_to: [`${particle.path}.externaly`]
2157
+ },
2158
+ {
2159
+ name: "Lifetime",
2160
+ type: "float",
2161
+ min: 0.1,
2162
+ max: 10,
2163
+ default: args.lifetime,
2164
+ bind_to: [`${particle.path}.life`]
2165
+ }
2166
+ ] : [];
1562
2167
  return finalize(ctx, {
1563
2168
  summary: `Created a particle system (emitter: ${args.emitter_shape}, ~${args.particle_count} particles, render: ${args.render_style}).`,
1564
2169
  builder,
1565
2170
  outputPath: out,
2171
+ controls,
1566
2172
  extra: {
1567
2173
  emitter_shape: args.emitter_shape,
1568
2174
  forces: args.forces,
@@ -1585,7 +2191,7 @@ var registerCreateParticleSystem = (server, ctx) => {
1585
2191
  };
1586
2192
 
1587
2193
  // src/tools/layer1/createVisualSystem.ts
1588
- import { z as z12 } from "zod";
2194
+ import { z as z13 } from "zod";
1589
2195
 
1590
2196
  // src/tools/layer1/intent.ts
1591
2197
  var GENERIC_TERMS = /* @__PURE__ */ new Set([
@@ -1611,11 +2217,15 @@ function significantTerms(description) {
1611
2217
  }
1612
2218
 
1613
2219
  // src/tools/layer1/createVisualSystem.ts
1614
- var createVisualSystemSchema = z12.object({
1615
- description: z12.string().min(1).describe("Natural-language description of the visual system."),
1616
- parent_path: z12.string().default("/project1"),
1617
- resolution: z12.enum(["720p", "1080p", "4K", "custom"]).default("1080p"),
1618
- target_fps: z12.number().positive().default(60)
2220
+ var createVisualSystemSchema = z13.object({
2221
+ description: z13.string().min(1).describe("Natural-language description of the visual system."),
2222
+ parent_path: z13.string().default("/project1"),
2223
+ resolution: z13.enum(["720p", "1080p", "4K", "custom"]).default("1080p").describe(
2224
+ "Advisory target resolution. Recorded in the build note; the sub-builders use their own internal sizes and do not enforce this per-node."
2225
+ ),
2226
+ target_fps: z13.coerce.number().positive().default(60).describe(
2227
+ "Advisory target frame rate (informational only \u2014 TD's real cook rate is a project-level setting, not set here)."
2228
+ )
1619
2229
  });
1620
2230
  function classify(description) {
1621
2231
  const d = description.toLowerCase();
@@ -1687,7 +2297,7 @@ function withNote(result, note) {
1687
2297
  }
1688
2298
  async function createVisualSystemImpl(ctx, args) {
1689
2299
  const { kind, label } = classify(args.description);
1690
- const note = `Interpreted "${args.description}" as a ${label} system (target ${args.resolution} @ ${args.target_fps}fps).`;
2300
+ const note = `Interpreted "${args.description}" as a ${label} system (advisory target ${args.resolution} @ ${args.target_fps}fps).`;
1691
2301
  ctx.logger.info("create_visual_system classified", { kind, description: args.description });
1692
2302
  switch (kind) {
1693
2303
  case "audio":
@@ -1697,6 +2307,7 @@ async function createVisualSystemImpl(ctx, args) {
1697
2307
  visual_style: pickAudioStyle(args.description),
1698
2308
  frequency_bands: 8,
1699
2309
  beat_detection: true,
2310
+ expose_controls: true,
1700
2311
  parent_path: args.parent_path
1701
2312
  }),
1702
2313
  note
@@ -1709,6 +2320,7 @@ async function createVisualSystemImpl(ctx, args) {
1709
2320
  forces: ["noise", "gravity"],
1710
2321
  render_style: "sprites",
1711
2322
  lifetime: 3,
2323
+ expose_controls: true,
1712
2324
  parent_path: args.parent_path
1713
2325
  }),
1714
2326
  note
@@ -1720,6 +2332,7 @@ async function createVisualSystemImpl(ctx, args) {
1720
2332
  transformations: ["blur", "displace", "level"],
1721
2333
  feedback_gain: 0.95,
1722
2334
  colors: parseColors(args.description),
2335
+ expose_controls: true,
1723
2336
  parent_path: args.parent_path
1724
2337
  }),
1725
2338
  note
@@ -1730,6 +2343,7 @@ async function createVisualSystemImpl(ctx, args) {
1730
2343
  await createGenerativeArtImpl(ctx, {
1731
2344
  technique: kind,
1732
2345
  evolution_speed: 1,
2346
+ expose_controls: true,
1733
2347
  parent_path: args.parent_path
1734
2348
  }),
1735
2349
  note
@@ -1738,13 +2352,18 @@ async function createVisualSystemImpl(ctx, args) {
1738
2352
  const recipe = ctx.recipes.findByTags(significantTerms(args.description));
1739
2353
  if (recipe) {
1740
2354
  return runBuild(async () => {
1741
- const { builder, outputPath } = await buildFromRecipe(ctx, recipe, args.parent_path);
2355
+ const { builder, outputPath, controls } = await buildFromRecipe(
2356
+ ctx,
2357
+ recipe,
2358
+ args.parent_path
2359
+ );
1742
2360
  return withNote(
1743
2361
  await finalize(ctx, {
1744
2362
  summary: `Built "${recipe.name}" from a matching recipe.`,
1745
2363
  builder,
1746
2364
  outputPath,
1747
- recipeId: recipe.id
2365
+ recipeId: recipe.id,
2366
+ controls
1748
2367
  }),
1749
2368
  note
1750
2369
  );
@@ -1754,6 +2373,7 @@ async function createVisualSystemImpl(ctx, args) {
1754
2373
  await createGenerativeArtImpl(ctx, {
1755
2374
  technique: "custom_glsl",
1756
2375
  evolution_speed: 1,
2376
+ expose_controls: true,
1757
2377
  parent_path: args.parent_path
1758
2378
  }),
1759
2379
  note
@@ -1775,9 +2395,9 @@ var registerCreateVisualSystem = (server, ctx) => {
1775
2395
  };
1776
2396
 
1777
2397
  // src/tools/layer1/describeProject.ts
1778
- import { z as z13 } from "zod";
1779
- var describeProjectSchema = z13.object({
1780
- description: z13.string().min(1).describe("Natural-language description of the visual you want.")
2398
+ import { z as z14 } from "zod";
2399
+ var describeProjectSchema = z14.object({
2400
+ description: z14.string().min(1).describe("Natural-language description of the visual you want.")
1781
2401
  });
1782
2402
  function describeProjectImpl(ctx, args) {
1783
2403
  const d = args.description.toLowerCase();
@@ -1803,7 +2423,7 @@ function describeProjectImpl(ctx, args) {
1803
2423
  tool = "create_feedback_network";
1804
2424
  summary = "feedback network";
1805
2425
  } else {
1806
- tool = "create_generative_art";
2426
+ tool = "create_visual_system";
1807
2427
  summary = "generative visual";
1808
2428
  recipeId = ctx.recipes.findByTags(significantTerms(d))?.id;
1809
2429
  }
@@ -1839,11 +2459,11 @@ var registerDescribeProject = (server, ctx) => {
1839
2459
  };
1840
2460
 
1841
2461
  // src/tools/layer1/getPreview.ts
1842
- import { z as z14 } from "zod";
1843
- var getPreviewSchema = z14.object({
1844
- node_path: z14.string().describe("Path of the TOP node to capture."),
1845
- width: z14.number().int().positive().default(640),
1846
- height: z14.number().int().positive().default(360)
2462
+ import { z as z15 } from "zod";
2463
+ var getPreviewSchema = z15.object({
2464
+ node_path: z15.string().describe("Path of the TOP node to capture."),
2465
+ width: z15.coerce.number().int().positive().default(640),
2466
+ height: z15.coerce.number().int().positive().default(360)
1847
2467
  });
1848
2468
  async function getPreviewImpl(ctx, args) {
1849
2469
  return guardTd(
@@ -1869,7 +2489,7 @@ var registerGetPreview = (server, ctx) => {
1869
2489
  };
1870
2490
 
1871
2491
  // src/tools/layer1/setupOutput.ts
1872
- import { z as z15 } from "zod";
2492
+ import { z as z16 } from "zod";
1873
2493
  var q8 = (value) => JSON.stringify(value);
1874
2494
  var OUTPUT_MAP = {
1875
2495
  window: "windowCOMP",
@@ -1883,12 +2503,12 @@ var RESOLUTIONS = {
1883
2503
  "1080p": [1920, 1080],
1884
2504
  "4K": [3840, 2160]
1885
2505
  };
1886
- var setupOutputSchema = z15.object({
1887
- source_path: z15.string().describe("Path of the final TOP to output."),
1888
- output_type: z15.enum(["window", "ndi", "syphon_spout", "record", "touch_out"]).default("window"),
1889
- resolution: z15.enum(["720p", "1080p", "4K"]).default("1080p"),
1890
- record_format: z15.enum(["mp4", "mov", "image_sequence"]).optional(),
1891
- parent_path: z15.string().default("/project1")
2506
+ var setupOutputSchema = z16.object({
2507
+ source_path: z16.string().describe("Path of the final TOP to output."),
2508
+ output_type: z16.enum(["window", "ndi", "syphon_spout", "record", "touch_out"]).default("window"),
2509
+ resolution: z16.enum(["720p", "1080p", "4K"]).default("1080p"),
2510
+ record_format: z16.enum(["mp4", "mov", "image_sequence"]).optional(),
2511
+ parent_path: z16.string().default("/project1")
1892
2512
  });
1893
2513
  async function setupOutputImpl(ctx, args) {
1894
2514
  return runBuild(async () => {
@@ -1913,7 +2533,13 @@ w.par.winh = ${height}`,
1913
2533
  }
1914
2534
  } else {
1915
2535
  try {
1916
- await connectNodesViaBridge(ctx.client, args.source_path, node.path);
2536
+ const select = await ctx.client.createNode({
2537
+ parent_path: args.parent_path,
2538
+ type: "selectTOP",
2539
+ name: `${args.output_type}_src`
2540
+ });
2541
+ await ctx.client.updateNodeParameters(select.path, { top: args.source_path });
2542
+ await connectNodesViaBridge(ctx.client, select.path, node.path);
1917
2543
  } catch (err) {
1918
2544
  warnings.push(`Could not connect source to output: ${friendlyTdError(err)}`);
1919
2545
  }
@@ -1961,13 +2587,165 @@ var layer1Registrars = [
1961
2587
  registerCreateVisualSystem
1962
2588
  ];
1963
2589
 
2590
+ // src/tools/layer2/animateParameter.ts
2591
+ import { z as z17 } from "zod";
2592
+ var WAVE_MAP = {
2593
+ sine: "sin",
2594
+ triangle: "tri",
2595
+ ramp: "ramp",
2596
+ square: "square",
2597
+ pulse: "pulse",
2598
+ random: "normal"
2599
+ };
2600
+ var animateParameterSchema = z17.object({
2601
+ targets: z17.array(z17.string()).min(1).describe(
2602
+ "Parameters to animate, each written as 'nodePath.parName' (e.g. '/project1/sys/blur1.size'). Each is switched to expression mode so it tracks the oscillator live."
2603
+ ),
2604
+ waveform: z17.enum(["sine", "triangle", "ramp", "square", "pulse", "random"]).default("sine").describe("Oscillator shape. Every waveform sweeps the full min\u2013max range."),
2605
+ min: z17.coerce.number().default(0).describe("Low end of the value sweep."),
2606
+ max: z17.coerce.number().default(1).describe("High end of the value sweep."),
2607
+ period_seconds: z17.coerce.number().positive().default(4).describe("Seconds for one full cycle (lower = faster)."),
2608
+ container_path: z17.string().optional().describe("Where to create the LFO CHOP; defaults to the first target's parent network."),
2609
+ name: z17.string().default("lfo_anim").describe("Name for the LFO CHOP.")
2610
+ });
2611
+ var ANIMATE_SCRIPT = `
2612
+ import json, base64, traceback
2613
+ _p = json.loads(base64.b64decode("__PAYLOAD_B64__").decode("utf-8"))
2614
+ report = {"targets_bound": [], "warnings": []}
2615
+ try:
2616
+ _targets = _p["targets"]
2617
+ _first = _targets[0]; _dot0 = _first.rfind(".")
2618
+ if _dot0 <= 0:
2619
+ report["fatal"] = "Invalid target '%s' (expected 'nodePath.parName')." % _first
2620
+ else:
2621
+ _np0 = _first[:_dot0]; _firstnode = op(_np0)
2622
+ if _firstnode is None:
2623
+ report["fatal"] = "Target node not found: %s" % _np0
2624
+ else:
2625
+ _cont = _p.get("container") or _firstnode.parent().path
2626
+ _parent = op(_cont)
2627
+ if _parent is None:
2628
+ report["fatal"] = "Container not found: %s" % _cont
2629
+ else:
2630
+ _lfo = _parent.create(lfoCHOP, _p["name"])
2631
+ _lfo.par.wavetype = _p["wavetype"]
2632
+ _lfo.par.frequency = _p["frequency"]
2633
+ _lfo.par.amp = _p["amp"]
2634
+ _lfo.par.offset = _p["offset"]
2635
+ _ch = _lfo.par.channelname.eval() or "chan1"
2636
+ report["lfo"] = _lfo.path; report["container"] = _cont
2637
+ report["channel"] = _ch; report["frequency"] = _p["frequency"]
2638
+ for _t in _targets:
2639
+ try:
2640
+ _dot = _t.rfind(".")
2641
+ if _dot <= 0:
2642
+ report["warnings"].append("Invalid target '%s' (expected 'nodePath.parName')." % _t); continue
2643
+ _npth = _t[:_dot]; _pn = _t[_dot + 1:]; _tn = op(_npth)
2644
+ if _tn is None:
2645
+ report["warnings"].append("Target node not found: %s" % _npth); continue
2646
+ _tp = getattr(_tn.par, _pn, None)
2647
+ if _tp is None:
2648
+ report["warnings"].append("Target parameter not found: %s.%s" % (_npth, _pn)); continue
2649
+ _PM = type(_tp.mode)
2650
+ _tp.expr = "op(%s)[%s]" % (repr(_lfo.path), repr(_ch))
2651
+ _tp.mode = _PM.EXPRESSION
2652
+ report["targets_bound"].append(_npth + "." + _pn)
2653
+ except Exception:
2654
+ report["warnings"].append("Failed to bind '%s': %s" % (_t, traceback.format_exc().splitlines()[-1]))
2655
+ except Exception:
2656
+ report["fatal"] = traceback.format_exc().splitlines()[-1]
2657
+ print(json.dumps(report))
2658
+ `;
2659
+ function buildAnimateScript(payload) {
2660
+ return buildPayloadScript(ANIMATE_SCRIPT, payload);
2661
+ }
2662
+ var BIPOLAR_WAVEFORMS = /* @__PURE__ */ new Set(["sine", "triangle", "square"]);
2663
+ async function animateParameterImpl(ctx, args) {
2664
+ const span = args.max - args.min;
2665
+ const bipolar = BIPOLAR_WAVEFORMS.has(args.waveform);
2666
+ const amp = bipolar ? span / 2 : span;
2667
+ const offset = bipolar ? (args.max + args.min) / 2 : args.min;
2668
+ const frequency = 1 / args.period_seconds;
2669
+ return guardTd(
2670
+ async () => {
2671
+ const script = buildAnimateScript({
2672
+ targets: args.targets,
2673
+ name: args.name,
2674
+ wavetype: WAVE_MAP[args.waveform],
2675
+ frequency,
2676
+ amp,
2677
+ offset,
2678
+ container: args.container_path ?? null
2679
+ });
2680
+ const exec = await ctx.client.executePythonScript(script, true);
2681
+ return parsePythonReport(exec.stdout);
2682
+ },
2683
+ (report) => {
2684
+ if (report.fatal) {
2685
+ return jsonResult(`Could not set up animation: ${report.fatal}`, report);
2686
+ }
2687
+ const summary = `Animating ${report.targets_bound.length} parameter(s) with a ${args.waveform} LFO (period ${args.period_seconds}s, range ${args.min}\u2013${args.max}) at ${report.lfo}${report.warnings.length ? `, ${report.warnings.length} warning(s)` : ""}.`;
2688
+ return jsonResult(summary, report);
2689
+ }
2690
+ );
2691
+ }
2692
+ var registerAnimateParameter = (server, ctx) => {
2693
+ server.registerTool(
2694
+ "animate_parameter",
2695
+ {
2696
+ title: "Animate parameter",
2697
+ description: "Drive one or more node parameters over time with an LFO (sine/triangle/ramp/square/pulse/random). Creates an LFO CHOP and binds each target so it oscillates between min and max with the given period \u2014 movement without manual keyframing.",
2698
+ inputSchema: animateParameterSchema.shape,
2699
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
2700
+ },
2701
+ (args) => animateParameterImpl(ctx, args)
2702
+ );
2703
+ };
2704
+
2705
+ // src/tools/layer2/arrangeNetwork.ts
2706
+ import { z as z18 } from "zod";
2707
+ var arrangeNetworkSchema = z18.object({
2708
+ path: z18.string().describe("COMP whose children to arrange, e.g. '/project1' or a container path."),
2709
+ recursive: z18.boolean().default(false).describe("Also arrange the nodes inside nested COMPs (each network is tidied on its own).")
2710
+ });
2711
+ async function arrangeNetworkImpl(ctx, args) {
2712
+ return guardTd(
2713
+ async () => {
2714
+ const topology = await ctx.client.getNetworkTopology(args.path, args.recursive);
2715
+ const nodes = topology.nodes.map((n) => n.path);
2716
+ const edges = topology.connections.map((c) => ({ from: c.source_path, to: c.target_path }));
2717
+ const positions = computeLayoutByParent(nodes, edges);
2718
+ if (nodes.length > 0) {
2719
+ await ctx.client.executePythonScript(layoutScript(positions), false);
2720
+ }
2721
+ return Object.keys(positions).length;
2722
+ },
2723
+ (arranged) => jsonResult(
2724
+ arranged === 0 ? `No nodes to arrange under ${args.path}.` : `Arranged ${arranged} node(s) under ${args.path} into a left\u2192right data-flow layout.`,
2725
+ { path: args.path, arranged, recursive: args.recursive }
2726
+ )
2727
+ );
2728
+ }
2729
+ var registerArrangeNetwork = (server, ctx) => {
2730
+ server.registerTool(
2731
+ "arrange_network",
2732
+ {
2733
+ title: "Arrange network layout",
2734
+ description: "Tidy an existing network: reposition a COMP's children into a readable left\u2192right data-flow layout (sources on the left, output on the right). Use this to clean up nodes that are piled on top of each other. Set recursive to also arrange the contents of nested COMPs.",
2735
+ inputSchema: arrangeNetworkSchema.shape,
2736
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
2737
+ },
2738
+ (args) => arrangeNetworkImpl(ctx, args)
2739
+ );
2740
+ };
2741
+
1964
2742
  // src/tools/layer2/connectNodes.ts
1965
- import { z as z16 } from "zod";
1966
- var connectNodesSchema = z16.object({
1967
- source_path: z16.string().describe("Path of the source node (output side)."),
1968
- target_path: z16.string().describe("Path of the target node (input side)."),
1969
- source_output: z16.number().int().nonnegative().default(0),
1970
- target_input: z16.number().int().nonnegative().default(0)
2743
+ import { z as z19 } from "zod";
2744
+ var connectNodesSchema = z19.object({
2745
+ source_path: z19.string().describe("Path of the source node (output side)."),
2746
+ target_path: z19.string().describe("Path of the target node (input side)."),
2747
+ source_output: z19.number().int().nonnegative().default(0),
2748
+ target_input: z19.number().int().nonnegative().default(0)
1971
2749
  });
1972
2750
  async function connectNodesImpl(ctx, args) {
1973
2751
  return guardTd(
@@ -2001,23 +2779,34 @@ var registerConnectNodes = (server, ctx) => {
2001
2779
  };
2002
2780
 
2003
2781
  // src/tools/layer2/createContainer.ts
2004
- import { z as z17 } from "zod";
2782
+ import { z as z20 } from "zod";
2005
2783
  var COMP_MAP = {
2006
2784
  container: "containerCOMP",
2007
2785
  base: "baseCOMP"
2008
2786
  };
2009
- var createContainerSchema = z17.object({
2010
- parent_path: z17.string().default("/project1").describe("Parent COMP to create the container in."),
2011
- name: z17.string().optional(),
2012
- comp_type: z17.enum(["container", "base"]).default("container").describe("'container' (2D panel COMP) or 'base' (generic COMP).")
2787
+ var createContainerSchema = z20.object({
2788
+ parent_path: z20.string().default("/project1").describe("Parent COMP to create the container in."),
2789
+ name: z20.string().optional(),
2790
+ comp_type: z20.enum(["container", "base"]).default("container").describe("'container' (2D panel COMP) or 'base' (generic COMP).")
2013
2791
  });
2014
2792
  async function createContainerImpl(ctx, args) {
2015
2793
  return guardTd(
2016
- () => ctx.client.createNode({
2017
- parent_path: args.parent_path,
2018
- type: COMP_MAP[args.comp_type],
2019
- name: args.name
2020
- }),
2794
+ async () => {
2795
+ const node = await ctx.client.createNode({
2796
+ parent_path: args.parent_path,
2797
+ type: COMP_MAP[args.comp_type],
2798
+ name: args.name
2799
+ });
2800
+ try {
2801
+ await ctx.client.executePythonScript(
2802
+ placeBelowSiblingsScript(args.parent_path, node.path),
2803
+ false
2804
+ );
2805
+ } catch (err) {
2806
+ ctx.logger.debug("container placement skipped", { err: String(err) });
2807
+ }
2808
+ return node;
2809
+ },
2021
2810
  (node) => jsonResult(`Created ${args.comp_type} COMP at ${node.path}.`, { node })
2022
2811
  );
2023
2812
  }
@@ -2034,20 +2823,161 @@ var registerCreateContainer = (server, ctx) => {
2034
2823
  );
2035
2824
  };
2036
2825
 
2826
+ // src/tools/layer2/createExternalIo.ts
2827
+ import { z as z21 } from "zod";
2828
+ var bindSchema = z21.object({
2829
+ channel: z21.string().describe(
2830
+ "Channel name produced by the input (e.g. an OSC address tail 'fader1', or a MIDI channel name)."
2831
+ ),
2832
+ target: z21.string().describe("Parameter to drive, written as 'nodePath.parName'.")
2833
+ });
2834
+ var createExternalIoSchema = z21.object({
2835
+ kind: z21.enum(["osc_in", "midi_in", "dmx_out", "ndi_in", "syphon_spout_in"]).describe(
2836
+ "What to bridge: OSC input, MIDI input, DMX/Art-Net output (lighting), or NDI / Syphon-Spout video input. (Video/NDI/Syphon *outputs* live in setup_output.)"
2837
+ ),
2838
+ parent_path: z21.string().default("/project1").describe("COMP to create the I/O operator in."),
2839
+ name: z21.string().optional(),
2840
+ port: z21.coerce.number().int().optional().describe("(osc_in) UDP port to listen on. Defaults to 7000."),
2841
+ normalize: z21.enum(["off", "0to1", "-1to1", "onoff"]).default("0to1").describe("(midi_in) How to scale incoming MIDI values."),
2842
+ bind_to: z21.array(bindSchema).optional().describe(
2843
+ "(osc_in/midi_in) Map incoming channels to parameters. Each binding tolerates a channel that hasn't arrived yet (falls back to 0 instead of erroring)."
2844
+ ),
2845
+ source_path: z21.string().optional().describe("(dmx_out) CHOP whose channel values are sent out as DMX."),
2846
+ interface: z21.enum(["artnet", "sacn", "enttecusbpro", "enttecusbpromk2", "serial", "kinet"]).default("artnet").describe("(dmx_out) DMX transport."),
2847
+ universe: z21.coerce.number().int().default(1).describe("(dmx_out) DMX universe."),
2848
+ net_address: z21.string().optional().describe("(dmx_out) Target IP address for Art-Net / sACN."),
2849
+ source_name: z21.string().optional().describe("(ndi_in/syphon_spout_in) Name of the NDI source or Spout sender to receive.")
2850
+ });
2851
+ var IO_SCRIPT = `
2852
+ import json, base64, traceback
2853
+ _p = json.loads(base64.b64decode("__PAYLOAD_B64__").decode("utf-8"))
2854
+ report = {"kind": _p["kind"], "warnings": []}
2855
+ _TYPEMAP = {"osc_in": oscinCHOP, "midi_in": midiinCHOP, "dmx_out": dmxoutCHOP, "ndi_in": ndiinTOP, "syphon_spout_in": syphonspoutinTOP}
2856
+ try:
2857
+ _kind = _p["kind"]; _parent = op(_p["parent"])
2858
+ if _parent is None:
2859
+ report["fatal"] = "Parent COMP not found: " + str(_p["parent"])
2860
+ else:
2861
+ _name = _p.get("name")
2862
+ _node = _parent.create(_TYPEMAP[_kind], _name) if _name else _parent.create(_TYPEMAP[_kind])
2863
+ report["node"] = _node.path; report["type"] = _node.type
2864
+ def _setpar(parname, val):
2865
+ if val is None:
2866
+ return
2867
+ pr = getattr(_node.par, parname, None)
2868
+ if pr is None:
2869
+ report["warnings"].append("No parameter '%s' on %s" % (parname, _node.type)); return
2870
+ try:
2871
+ pr.val = val
2872
+ except Exception:
2873
+ report["warnings"].append("Could not set parameter '%s'" % parname)
2874
+ if _kind == "osc_in":
2875
+ _setpar("port", _p.get("port"))
2876
+ elif _kind == "midi_in":
2877
+ _setpar("norm", _p.get("normalize"))
2878
+ elif _kind == "dmx_out":
2879
+ _setpar("interface", _p.get("interface")); _setpar("universe", _p.get("universe")); _setpar("netaddress", _p.get("net_address"))
2880
+ _src = _p.get("source")
2881
+ if _src:
2882
+ _s = op(_src)
2883
+ if _s is None:
2884
+ report["warnings"].append("Source CHOP not found: " + _src)
2885
+ else:
2886
+ try:
2887
+ _node.inputConnectors[0].connect(_s); report["source"] = _s.path
2888
+ except Exception:
2889
+ report["warnings"].append("Could not connect source " + _src)
2890
+ elif _kind == "ndi_in":
2891
+ _setpar("name", _p.get("source_name"))
2892
+ elif _kind == "syphon_spout_in":
2893
+ _setpar("sendername", _p.get("source_name"))
2894
+ _bound = []
2895
+ if _kind in ("osc_in", "midi_in"):
2896
+ for _b in (_p.get("bind_to") or []):
2897
+ try:
2898
+ _ch = _b["channel"]; _t = _b["target"]; _dot = _t.rfind(".")
2899
+ if _dot <= 0:
2900
+ report["warnings"].append("Invalid bind target '%s' (expected 'nodePath.parName')." % _t); continue
2901
+ _np = _t[:_dot]; _pn = _t[_dot + 1:]; _tn = op(_np)
2902
+ if _tn is None:
2903
+ report["warnings"].append("Bind target node not found: " + _np); continue
2904
+ _tp = getattr(_tn.par, _pn, None)
2905
+ if _tp is None:
2906
+ report["warnings"].append("Bind target parameter not found: %s.%s" % (_np, _pn)); continue
2907
+ _expr = "op(%r)[%r] if %r in [c.name for c in op(%r).chans()] else 0" % (_node.path, _ch, _ch, _node.path)
2908
+ _PM = type(_tp.mode); _tp.expr = _expr; _tp.mode = _PM.EXPRESSION
2909
+ _bound.append({"channel": _ch, "target": _np + "." + _pn})
2910
+ except Exception:
2911
+ report["warnings"].append("Bind failed: " + traceback.format_exc().splitlines()[-1])
2912
+ report["bound"] = _bound
2913
+ report["errors"] = [str(e) for e in _node.errors()][:3]
2914
+ except Exception:
2915
+ report["fatal"] = traceback.format_exc().splitlines()[-1]
2916
+ print(json.dumps(report))
2917
+ `;
2918
+ function buildIoScript(payload) {
2919
+ return buildPayloadScript(IO_SCRIPT, payload);
2920
+ }
2921
+ async function createExternalIoImpl(ctx, args) {
2922
+ return guardTd(
2923
+ async () => {
2924
+ const script = buildIoScript({
2925
+ kind: args.kind,
2926
+ parent: args.parent_path,
2927
+ name: args.name ?? null,
2928
+ port: args.kind === "osc_in" ? args.port ?? 7e3 : null,
2929
+ normalize: args.normalize,
2930
+ bind_to: args.bind_to ?? null,
2931
+ source: args.source_path ?? null,
2932
+ interface: args.interface,
2933
+ universe: args.universe,
2934
+ net_address: args.net_address ?? null,
2935
+ source_name: args.source_name ?? null
2936
+ });
2937
+ const exec = await ctx.client.executePythonScript(script, true);
2938
+ return parsePythonReport(exec.stdout);
2939
+ },
2940
+ (report) => {
2941
+ if (report.fatal) {
2942
+ return jsonResult(`Could not create ${report.kind}: ${report.fatal}`, report);
2943
+ }
2944
+ const bound = report.bound?.length ? `, ${report.bound.length} binding(s)` : "";
2945
+ const errs = report.errors?.length ? `, ${report.errors.length} node error(s)` : "";
2946
+ const warns = report.warnings.length ? `, ${report.warnings.length} warning(s)` : "";
2947
+ return jsonResult(
2948
+ `Created ${report.kind} (${report.type}) at ${report.node}${bound}${errs}${warns}.`,
2949
+ report
2950
+ );
2951
+ }
2952
+ );
2953
+ }
2954
+ var registerCreateExternalIo = (server, ctx) => {
2955
+ server.registerTool(
2956
+ "create_external_io",
2957
+ {
2958
+ title: "Create external I/O",
2959
+ description: "Bridge TouchDesigner to the outside world: OSC input or MIDI input (a control surface \u2014 bind incoming channels straight to parameters), DMX/Art-Net output for lighting, or NDI / Syphon-Spout video input. Validate live where possible, but real signal needs the hardware/sender present.",
2960
+ inputSchema: createExternalIoSchema.shape,
2961
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
2962
+ },
2963
+ (args) => createExternalIoImpl(ctx, args)
2964
+ );
2965
+ };
2966
+
2037
2967
  // src/tools/layer2/createGlslShader.ts
2038
- import { z as z18 } from "zod";
2039
- var UniformSchema = z18.object({
2040
- name: z18.string(),
2041
- type: z18.enum(["float", "vec2", "vec3", "vec4", "int", "sampler2D"]),
2042
- default_value: z18.string().optional()
2968
+ import { z as z22 } from "zod";
2969
+ var UniformSchema = z22.object({
2970
+ name: z22.string(),
2971
+ type: z22.enum(["float", "vec2", "vec3", "vec4", "int", "sampler2D"]),
2972
+ default_value: z22.string().optional()
2043
2973
  });
2044
- var createGlslShaderSchema = z18.object({
2045
- parent_path: z18.string().describe("Parent COMP to create the GLSL TOP inside."),
2046
- name: z18.string().optional().describe("Name for the GLSL TOP (default 'glsl1')."),
2047
- fragment_shader: z18.string().min(1).describe("GLSL fragment (pixel) shader source."),
2048
- vertex_shader: z18.string().optional().describe("Optional GLSL vertex shader source."),
2049
- uniforms: z18.array(UniformSchema).optional().describe("Optional uniform declarations to best-effort bind on the GLSL TOP."),
2050
- resolution: z18.enum(["720p", "1080p", "4K", "input"]).default("input")
2974
+ var createGlslShaderSchema = z22.object({
2975
+ parent_path: z22.string().describe("Parent COMP to create the GLSL TOP inside."),
2976
+ name: z22.string().optional().describe("Name for the GLSL TOP (default 'glsl1')."),
2977
+ fragment_shader: z22.string().min(1).describe("GLSL fragment (pixel) shader source."),
2978
+ vertex_shader: z22.string().optional().describe("Optional GLSL vertex shader source."),
2979
+ uniforms: z22.array(UniformSchema).optional().describe("Optional uniform declarations to best-effort bind on the GLSL TOP."),
2980
+ resolution: z22.enum(["720p", "1080p", "4K", "input"]).default("input")
2051
2981
  });
2052
2982
  var RESOLUTIONS2 = {
2053
2983
  "720p": [1280, 720],
@@ -2090,25 +3020,35 @@ async function createGlslShaderImpl(ctx, args) {
2090
3020
  }
2091
3021
  await ctx.client.executePythonScript(wiring.join("\n"), false);
2092
3022
  if (args.uniforms && args.uniforms.length > 0) {
2093
- const names = JSON.stringify(args.uniforms.map((u) => u.name));
2094
- const values = JSON.stringify(args.uniforms.map((u) => u.default_value ?? ""));
2095
- const bind = [
2096
- `g = op(${q9(glsl.path)})`,
2097
- `for i, nm in enumerate(${names}):`,
2098
- " try: setattr(g.par, 'uniname' + str(i), nm)",
2099
- " except Exception: pass",
2100
- `for i, v in enumerate(${values}):`,
2101
- " try:",
2102
- " if v != '': setattr(g.par, 'value' + str(i) + 'x', v)",
2103
- " except Exception: pass"
2104
- ].join("\n");
2105
- try {
2106
- await ctx.client.executePythonScript(bind, false);
2107
- } catch {
3023
+ const samplers = args.uniforms.filter((u) => u.type === "sampler2D");
3024
+ if (samplers.length > 0) {
2108
3025
  warnings.push(
2109
- "Could not auto-bind uniforms; declare them in the shader and set them on the GLSL TOP manually."
3026
+ `sampler2D uniform(s) ${samplers.map((u) => u.name).join(", ")} must be wired manually (GLSL TOP Samplers page / TOP inputs).`
2110
3027
  );
2111
3028
  }
3029
+ const specs = args.uniforms.filter((u) => u.type !== "sampler2D").map((u) => ({
3030
+ name: u.name,
3031
+ comps: (u.default_value ?? "").split(",").map((s) => Number(s.trim())).filter((n) => Number.isFinite(n)).slice(0, 4)
3032
+ })).filter((s) => s.comps.length > 0);
3033
+ if (specs.length > 0) {
3034
+ const bind = [
3035
+ `g = op(${q9(glsl.path)})`,
3036
+ `_specs = ${JSON.stringify(specs)}`,
3037
+ "g.seq.vec.numBlocks = max(g.seq.vec.numBlocks, len(_specs))",
3038
+ "for i, s in enumerate(_specs):",
3039
+ " setattr(g.par, 'vec%dname' % i, s['name'])",
3040
+ " for axis, val in zip('xyzw', s['comps']):",
3041
+ " try: setattr(g.par, 'vec%dvalue%s' % (i, axis), val)",
3042
+ " except Exception: pass"
3043
+ ].join("\n");
3044
+ try {
3045
+ await ctx.client.executePythonScript(bind, false);
3046
+ } catch {
3047
+ warnings.push(
3048
+ "Could not auto-bind uniforms; declare them in the shader and set them on the GLSL TOP's Vectors page manually."
3049
+ );
3050
+ }
3051
+ }
2112
3052
  }
2113
3053
  if (args.resolution !== "input") {
2114
3054
  const [width, height] = RESOLUTIONS2[args.resolution];
@@ -2137,16 +3077,16 @@ var registerCreateGlslShader = (server, ctx) => {
2137
3077
  };
2138
3078
 
2139
3079
  // src/tools/layer2/createNodeChain.ts
2140
- import { z as z19 } from "zod";
2141
- var ChainNodeSchema = z19.object({
2142
- type: z19.string().describe("Operator type, e.g. 'noiseTOP'."),
2143
- name: z19.string().optional(),
2144
- parameters: z19.record(z19.string(), z19.unknown()).optional()
3080
+ import { z as z23 } from "zod";
3081
+ var ChainNodeSchema = z23.object({
3082
+ type: z23.string().describe("Operator type, e.g. 'noiseTOP'."),
3083
+ name: z23.string().optional(),
3084
+ parameters: z23.record(z23.string(), z23.unknown()).optional()
2145
3085
  });
2146
- var createNodeChainSchema = z19.object({
2147
- parent_path: z19.string().describe("Parent COMP to create the chain inside."),
2148
- nodes: z19.array(ChainNodeSchema).min(1).describe("Ordered list of nodes to create."),
2149
- connect_sequentially: z19.boolean().default(true).describe("Wire output[0] \u2192 input[0] for each consecutive pair.")
3086
+ var createNodeChainSchema = z23.object({
3087
+ parent_path: z23.string().describe("Parent COMP to create the chain inside."),
3088
+ nodes: z23.array(ChainNodeSchema).min(1).describe("Ordered list of nodes to create."),
3089
+ connect_sequentially: z23.boolean().default(true).describe("Wire output[0] \u2192 input[0] for each consecutive pair.")
2150
3090
  });
2151
3091
  async function createNodeChainImpl(ctx, args) {
2152
3092
  const created = [];
@@ -2189,6 +3129,25 @@ async function createNodeChainImpl(ctx, args) {
2189
3129
  }
2190
3130
  }
2191
3131
  }
3132
+ const edges = [];
3133
+ if (args.connect_sequentially) {
3134
+ for (let i = 0; i < created.length - 1; i++) {
3135
+ const from = created[i];
3136
+ const to = created[i + 1];
3137
+ if (from && to) edges.push({ from: from.path, to: to.path });
3138
+ }
3139
+ }
3140
+ const positions = computeDataflowLayout(
3141
+ created.map((c) => c.path),
3142
+ edges
3143
+ );
3144
+ if (Object.keys(positions).length > 0) {
3145
+ try {
3146
+ await ctx.client.executePythonScript(layoutScript(positions), false);
3147
+ } catch (err) {
3148
+ warnings.push(`Auto-layout skipped: ${friendlyTdError(err)}`);
3149
+ }
3150
+ }
2192
3151
  return jsonResult(
2193
3152
  `Created ${created.length} node(s) and ${connections.length} connection(s) under ${args.parent_path}.`,
2194
3153
  { created, connections, warnings }
@@ -2208,17 +3167,17 @@ var registerCreateNodeChain = (server, ctx) => {
2208
3167
  };
2209
3168
 
2210
3169
  // src/tools/layer2/createPythonScript.ts
2211
- import { z as z20 } from "zod";
3170
+ import { z as z24 } from "zod";
2212
3171
  var TYPE_MAP = {
2213
3172
  text: "textDAT",
2214
3173
  execute: "executeDAT",
2215
3174
  script: "scriptDAT"
2216
3175
  };
2217
- var createPythonScriptSchema = z20.object({
2218
- parent_path: z20.string().describe("Parent COMP to create the DAT inside."),
2219
- name: z20.string().optional(),
2220
- code: z20.string().min(1).describe("Python source to place in the DAT."),
2221
- dat_type: z20.enum(["text", "execute", "script"]).default("text").describe("Kind of DAT: 'text' (plain), 'execute' (event hooks), or 'script' (table builder).")
3176
+ var createPythonScriptSchema = z24.object({
3177
+ parent_path: z24.string().describe("Parent COMP to create the DAT inside."),
3178
+ name: z24.string().optional(),
3179
+ code: z24.string().min(1).describe("Python source to place in the DAT."),
3180
+ dat_type: z24.enum(["text", "execute", "script"]).default("text").describe("Kind of DAT: 'text' (plain), 'execute' (event hooks), or 'script' (table builder).")
2222
3181
  });
2223
3182
  async function createPythonScriptImpl(ctx, args) {
2224
3183
  return guardTd(
@@ -2228,10 +3187,20 @@ async function createPythonScriptImpl(ctx, args) {
2228
3187
  type: TYPE_MAP[args.dat_type],
2229
3188
  name: args.name
2230
3189
  });
2231
- await ctx.client.executePythonScript(
2232
- `op(${JSON.stringify(dat.path)}).text = ${JSON.stringify(args.code)}`,
2233
- false
2234
- );
3190
+ const path = JSON.stringify(dat.path);
3191
+ const code = JSON.stringify(args.code);
3192
+ const script = args.dat_type === "script" ? [
3193
+ `_op = op(${path})`,
3194
+ "_cb = None",
3195
+ "try:",
3196
+ " _cb = _op.par.callbacks.eval()",
3197
+ "except Exception:",
3198
+ " _cb = None",
3199
+ "if _cb is None:",
3200
+ " _cb = _op.parent().op(_op.name + '_callbacks')",
3201
+ `_cb.text = ${code}`
3202
+ ].join("\n") : `op(${path}).text = ${code}`;
3203
+ await ctx.client.executePythonScript(script, false);
2235
3204
  return dat;
2236
3205
  },
2237
3206
  (dat) => jsonResult(`Created ${args.dat_type} DAT at ${dat.path}.`, { node: dat })
@@ -2251,12 +3220,12 @@ var registerCreatePythonScript = (server, ctx) => {
2251
3220
  };
2252
3221
 
2253
3222
  // src/tools/layer2/duplicateNetwork.ts
2254
- import { z as z21 } from "zod";
3223
+ import { z as z25 } from "zod";
2255
3224
  var q10 = (value) => JSON.stringify(value);
2256
- var duplicateNetworkSchema = z21.object({
2257
- source_path: z21.string().describe("Path of the node/COMP to duplicate."),
2258
- name: z21.string().optional().describe("Name for the copy (auto-generated if omitted)."),
2259
- parent_path: z21.string().optional().describe("Where to place the copy (defaults to the source's parent).")
3225
+ var duplicateNetworkSchema = z25.object({
3226
+ source_path: z25.string().describe("Path of the node/COMP to duplicate."),
3227
+ name: z25.string().optional().describe("Name for the copy (auto-generated if omitted)."),
3228
+ parent_path: z25.string().optional().describe("Where to place the copy (defaults to the source's parent).")
2260
3229
  });
2261
3230
  async function duplicateNetworkImpl(ctx, args) {
2262
3231
  const script = [
@@ -2288,14 +3257,236 @@ var registerDuplicateNetwork = (server, ctx) => {
2288
3257
  );
2289
3258
  };
2290
3259
 
3260
+ // src/tools/layer2/manageComponent.ts
3261
+ import { z as z26 } from "zod";
3262
+ var manageComponentSchema = z26.object({
3263
+ action: z26.enum(["save", "load"]).describe("save a COMP to a .tox file, or load a .tox into the project."),
3264
+ file_path: z26.string().describe("Absolute path to the .tox file (e.g. '/Users/me/components/widget.tox')."),
3265
+ comp_path: z26.string().optional().describe("(save) The COMP to save as a reusable .tox component."),
3266
+ parent_path: z26.string().default("/project1").describe("(load) COMP to place the loaded component inside."),
3267
+ linked: z26.boolean().default(false).describe(
3268
+ "(load) Create a live-linked instance (externaltox) that re-reads the file on change, instead of an independent copy."
3269
+ ),
3270
+ name: z26.string().optional().describe("(load, linked) Name for the linked COMP; defaults to the file name."),
3271
+ create_folders: z26.boolean().default(false).describe("(save) Create the parent folders if they do not exist.")
3272
+ });
3273
+ var COMPONENT_SCRIPT = `
3274
+ import json, base64, traceback, os
3275
+ _p = json.loads(base64.b64decode("__PAYLOAD_B64__").decode("utf-8"))
3276
+ report = {"action": _p["action"], "file_path": _p["file_path"], "warnings": []}
3277
+ try:
3278
+ _action = _p["action"]; _fp = _p["file_path"]
3279
+ if _action == "save":
3280
+ _c = op(_p.get("comp"))
3281
+ if _c is None:
3282
+ report["fatal"] = "COMP not found: " + str(_p.get("comp"))
3283
+ elif not _c.isCOMP:
3284
+ report["fatal"] = str(_p.get("comp")) + " is not a COMP, so it cannot be saved as a .tox."
3285
+ else:
3286
+ _saved = _c.save(_fp, createFolders=bool(_p.get("create_folders")))
3287
+ report["saved"] = str(_saved)
3288
+ report["size"] = os.path.getsize(_fp) if os.path.isfile(_fp) else None
3289
+ elif _action == "load":
3290
+ if not os.path.isfile(_fp):
3291
+ report["fatal"] = "File not found: " + _fp
3292
+ else:
3293
+ _parent = op(_p["parent"])
3294
+ if _parent is None:
3295
+ report["fatal"] = "Parent COMP not found: " + str(_p["parent"])
3296
+ elif _p.get("linked"):
3297
+ _stem = os.path.splitext(os.path.basename(_fp))[0]
3298
+ _new = _parent.create(baseCOMP, _p.get("name") or _stem)
3299
+ _new.par.externaltox = _fp
3300
+ try:
3301
+ _new.par.reinitnet.pulse()
3302
+ except Exception:
3303
+ pass
3304
+ report["loaded"] = _new.path; report["linked"] = True
3305
+ report["type"] = _new.type; report["children"] = sorted([c.name for c in _new.children])
3306
+ else:
3307
+ _new = _parent.loadTox(_fp)
3308
+ if _new is None:
3309
+ report["fatal"] = "loadTox produced no component from " + _fp
3310
+ else:
3311
+ report["loaded"] = _new.path; report["linked"] = False
3312
+ report["type"] = _new.type; report["children"] = sorted([c.name for c in _new.children])
3313
+ else:
3314
+ report["fatal"] = "Unknown action: " + str(_action)
3315
+ except Exception:
3316
+ report["fatal"] = traceback.format_exc().splitlines()[-1]
3317
+ print(json.dumps(report))
3318
+ `;
3319
+ function buildComponentScript(payload) {
3320
+ return buildPayloadScript(COMPONENT_SCRIPT, payload);
3321
+ }
3322
+ async function manageComponentImpl(ctx, args) {
3323
+ if (args.action === "save" && !args.comp_path) {
3324
+ return errorResult("A `comp_path` is required to save a component.");
3325
+ }
3326
+ return guardTd(
3327
+ async () => {
3328
+ const script = buildComponentScript({
3329
+ action: args.action,
3330
+ file_path: args.file_path,
3331
+ comp: args.comp_path ?? null,
3332
+ parent: args.parent_path,
3333
+ linked: args.linked,
3334
+ name: args.name ?? null,
3335
+ create_folders: args.create_folders
3336
+ });
3337
+ const exec = await ctx.client.executePythonScript(script, true);
3338
+ return parsePythonReport(exec.stdout);
3339
+ },
3340
+ (report) => {
3341
+ if (report.fatal) {
3342
+ return jsonResult(`Component ${report.action} failed: ${report.fatal}`, report);
3343
+ }
3344
+ const summary = report.action === "save" ? `Saved ${args.comp_path} to ${report.saved}${report.size != null ? ` (${report.size} bytes)` : ""}.` : `Loaded ${report.loaded}${report.linked ? " (live-linked)" : ""} from ${report.file_path}${report.children?.length ? ` \u2014 ${report.children.length} child node(s)` : ""}.`;
3345
+ return jsonResult(summary, report);
3346
+ }
3347
+ );
3348
+ }
3349
+ var registerManageComponent = (server, ctx) => {
3350
+ server.registerTool(
3351
+ "manage_component",
3352
+ {
3353
+ title: "Save / load component (.tox)",
3354
+ description: "Build a reusable component library: save any COMP as a .tox file, or load a .tox back into the project (as an independent copy, or a live-linked instance via `linked`). Paths are on the machine running TouchDesigner.",
3355
+ inputSchema: manageComponentSchema.shape,
3356
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
3357
+ },
3358
+ (args) => manageComponentImpl(ctx, args)
3359
+ );
3360
+ };
3361
+
3362
+ // src/tools/layer2/managePresets.ts
3363
+ import { z as z27 } from "zod";
3364
+ var managePresetsSchema = z27.object({
3365
+ action: z27.enum(["store", "recall", "list", "delete"]).describe("store a snapshot, recall one, list all, or delete one."),
3366
+ comp_path: z27.string().default("/project1").describe(
3367
+ "COMP whose parameter values the preset captures \u2014 usually a control-panel container."
3368
+ ),
3369
+ name: z27.string().optional().describe("Preset name (required for store/recall/delete)."),
3370
+ params: z27.array(z27.string()).optional().describe(
3371
+ "Specific custom-parameter names to capture/restore. Defaults to every custom parameter on the COMP."
3372
+ )
3373
+ });
3374
+ var PRESETS_SCRIPT = `
3375
+ import json, base64, traceback
3376
+ _p = json.loads(base64.b64decode("__PAYLOAD_B64__").decode("utf-8"))
3377
+ KEY = "tdmcp_presets"
3378
+ report = {"action": _p["action"], "comp": _p["comp"], "warnings": []}
3379
+ _c = op(_p["comp"])
3380
+ try:
3381
+ if _c is None:
3382
+ report["fatal"] = "COMP not found: " + _p["comp"]
3383
+ elif not hasattr(_c, "customPars"):
3384
+ report["fatal"] = _p["comp"] + " is not a COMP, so it has no custom parameters to snapshot."
3385
+ else:
3386
+ _store = dict(_c.fetch(KEY, {}))
3387
+ _action = _p["action"]; _name = _p.get("name")
3388
+ if _action == "list":
3389
+ report["presets"] = sorted(_store.keys())
3390
+ elif _action == "store":
3391
+ _wanted = _p.get("params") or None
3392
+ _pars = list(_c.customPars) if _wanted is None else [getattr(_c.par, n, None) for n in _wanted]
3393
+ _vals = {}
3394
+ for _pr in _pars:
3395
+ if _pr is None:
3396
+ continue
3397
+ _vals[_pr.name] = _pr.eval()
3398
+ _store[_name] = _vals; _c.store(KEY, _store)
3399
+ report["name"] = _name; report["captured"] = sorted(_vals.keys()); report["presets"] = sorted(_store.keys())
3400
+ elif _action == "recall":
3401
+ if _name not in _store:
3402
+ report["fatal"] = "Preset not found: '%s' (available: %s)" % (_name, ", ".join(sorted(_store.keys())) or "none")
3403
+ else:
3404
+ _restored = []
3405
+ for _nm, _v in _store[_name].items():
3406
+ _pr = getattr(_c.par, _nm, None)
3407
+ if _pr is None:
3408
+ report["warnings"].append("Parameter no longer exists: " + _nm); continue
3409
+ if _pr.readOnly:
3410
+ report["warnings"].append("Parameter is read-only: " + _nm); continue
3411
+ try:
3412
+ _pr.val = _v; _restored.append(_nm)
3413
+ except Exception:
3414
+ report["warnings"].append("Could not restore " + _nm)
3415
+ report["name"] = _name; report["restored"] = sorted(_restored)
3416
+ elif _action == "delete":
3417
+ if _name in _store:
3418
+ _store.pop(_name, None); _c.store(KEY, _store); report["deleted"] = _name
3419
+ else:
3420
+ report["warnings"].append("Preset not found: " + str(_name))
3421
+ report["presets"] = sorted(_store.keys())
3422
+ else:
3423
+ report["fatal"] = "Unknown action: " + str(_action)
3424
+ except Exception:
3425
+ report["fatal"] = traceback.format_exc().splitlines()[-1]
3426
+ print(json.dumps(report))
3427
+ `;
3428
+ function buildPresetsScript(payload) {
3429
+ return buildPayloadScript(PRESETS_SCRIPT, payload);
3430
+ }
3431
+ async function managePresetsImpl(ctx, args) {
3432
+ if (args.action !== "list" && !args.name) {
3433
+ return errorResult(`A preset name is required for the '${args.action}' action.`);
3434
+ }
3435
+ return guardTd(
3436
+ async () => {
3437
+ const script = buildPresetsScript({
3438
+ action: args.action,
3439
+ comp: args.comp_path,
3440
+ name: args.name,
3441
+ params: args.params ?? null
3442
+ });
3443
+ const exec = await ctx.client.executePythonScript(script, true);
3444
+ return parsePythonReport(exec.stdout);
3445
+ },
3446
+ (report) => {
3447
+ if (report.fatal) {
3448
+ return jsonResult(`Preset ${report.action} failed: ${report.fatal}`, report);
3449
+ }
3450
+ let summary;
3451
+ switch (report.action) {
3452
+ case "store":
3453
+ summary = `Stored preset "${report.name}" (${report.captured?.length ?? 0} parameter(s)) on ${report.comp}.`;
3454
+ break;
3455
+ case "recall":
3456
+ summary = `Recalled preset "${report.name}" (${report.restored?.length ?? 0} parameter(s) restored) on ${report.comp}.`;
3457
+ break;
3458
+ case "delete":
3459
+ summary = report.deleted ? `Deleted preset "${report.deleted}" on ${report.comp}.` : `No preset to delete on ${report.comp}.`;
3460
+ break;
3461
+ default:
3462
+ summary = `${report.presets?.length ?? 0} preset(s) on ${report.comp}: ${report.presets?.join(", ") || "none"}.`;
3463
+ }
3464
+ if (report.warnings.length) summary += ` ${report.warnings.length} warning(s).`;
3465
+ return jsonResult(summary, report);
3466
+ }
3467
+ );
3468
+ }
3469
+ var registerManagePresets = (server, ctx) => {
3470
+ server.registerTool(
3471
+ "manage_presets",
3472
+ {
3473
+ title: "Manage presets",
3474
+ description: "Store, recall, list, or delete named snapshots of a COMP's parameter values \u2014 the live-performance preset system. Pair with create_control_panel: snapshot the knob positions and jump between looks. Snapshots are saved in the COMP's storage so they persist with the project.",
3475
+ inputSchema: managePresetsSchema.shape,
3476
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
3477
+ },
3478
+ (args) => managePresetsImpl(ctx, args)
3479
+ );
3480
+ };
3481
+
2291
3482
  // src/tools/layer2/setParametersBatch.ts
2292
- import { z as z22 } from "zod";
2293
- var UpdateSchema = z22.object({
2294
- path: z22.string(),
2295
- parameters: z22.record(z22.string(), z22.unknown())
3483
+ import { z as z28 } from "zod";
3484
+ var UpdateSchema = z28.object({
3485
+ path: z28.string(),
3486
+ parameters: z28.record(z28.string(), z28.unknown())
2296
3487
  });
2297
- var setParametersBatchSchema = z22.object({
2298
- updates: z22.array(UpdateSchema).min(1).describe(
3488
+ var setParametersBatchSchema = z28.object({
3489
+ updates: z28.array(UpdateSchema).min(1).describe(
2299
3490
  "List of { path, parameters } updates sent in one batch request (per-update results; not transactional \u2014 a failed update does not roll back the others)."
2300
3491
  )
2301
3492
  });
@@ -2308,7 +3499,12 @@ async function setParametersBatchImpl(ctx, args) {
2308
3499
  parameters: update.parameters
2309
3500
  }))
2310
3501
  ),
2311
- (result) => jsonResult(`Applied ${args.updates.length} parameter update(s) in one batch.`, result)
3502
+ (result) => {
3503
+ const failed = result.results.filter((r) => !r.ok);
3504
+ const okCount = result.results.length - failed.length;
3505
+ const summary = failed.length ? `Applied ${okCount}/${result.results.length} parameter update(s); ${failed.length} failed (see results).` : `Applied ${okCount} parameter update(s) in one batch.`;
3506
+ return jsonResult(summary, result);
3507
+ }
2312
3508
  );
2313
3509
  }
2314
3510
  var registerSetParametersBatch = (server, ctx) => {
@@ -2332,26 +3528,32 @@ var layer2Registrars = [
2332
3528
  registerCreatePythonScript,
2333
3529
  registerSetParametersBatch,
2334
3530
  registerCreateContainer,
2335
- registerDuplicateNetwork
3531
+ registerCreateControlPanel,
3532
+ registerAnimateParameter,
3533
+ registerManagePresets,
3534
+ registerManageComponent,
3535
+ registerCreateExternalIo,
3536
+ registerDuplicateNetwork,
3537
+ registerArrangeNetwork
2336
3538
  ];
2337
3539
 
2338
3540
  // src/tools/layer3/compareTdNodes.ts
2339
- import { z as z23 } from "zod";
2340
- var compareTdNodesSchema = z23.object({
2341
- path_a: z23.string().describe("First node path."),
2342
- path_b: z23.string().describe("Second node path."),
2343
- only_diff: z23.boolean().default(true).describe("Return only the parameters that differ (true) or also list the identical ones.")
3541
+ import { z as z29 } from "zod";
3542
+ var compareTdNodesSchema = z29.object({
3543
+ path_a: z29.string().describe("First node path."),
3544
+ path_b: z29.string().describe("Second node path."),
3545
+ only_diff: z29.boolean().default(true).describe("Return only the parameters that differ (true) or also list the identical ones.")
2344
3546
  });
2345
- var compareTdNodesOutputSchema = z23.object({
2346
- a: z23.string(),
2347
- b: z23.string(),
2348
- type_a: z23.string(),
2349
- type_b: z23.string(),
2350
- type_match: z23.boolean(),
2351
- differing_count: z23.number(),
2352
- same_count: z23.number(),
2353
- differing: z23.array(z23.object({ param: z23.string(), a: z23.unknown(), b: z23.unknown() })),
2354
- identical: z23.array(z23.string()).optional()
3547
+ var compareTdNodesOutputSchema = z29.object({
3548
+ a: z29.string(),
3549
+ b: z29.string(),
3550
+ type_a: z29.string(),
3551
+ type_b: z29.string(),
3552
+ type_match: z29.boolean(),
3553
+ differing_count: z29.number(),
3554
+ same_count: z29.number(),
3555
+ differing: z29.array(z29.object({ param: z29.string(), a: z29.unknown(), b: z29.unknown() })),
3556
+ identical: z29.array(z29.string()).optional()
2355
3557
  });
2356
3558
  async function compareTdNodesImpl(ctx, args) {
2357
3559
  return guardTd(
@@ -2405,12 +3607,12 @@ var registerCompareTdNodes = (server, ctx) => {
2405
3607
  };
2406
3608
 
2407
3609
  // src/tools/layer3/createTdNode.ts
2408
- import { z as z24 } from "zod";
2409
- var createTdNodeSchema = z24.object({
2410
- parent_path: z24.string().default("/project1").describe("Parent COMP path to create the node inside."),
2411
- type: z24.string().describe("Operator type string, e.g. 'noiseTOP', 'feedbackTOP', 'nullTOP', 'constantCHOP'."),
2412
- name: z24.string().optional().describe("Optional node name (auto-generated if omitted)."),
2413
- parameters: z24.record(z24.string(), z24.unknown()).optional().describe("Optional initial parameter overrides as key\u2192value pairs.")
3610
+ import { z as z30 } from "zod";
3611
+ var createTdNodeSchema = z30.object({
3612
+ parent_path: z30.string().default("/project1").describe("Parent COMP path to create the node inside."),
3613
+ type: z30.string().describe("Operator type string, e.g. 'noiseTOP', 'feedbackTOP', 'nullTOP', 'constantCHOP'."),
3614
+ name: z30.string().optional().describe("Optional node name (auto-generated if omitted)."),
3615
+ parameters: z30.record(z30.string(), z30.unknown()).optional().describe("Optional initial parameter overrides as key\u2192value pairs.")
2414
3616
  });
2415
3617
  async function createTdNodeImpl(ctx, args) {
2416
3618
  const warnings = [];
@@ -2427,7 +3629,18 @@ async function createTdNodeImpl(ctx, args) {
2427
3629
  name: args.name,
2428
3630
  parameters: args.parameters
2429
3631
  }),
2430
- (node) => jsonResult(`Created ${node.type || args.type} at ${node.path}.`, { node, warnings })
3632
+ (node) => {
3633
+ const allWarnings = [...warnings];
3634
+ if (node.parameter_warnings?.length) {
3635
+ allWarnings.push(
3636
+ `These parameter(s) were not applied (unknown name or bad value): ${node.parameter_warnings.join(", ")}.`
3637
+ );
3638
+ }
3639
+ return jsonResult(`Created ${node.type || args.type} at ${node.path}.`, {
3640
+ node,
3641
+ warnings: allWarnings
3642
+ });
3643
+ }
2431
3644
  );
2432
3645
  }
2433
3646
  var registerCreateTdNode = (server, ctx) => {
@@ -2444,9 +3657,9 @@ var registerCreateTdNode = (server, ctx) => {
2444
3657
  };
2445
3658
 
2446
3659
  // src/tools/layer3/deleteTdNode.ts
2447
- import { z as z25 } from "zod";
2448
- var deleteTdNodeSchema = z25.object({
2449
- path: z25.string().describe("Full path of the node to delete, e.g. '/project1/noise1'.")
3660
+ import { z as z31 } from "zod";
3661
+ var deleteTdNodeSchema = z31.object({
3662
+ path: z31.string().describe("Full path of the node to delete, e.g. '/project1/noise1'.")
2450
3663
  });
2451
3664
  async function deleteTdNodeImpl(ctx, args) {
2452
3665
  return guardTd(
@@ -2468,12 +3681,12 @@ var registerDeleteTdNode = (server, ctx) => {
2468
3681
  };
2469
3682
 
2470
3683
  // src/tools/layer3/execNodeMethod.ts
2471
- import { z as z26 } from "zod";
2472
- var execNodeMethodSchema = z26.object({
2473
- path: z26.string().describe("Full path of the node to call the method on."),
2474
- method: z26.string().describe("Method name to call, e.g. 'cook', 'par', 'destroy', 'copy'."),
2475
- args: z26.array(z26.unknown()).default([]).describe("Positional arguments."),
2476
- kwargs: z26.record(z26.string(), z26.unknown()).default({}).describe("Keyword arguments.")
3684
+ import { z as z32 } from "zod";
3685
+ var execNodeMethodSchema = z32.object({
3686
+ path: z32.string().describe("Full path of the node to call the method on."),
3687
+ method: z32.string().describe("Method name to call, e.g. 'cook', 'par', 'destroy', 'copy'."),
3688
+ args: z32.array(z32.unknown()).default([]).describe("Positional arguments."),
3689
+ kwargs: z32.record(z32.string(), z32.unknown()).default({}).describe("Keyword arguments.")
2477
3690
  });
2478
3691
  async function execNodeMethodImpl(ctx, args) {
2479
3692
  return guardTd(
@@ -2496,12 +3709,12 @@ var registerExecNodeMethod = (server, ctx) => {
2496
3709
  };
2497
3710
 
2498
3711
  // src/tools/layer3/executePythonScript.ts
2499
- import { z as z27 } from "zod";
2500
- var executePythonScriptSchema = z27.object({
2501
- script: z27.string().min(1).describe(
3712
+ import { z as z33 } from "zod";
3713
+ var executePythonScriptSchema = z33.object({
3714
+ script: z33.string().min(1).describe(
2502
3715
  "Python source to execute inside TouchDesigner (runs in the TD process, not locally)."
2503
3716
  ),
2504
- return_output: z27.boolean().default(true).describe("Capture stdout / the value of the last expression and return it.")
3717
+ return_output: z33.boolean().default(true).describe("Capture stdout / the value of the last expression and return it.")
2505
3718
  });
2506
3719
  async function executePythonScriptImpl(ctx, args) {
2507
3720
  return guardTd(
@@ -2524,110 +3737,112 @@ var registerExecutePythonScript = (server, ctx) => {
2524
3737
  };
2525
3738
 
2526
3739
  // src/tools/layer3/findTdNodes.ts
2527
- import { z as z29 } from "zod";
3740
+ import { z as z35 } from "zod";
2528
3741
 
2529
3742
  // src/td-client/validators.ts
2530
- import { z as z28 } from "zod";
2531
- var ApiEnvelopeSchema = z28.object({
2532
- ok: z28.boolean(),
2533
- data: z28.unknown().optional(),
2534
- error: z28.object({ code: z28.string().optional(), message: z28.string() }).optional()
3743
+ import { z as z34 } from "zod";
3744
+ var ApiEnvelopeSchema = z34.object({
3745
+ ok: z34.boolean(),
3746
+ data: z34.unknown().optional(),
3747
+ error: z34.object({ code: z34.string().optional(), message: z34.string() }).optional()
2535
3748
  });
2536
- var NodeRefSchema = z28.object({
2537
- path: z28.string(),
2538
- type: z28.string().default(""),
2539
- name: z28.string().default("")
3749
+ var NodeRefSchema = z34.object({
3750
+ path: z34.string(),
3751
+ type: z34.string().default(""),
3752
+ name: z34.string().default(""),
3753
+ /** Parameters that could not be applied at create time (unknown name or bad value). */
3754
+ parameter_warnings: z34.array(z34.string()).optional()
2540
3755
  });
2541
3756
  var NodeDetailSchema = NodeRefSchema.extend({
2542
- parameters: z28.record(z28.string(), z28.unknown()).default({}),
2543
- inputs: z28.array(z28.string()).optional(),
2544
- outputs: z28.array(z28.string()).optional(),
2545
- family: z28.string().optional(),
2546
- errors: z28.array(z28.string()).optional()
3757
+ parameters: z34.record(z34.string(), z34.unknown()).default({}),
3758
+ inputs: z34.array(z34.string()).optional(),
3759
+ outputs: z34.array(z34.string()).optional(),
3760
+ family: z34.string().optional(),
3761
+ errors: z34.array(z34.string()).optional()
2547
3762
  });
2548
- var NodeListSchema = z28.object({ nodes: z28.array(NodeRefSchema).default([]) });
2549
- var InfoSchema = z28.object({
2550
- td_version: z28.string().optional(),
2551
- python_version: z28.string().optional(),
2552
- build: z28.string().optional(),
2553
- bridge_version: z28.string().optional(),
2554
- project: z28.string().optional()
3763
+ var NodeListSchema = z34.object({ nodes: z34.array(NodeRefSchema).default([]) });
3764
+ var InfoSchema = z34.object({
3765
+ td_version: z34.string().optional(),
3766
+ python_version: z34.string().optional(),
3767
+ build: z34.string().optional(),
3768
+ bridge_version: z34.string().optional(),
3769
+ project: z34.string().optional()
2555
3770
  });
2556
- var NodeErrorSchema = z28.object({
2557
- path: z28.string(),
2558
- message: z28.string(),
2559
- type: z28.string().optional()
3771
+ var NodeErrorSchema = z34.object({
3772
+ path: z34.string(),
3773
+ message: z34.string(),
3774
+ type: z34.string().optional()
2560
3775
  });
2561
- var NodeErrorsSchema = z28.object({ errors: z28.array(NodeErrorSchema).default([]) });
2562
- var PreviewSchema = z28.object({
2563
- path: z28.string(),
2564
- width: z28.number().int().positive(),
2565
- height: z28.number().int().positive(),
2566
- format: z28.string().default("png"),
2567
- base64: z28.string()
3776
+ var NodeErrorsSchema = z34.object({ errors: z34.array(NodeErrorSchema).default([]) });
3777
+ var PreviewSchema = z34.object({
3778
+ path: z34.string(),
3779
+ width: z34.number().int().positive(),
3780
+ height: z34.number().int().positive(),
3781
+ format: z34.string().default("png"),
3782
+ base64: z34.string()
2568
3783
  });
2569
- var ExecResultSchema = z28.object({
2570
- result: z28.unknown().optional(),
2571
- stdout: z28.string().optional(),
2572
- printed: z28.array(z28.string()).optional()
3784
+ var ExecResultSchema = z34.object({
3785
+ result: z34.unknown().optional(),
3786
+ stdout: z34.string().optional(),
3787
+ printed: z34.array(z34.string()).optional()
2573
3788
  });
2574
- var MethodResultSchema = z28.object({ result: z28.unknown() });
2575
- var DeleteResultSchema = z28.object({ deleted: z28.string() });
2576
- var ConnectionSchema = z28.object({
2577
- source_path: z28.string(),
2578
- source_output: z28.number().int().default(0),
2579
- target_path: z28.string(),
2580
- target_input: z28.number().int().default(0)
3789
+ var MethodResultSchema = z34.object({ result: z34.unknown() });
3790
+ var DeleteResultSchema = z34.object({ deleted: z34.string() });
3791
+ var ConnectionSchema = z34.object({
3792
+ source_path: z34.string(),
3793
+ source_output: z34.number().int().default(0),
3794
+ target_path: z34.string(),
3795
+ target_input: z34.number().int().default(0)
2581
3796
  });
2582
- var TopologySchema = z28.object({
2583
- nodes: z28.array(NodeRefSchema).default([]),
2584
- connections: z28.array(ConnectionSchema).default([])
3797
+ var TopologySchema = z34.object({
3798
+ nodes: z34.array(NodeRefSchema).default([]),
3799
+ connections: z34.array(ConnectionSchema).default([])
2585
3800
  });
2586
- var PerformanceSchema = z28.object({
2587
- nodes: z28.array(
2588
- z28.object({
2589
- path: z28.string(),
2590
- cook_time_ms: z28.number().default(0),
2591
- cook_count: z28.number().optional()
3801
+ var PerformanceSchema = z34.object({
3802
+ nodes: z34.array(
3803
+ z34.object({
3804
+ path: z34.string(),
3805
+ cook_time_ms: z34.number().default(0),
3806
+ cook_count: z34.number().optional()
2592
3807
  })
2593
3808
  ).default([]),
2594
- total_cook_time_ms: z28.number().optional(),
2595
- gpu_memory_mb: z28.number().optional()
3809
+ total_cook_time_ms: z34.number().optional(),
3810
+ gpu_memory_mb: z34.number().optional()
2596
3811
  });
2597
- var BatchOpResultSchema = z28.object({
2598
- action: z28.string(),
2599
- ok: z28.boolean(),
2600
- path: z28.string().optional(),
2601
- data: z28.unknown().optional(),
2602
- error: z28.string().optional()
3812
+ var BatchOpResultSchema = z34.object({
3813
+ action: z34.string(),
3814
+ ok: z34.boolean(),
3815
+ path: z34.string().optional(),
3816
+ data: z34.unknown().optional(),
3817
+ error: z34.string().optional()
2603
3818
  });
2604
- var BatchResultSchema = z28.object({ results: z28.array(BatchOpResultSchema).default([]) });
2605
- var CreateNodeInputSchema = z28.object({
2606
- parent_path: z28.string(),
2607
- type: z28.string(),
2608
- name: z28.string().optional(),
2609
- parameters: z28.record(z28.string(), z28.unknown()).optional()
3819
+ var BatchResultSchema = z34.object({ results: z34.array(BatchOpResultSchema).default([]) });
3820
+ var CreateNodeInputSchema = z34.object({
3821
+ parent_path: z34.string(),
3822
+ type: z34.string(),
3823
+ name: z34.string().optional(),
3824
+ parameters: z34.record(z34.string(), z34.unknown()).optional()
2610
3825
  });
2611
- var BatchOperationSchema = z28.discriminatedUnion("action", [
2612
- z28.object({
2613
- action: z28.literal("create"),
2614
- parent_path: z28.string(),
2615
- type: z28.string(),
2616
- name: z28.string().optional(),
2617
- parameters: z28.record(z28.string(), z28.unknown()).optional()
3826
+ var BatchOperationSchema = z34.discriminatedUnion("action", [
3827
+ z34.object({
3828
+ action: z34.literal("create"),
3829
+ parent_path: z34.string(),
3830
+ type: z34.string(),
3831
+ name: z34.string().optional(),
3832
+ parameters: z34.record(z34.string(), z34.unknown()).optional()
2618
3833
  }),
2619
- z28.object({
2620
- action: z28.literal("update"),
2621
- path: z28.string(),
2622
- parameters: z28.record(z28.string(), z28.unknown())
3834
+ z34.object({
3835
+ action: z34.literal("update"),
3836
+ path: z34.string(),
3837
+ parameters: z34.record(z34.string(), z34.unknown())
2623
3838
  }),
2624
- z28.object({ action: z28.literal("delete"), path: z28.string() }),
2625
- z28.object({
2626
- action: z28.literal("connect"),
2627
- source_path: z28.string(),
2628
- target_path: z28.string(),
2629
- source_output: z28.number().int().default(0),
2630
- target_input: z28.number().int().default(0)
3839
+ z34.object({ action: z34.literal("delete"), path: z34.string() }),
3840
+ z34.object({
3841
+ action: z34.literal("connect"),
3842
+ source_path: z34.string(),
3843
+ target_path: z34.string(),
3844
+ source_output: z34.number().int().default(0),
3845
+ target_input: z34.number().int().default(0)
2631
3846
  })
2632
3847
  ]);
2633
3848
 
@@ -2636,27 +3851,27 @@ function globToRegExp(pattern) {
2636
3851
  const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
2637
3852
  return new RegExp(escaped, "i");
2638
3853
  }
2639
- function parentOf(path) {
3854
+ function parentOf2(path) {
2640
3855
  const idx = path.lastIndexOf("/");
2641
3856
  return idx <= 0 ? "/" : path.slice(0, idx);
2642
3857
  }
2643
3858
 
2644
3859
  // src/tools/layer3/findTdNodes.ts
2645
- var findTdNodesSchema = z29.object({
2646
- parent_path: z29.string().default("/project1").describe("Where to search from."),
2647
- pattern: z29.string().optional().describe("Case-insensitive name/path filter with '*' wildcards (e.g. 'text*', '*noise*')."),
2648
- type: z29.string().optional().describe("Case-insensitive operator-type substring (e.g. 'TOP', 'noise')."),
2649
- recursive: z29.boolean().default(true).describe("Search the whole sub-network (true) or only direct children (false)."),
2650
- path_only: z29.boolean().default(false).describe("Return only matching paths."),
2651
- limit: z29.number().int().positive().default(50).describe("Max matches to return.")
3860
+ var findTdNodesSchema = z35.object({
3861
+ parent_path: z35.string().default("/project1").describe("Where to search from."),
3862
+ pattern: z35.string().optional().describe("Case-insensitive name/path filter with '*' wildcards (e.g. 'text*', '*noise*')."),
3863
+ type: z35.string().optional().describe("Case-insensitive operator-type substring (e.g. 'TOP', 'noise')."),
3864
+ recursive: z35.boolean().default(true).describe("Search the whole sub-network (true) or only direct children (false)."),
3865
+ path_only: z35.boolean().default(false).describe("Return only matching paths."),
3866
+ limit: z35.number().int().positive().default(50).describe("Max matches to return.")
2652
3867
  });
2653
- var findTdNodesOutputSchema = z29.object({
2654
- parent_path: z29.string(),
2655
- recursive: z29.boolean(),
2656
- count: z29.number(),
2657
- truncated: z29.boolean(),
2658
- paths: z29.array(z29.string()).optional(),
2659
- matches: z29.array(NodeRefSchema).optional()
3868
+ var findTdNodesOutputSchema = z35.object({
3869
+ parent_path: z35.string(),
3870
+ recursive: z35.boolean(),
3871
+ count: z35.number(),
3872
+ truncated: z35.boolean(),
3873
+ paths: z35.array(z35.string()).optional(),
3874
+ matches: z35.array(NodeRefSchema).optional()
2660
3875
  });
2661
3876
  async function findTdNodesImpl(ctx, args) {
2662
3877
  const fetch2 = args.recursive ? async () => (await ctx.client.getNetworkTopology(args.parent_path, true)).nodes : async () => (await ctx.client.getNodes(args.parent_path)).nodes;
@@ -2693,9 +3908,9 @@ var registerFindTdNodes = (server, ctx) => {
2693
3908
  };
2694
3909
 
2695
3910
  // src/tools/layer3/getModuleHelp.ts
2696
- import { z as z30 } from "zod";
2697
- var getModuleHelpSchema = z30.object({
2698
- name: z30.string().describe("Class or module name to get help for, e.g. 'OP', 'App', 'Project'.")
3911
+ import { z as z36 } from "zod";
3912
+ var getModuleHelpSchema = z36.object({
3913
+ name: z36.string().describe("Class or module name to get help for, e.g. 'OP', 'App', 'Project'.")
2699
3914
  });
2700
3915
  function getModuleHelpImpl(ctx, args) {
2701
3916
  const cls = ctx.knowledge.getPythonClass(args.name);
@@ -2736,9 +3951,9 @@ var registerGetModuleHelp = (server, ctx) => {
2736
3951
  };
2737
3952
 
2738
3953
  // src/tools/layer3/getTdClassDetails.ts
2739
- import { z as z31 } from "zod";
2740
- var getTdClassDetailsSchema = z31.object({
2741
- class_name: z31.string().describe("Python class name, e.g. 'OP', 'TOP', 'App', 'CHOP'.")
3954
+ import { z as z37 } from "zod";
3955
+ var getTdClassDetailsSchema = z37.object({
3956
+ class_name: z37.string().describe("Python class name, e.g. 'OP', 'TOP', 'App', 'CHOP'.")
2742
3957
  });
2743
3958
  function getTdClassDetailsImpl(ctx, args) {
2744
3959
  const cls = ctx.knowledge.getPythonClass(args.class_name);
@@ -2768,9 +3983,9 @@ var registerGetTdClassDetails = (server, ctx) => {
2768
3983
  };
2769
3984
 
2770
3985
  // src/tools/layer3/getTdClasses.ts
2771
- import { z as z32 } from "zod";
2772
- var getTdClassesSchema = z32.object({
2773
- filter: z32.string().optional().describe("Optional case-insensitive substring to filter class names by.")
3986
+ import { z as z38 } from "zod";
3987
+ var getTdClassesSchema = z38.object({
3988
+ filter: z38.string().optional().describe("Optional case-insensitive substring to filter class names by.")
2774
3989
  });
2775
3990
  function getTdClassesImpl(ctx, args) {
2776
3991
  let classes = ctx.knowledge.listPythonClasses();
@@ -2831,17 +4046,17 @@ var registerGetTdInfo = (server, ctx) => {
2831
4046
  };
2832
4047
 
2833
4048
  // src/tools/layer3/getTdNodeErrors.ts
2834
- import { z as z33 } from "zod";
2835
- var getTdNodeErrorsSchema = z33.object({
2836
- path: z33.string().describe("Full path of the node (or network root) to check for errors."),
2837
- recursive: z33.boolean().default(false).describe("If true, check the whole network under `path`; otherwise just that node."),
2838
- summary: z33.boolean().default(false).describe("Return only counts grouped by error type instead of the full error list.")
4049
+ import { z as z39 } from "zod";
4050
+ var getTdNodeErrorsSchema = z39.object({
4051
+ path: z39.string().describe("Full path of the node (or network root) to check for errors."),
4052
+ recursive: z39.boolean().default(false).describe("If true, check the whole network under `path`; otherwise just that node."),
4053
+ summary: z39.boolean().default(false).describe("Return only counts grouped by error type instead of the full error list.")
2839
4054
  });
2840
- var getTdNodeErrorsOutputSchema = z33.object({
2841
- path: z33.string(),
2842
- total: z33.number(),
2843
- errors: z33.array(NodeErrorSchema).optional(),
2844
- by_type: z33.record(z33.string(), z33.number()).optional()
4055
+ var getTdNodeErrorsOutputSchema = z39.object({
4056
+ path: z39.string(),
4057
+ total: z39.number(),
4058
+ errors: z39.array(NodeErrorSchema).optional(),
4059
+ by_type: z39.record(z39.string(), z39.number()).optional()
2845
4060
  });
2846
4061
  async function getTdNodeErrorsImpl(ctx, args) {
2847
4062
  return guardTd(
@@ -2883,11 +4098,11 @@ var registerGetTdNodeErrors = (server, ctx) => {
2883
4098
  };
2884
4099
 
2885
4100
  // src/tools/layer3/getTdNodeParameters.ts
2886
- import { z as z34 } from "zod";
2887
- var getTdNodeParametersSchema = z34.object({
2888
- path: z34.string().describe("Full path of the node to inspect."),
2889
- keys: z34.array(z34.string()).optional().describe("Only return these parameter names (case-sensitive). Omit to return all parameters."),
2890
- omit_io: z34.boolean().default(false).describe("Drop the inputs/outputs lists from the result to save context.")
4101
+ import { z as z40 } from "zod";
4102
+ var getTdNodeParametersSchema = z40.object({
4103
+ path: z40.string().describe("Full path of the node to inspect."),
4104
+ keys: z40.array(z40.string()).optional().describe("Only return these parameter names (case-sensitive). Omit to return all parameters."),
4105
+ omit_io: z40.boolean().default(false).describe("Drop the inputs/outputs lists from the result to save context.")
2891
4106
  });
2892
4107
  async function getTdNodeParametersImpl(ctx, args) {
2893
4108
  return guardTd(
@@ -2930,29 +4145,29 @@ var registerGetTdNodeParameters = (server, ctx) => {
2930
4145
  };
2931
4146
 
2932
4147
  // src/tools/layer3/getTdNodes.ts
2933
- import { z as z35 } from "zod";
2934
- var getTdNodesSchema = z35.object({
2935
- parent_path: z35.string().default("/project1").describe("Parent COMP whose direct children should be listed."),
2936
- pattern: z35.string().optional().describe(
4148
+ import { z as z41 } from "zod";
4149
+ var getTdNodesSchema = z41.object({
4150
+ parent_path: z41.string().default("/project1").describe("Parent COMP whose direct children should be listed."),
4151
+ pattern: z41.string().optional().describe(
2937
4152
  "Case-insensitive filter on node name/path. Supports '*' wildcards (e.g. 'text*', '*noise*')."
2938
4153
  ),
2939
- path_only: z35.boolean().default(false).describe("Return only the list of node paths, dropping type/name."),
2940
- limit: z35.number().int().positive().optional().describe("Cap the number of nodes returned."),
2941
- detail_level: z35.enum(["summary", "full"]).default("summary").describe(
4154
+ path_only: z41.boolean().default(false).describe("Return only the list of node paths, dropping type/name."),
4155
+ limit: z41.number().int().positive().optional().describe("Cap the number of nodes returned."),
4156
+ detail_level: z41.enum(["summary", "full"]).default("summary").describe(
2942
4157
  "'summary' (default) returns a count, a type breakdown and the first few paths; 'full' returns every node. Use 'full' (or path_only) when you need the complete list."
2943
4158
  )
2944
4159
  });
2945
4160
  var SAMPLE_SIZE = 10;
2946
- var getTdNodesOutputSchema = z35.object({
2947
- parent_path: z35.string(),
2948
- count: z35.number(),
2949
- detail_level: z35.enum(["summary", "full"]),
2950
- truncated: z35.boolean(),
2951
- by_type: z35.record(z35.string(), z35.number()).optional(),
2952
- sample: z35.array(z35.string()).optional(),
2953
- paths: z35.array(z35.string()).optional(),
2954
- nodes: z35.array(NodeRefSchema).optional(),
2955
- hint: z35.string().optional()
4161
+ var getTdNodesOutputSchema = z41.object({
4162
+ parent_path: z41.string(),
4163
+ count: z41.number(),
4164
+ detail_level: z41.enum(["summary", "full"]),
4165
+ truncated: z41.boolean(),
4166
+ by_type: z41.record(z41.string(), z41.number()).optional(),
4167
+ sample: z41.array(z41.string()).optional(),
4168
+ paths: z41.array(z41.string()).optional(),
4169
+ nodes: z41.array(NodeRefSchema).optional(),
4170
+ hint: z41.string().optional()
2956
4171
  });
2957
4172
  async function getTdNodesImpl(ctx, args) {
2958
4173
  return guardTd(
@@ -3020,11 +4235,11 @@ var registerGetTdNodes = (server, ctx) => {
3020
4235
  };
3021
4236
 
3022
4237
  // src/tools/layer3/getTdPerformance.ts
3023
- import { z as z36 } from "zod";
4238
+ import { z as z42 } from "zod";
3024
4239
 
3025
4240
  // src/feedback/performanceMonitor.ts
3026
- async function checkPerformance(client, path, targetFps = 60) {
3027
- const perf = await client.getNetworkPerformance(path);
4241
+ async function checkPerformance(client, path, targetFps = 60, recursive = true) {
4242
+ const perf = await client.getNetworkPerformance(path, recursive);
3028
4243
  const frameBudgetMs = 1e3 / targetFps;
3029
4244
  const warnings = [];
3030
4245
  for (const node of perf.nodes) {
@@ -3040,31 +4255,35 @@ async function checkPerformance(client, path, targetFps = 60) {
3040
4255
  `Total cook time ${totalCookMs.toFixed(2)}ms exceeds the ${frameBudgetMs.toFixed(2)}ms budget at ${targetFps}fps.`
3041
4256
  );
3042
4257
  }
3043
- return { path, targetFps, frameBudgetMs, totalCookMs, nodes: perf.nodes, warnings };
4258
+ const nodes = [...perf.nodes].sort((a, b) => b.cook_time_ms - a.cook_time_ms);
4259
+ return { path, targetFps, frameBudgetMs, totalCookMs, nodes, warnings };
3044
4260
  }
3045
4261
 
3046
4262
  // src/tools/layer3/getTdPerformance.ts
3047
- var getTdPerformanceSchema = z36.object({
3048
- root_path: z36.string().default("/project1").describe("Network root to measure cook times under."),
3049
- target_fps: z36.number().positive().default(60).describe("Frame-rate target used to flag slow nodes.")
4263
+ var getTdPerformanceSchema = z42.object({
4264
+ root_path: z42.string().default("/project1").describe("Network root to measure cook times under."),
4265
+ target_fps: z42.number().positive().default(60).describe("Frame-rate target used to flag slow nodes."),
4266
+ recursive: z42.boolean().default(true).describe(
4267
+ "Measure every descendant (true, default) so cook time inside generated containers is counted, not just the root's direct children."
4268
+ )
3050
4269
  });
3051
- var getTdPerformanceOutputSchema = z36.object({
3052
- path: z36.string(),
3053
- targetFps: z36.number(),
3054
- frameBudgetMs: z36.number(),
3055
- totalCookMs: z36.number(),
3056
- nodes: z36.array(
3057
- z36.object({
3058
- path: z36.string(),
3059
- cook_time_ms: z36.number(),
3060
- cook_count: z36.number().optional()
4270
+ var getTdPerformanceOutputSchema = z42.object({
4271
+ path: z42.string(),
4272
+ targetFps: z42.number(),
4273
+ frameBudgetMs: z42.number(),
4274
+ totalCookMs: z42.number(),
4275
+ nodes: z42.array(
4276
+ z42.object({
4277
+ path: z42.string(),
4278
+ cook_time_ms: z42.number(),
4279
+ cook_count: z42.number().optional()
3061
4280
  })
3062
4281
  ),
3063
- warnings: z36.array(z36.string())
4282
+ warnings: z42.array(z42.string())
3064
4283
  });
3065
4284
  async function getTdPerformanceImpl(ctx, args) {
3066
4285
  return guardTd(
3067
- () => checkPerformance(ctx.client, args.root_path, args.target_fps),
4286
+ () => checkPerformance(ctx.client, args.root_path, args.target_fps, args.recursive),
3068
4287
  (report) => structuredResult(
3069
4288
  report.warnings.length === 0 ? `Within budget: ${report.totalCookMs.toFixed(2)}ms total under ${args.root_path} (${args.target_fps}fps).` : `${report.warnings.length} performance warning(s) under ${args.root_path}.`,
3070
4289
  report
@@ -3076,7 +4295,7 @@ var registerGetTdPerformance = (server, ctx) => {
3076
4295
  "get_td_performance",
3077
4296
  {
3078
4297
  title: "Get network performance",
3079
- description: "Report cook times under a network and warn about nodes that exceed the frame budget.",
4298
+ description: "Report cook times under a network (recursively by default, slowest node first) and warn about nodes that exceed the frame budget.",
3080
4299
  inputSchema: getTdPerformanceSchema.shape,
3081
4300
  outputSchema: getTdPerformanceOutputSchema.shape,
3082
4301
  annotations: { readOnlyHint: true, openWorldHint: true }
@@ -3086,7 +4305,7 @@ var registerGetTdPerformance = (server, ctx) => {
3086
4305
  };
3087
4306
 
3088
4307
  // src/tools/layer3/getTdTopology.ts
3089
- import { z as z37 } from "zod";
4308
+ import { z as z43 } from "zod";
3090
4309
 
3091
4310
  // src/feedback/networkVerifier.ts
3092
4311
  async function verifyNetwork(client, path) {
@@ -3107,14 +4326,14 @@ async function verifyNetwork(client, path) {
3107
4326
  }
3108
4327
 
3109
4328
  // src/tools/layer3/getTdTopology.ts
3110
- var getTdTopologySchema = z37.object({
3111
- root_path: z37.string().default("/project1").describe("Network root to map.")
4329
+ var getTdTopologySchema = z43.object({
4330
+ root_path: z43.string().default("/project1").describe("Network root to map.")
3112
4331
  });
3113
- var getTdTopologyOutputSchema = z37.object({
3114
- path: z37.string(),
3115
- nodeCount: z37.number(),
3116
- connectionCount: z37.number(),
3117
- issues: z37.array(z37.string()),
4332
+ var getTdTopologyOutputSchema = z43.object({
4333
+ path: z43.string(),
4334
+ nodeCount: z43.number(),
4335
+ connectionCount: z43.number(),
4336
+ issues: z43.array(z43.string()),
3118
4337
  topology: TopologySchema
3119
4338
  });
3120
4339
  async function getTdTopologyImpl(ctx, args) {
@@ -3141,27 +4360,27 @@ var registerGetTdTopology = (server, ctx) => {
3141
4360
  };
3142
4361
 
3143
4362
  // src/tools/layer3/snapshotTdGraph.ts
3144
- import { z as z38 } from "zod";
4363
+ import { z as z44 } from "zod";
3145
4364
  var MAX_PARAM_NODES = 60;
3146
- var snapshotTdGraphSchema = z38.object({
3147
- path: z38.string().default("/project1").describe("Network root to snapshot."),
3148
- include_params: z38.boolean().default(false).describe("Also fetch each node's parameters (one request per node; capped for large graphs).")
4365
+ var snapshotTdGraphSchema = z44.object({
4366
+ path: z44.string().default("/project1").describe("Network root to snapshot."),
4367
+ include_params: z44.boolean().default(false).describe("Also fetch each node's parameters (one request per node; capped for large graphs).")
3149
4368
  });
3150
- var snapshotTdGraphOutputSchema = z38.object({
3151
- path: z38.string(),
3152
- nodeCount: z38.number(),
3153
- connectionCount: z38.number(),
3154
- issues: z38.array(z38.string()),
3155
- params_truncated: z38.boolean(),
3156
- nodes: z38.array(
3157
- z38.object({
3158
- path: z38.string(),
3159
- type: z38.string(),
3160
- name: z38.string(),
3161
- parameters: z38.record(z38.string(), z38.unknown()).optional()
4369
+ var snapshotTdGraphOutputSchema = z44.object({
4370
+ path: z44.string(),
4371
+ nodeCount: z44.number(),
4372
+ connectionCount: z44.number(),
4373
+ issues: z44.array(z44.string()),
4374
+ params_truncated: z44.boolean(),
4375
+ nodes: z44.array(
4376
+ z44.object({
4377
+ path: z44.string(),
4378
+ type: z44.string(),
4379
+ name: z44.string(),
4380
+ parameters: z44.record(z44.string(), z44.unknown()).optional()
3162
4381
  })
3163
4382
  ),
3164
- connections: z38.array(ConnectionSchema)
4383
+ connections: z44.array(ConnectionSchema)
3165
4384
  });
3166
4385
  async function snapshotTdGraphImpl(ctx, args) {
3167
4386
  return guardTd(
@@ -3215,25 +4434,25 @@ var registerSnapshotTdGraph = (server, ctx) => {
3215
4434
  };
3216
4435
 
3217
4436
  // src/tools/layer3/summarizeTdErrors.ts
3218
- import { z as z39 } from "zod";
3219
- var summarizeTdErrorsSchema = z39.object({
3220
- path: z39.string().default("/project1").describe("Network root to collect errors under."),
3221
- group_by: z39.enum(["message", "type", "parent"]).default("message").describe(
4437
+ import { z as z45 } from "zod";
4438
+ var summarizeTdErrorsSchema = z45.object({
4439
+ path: z45.string().default("/project1").describe("Network root to collect errors under."),
4440
+ group_by: z45.enum(["message", "type", "parent"]).default("message").describe(
3222
4441
  "How to cluster errors: by exact message, by error type, or by parent container (to find a common upstream cause)."
3223
4442
  )
3224
4443
  });
3225
- var summarizeTdErrorsOutputSchema = z39.object({
3226
- path: z39.string(),
3227
- total: z39.number(),
3228
- group_by: z39.enum(["message", "type", "parent"]),
3229
- groups: z39.array(
3230
- z39.object({
3231
- key: z39.string(),
3232
- count: z39.number(),
3233
- sample: z39.object({ path: z39.string(), message: z39.string() })
4444
+ var summarizeTdErrorsOutputSchema = z45.object({
4445
+ path: z45.string(),
4446
+ total: z45.number(),
4447
+ group_by: z45.enum(["message", "type", "parent"]),
4448
+ groups: z45.array(
4449
+ z45.object({
4450
+ key: z45.string(),
4451
+ count: z45.number(),
4452
+ sample: z45.object({ path: z45.string(), message: z45.string() })
3234
4453
  })
3235
4454
  ),
3236
- suggestions: z39.array(z39.string())
4455
+ suggestions: z45.array(z45.string())
3237
4456
  });
3238
4457
  async function summarizeTdErrorsImpl(ctx, args) {
3239
4458
  return guardTd(
@@ -3250,7 +4469,7 @@ async function summarizeTdErrorsImpl(ctx, args) {
3250
4469
  suggestions: []
3251
4470
  });
3252
4471
  }
3253
- const keyOf = (e) => args.group_by === "message" ? e.message : args.group_by === "type" ? e.type || "error" : parentOf(e.path);
4472
+ const keyOf = (e) => args.group_by === "message" ? e.message : args.group_by === "type" ? e.type || "error" : parentOf2(e.path);
3254
4473
  const grouped = /* @__PURE__ */ new Map();
3255
4474
  const byPath = /* @__PURE__ */ new Map();
3256
4475
  for (const e of errors) {
@@ -3293,10 +4512,10 @@ var registerSummarizeTdErrors = (server, ctx) => {
3293
4512
  };
3294
4513
 
3295
4514
  // src/tools/layer3/updateTdNodeParameters.ts
3296
- import { z as z40 } from "zod";
3297
- var updateTdNodeParametersSchema = z40.object({
3298
- path: z40.string().describe("Full path of the node whose parameters to update."),
3299
- parameters: z40.record(z40.string(), z40.unknown()).describe("Parameter overrides as key\u2192value pairs, e.g. { period: 4, amplitude: 0.5 }.")
4515
+ import { z as z46 } from "zod";
4516
+ var updateTdNodeParametersSchema = z46.object({
4517
+ path: z46.string().describe("Full path of the node whose parameters to update."),
4518
+ parameters: z46.record(z46.string(), z46.unknown()).describe("Parameter overrides as key\u2192value pairs, e.g. { period: 4, amplitude: 0.5 }.")
3300
4519
  });
3301
4520
  async function updateTdNodeParametersImpl(ctx, args) {
3302
4521
  return guardTd(
@@ -3359,7 +4578,7 @@ function getVersion() {
3359
4578
  try {
3360
4579
  const raw = readFileSync(resolve2(dir, "package.json"), "utf8");
3361
4580
  const pkg = JSON.parse(raw);
3362
- if (pkg.name === "tdmcp" && pkg.version) {
4581
+ if (pkg.version && (pkg.name === "@dpantani/tdmcp" || pkg.name === "tdmcp")) {
3363
4582
  cached = pkg.version;
3364
4583
  return cached;
3365
4584
  }
@@ -3730,61 +4949,68 @@ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as
3730
4949
  import { join as join3 } from "path";
3731
4950
 
3732
4951
  // src/recipes/schema.ts
3733
- import { z as z41 } from "zod";
3734
- var RecipeNodeSchema = z41.object({
3735
- name: z41.string().describe("Unique node name within the recipe (used for wiring)."),
3736
- type: z41.string().describe("Operator type, e.g. 'noiseTOP'."),
3737
- parameters: z41.record(z41.string(), z41.unknown()).default({}),
3738
- parent: z41.string().optional().describe(
4952
+ import { z as z47 } from "zod";
4953
+ var RecipeNodeSchema = z47.object({
4954
+ name: z47.string().describe("Unique node name within the recipe (used for wiring)."),
4955
+ type: z47.string().describe("Operator type, e.g. 'noiseTOP'."),
4956
+ parameters: z47.record(z47.string(), z47.unknown()).default({}),
4957
+ parent: z47.string().optional().describe(
3739
4958
  "Name of another recipe node (a COMP, e.g. a geometryCOMP) to nest this node inside of. The parent must appear earlier in `nodes`. Used to place SOPs inside a Geometry COMP."
3740
4959
  ),
3741
- render: z41.boolean().optional().describe(
4960
+ render: z47.boolean().optional().describe(
3742
4961
  "For a SOP nested in a geometryCOMP: make this the rendered geometry. Sets the render/display flags on it and clears its siblings, so the COMP renders this instead of its default torus."
3743
4962
  ),
3744
- comment: z41.string().optional()
4963
+ comment: z47.string().optional()
3745
4964
  });
3746
- var RecipeConnectionSchema = z41.object({
3747
- from: z41.string().describe("Source node name."),
3748
- to: z41.string().describe("Target node name."),
3749
- from_output: z41.number().int().nonnegative().default(0),
3750
- to_input: z41.number().int().nonnegative().default(0)
4965
+ var RecipeConnectionSchema = z47.object({
4966
+ from: z47.string().describe("Source node name."),
4967
+ to: z47.string().describe("Target node name."),
4968
+ from_output: z47.number().int().nonnegative().default(0),
4969
+ to_input: z47.number().int().nonnegative().default(0)
3751
4970
  });
3752
- var RecipeParameterSchema = z41.object({
3753
- name: z41.string().describe("Friendly name of the exposed control."),
3754
- node: z41.string().describe("Recipe node name the parameter belongs to."),
3755
- param: z41.string().describe("TD parameter name on that node."),
3756
- value: z41.unknown().optional(),
3757
- label: z41.string().optional(),
3758
- min: z41.number().optional(),
3759
- max: z41.number().optional(),
3760
- description: z41.string().optional()
4971
+ var RecipeParameterSchema = z47.object({
4972
+ name: z47.string().describe("Friendly name of the exposed control."),
4973
+ node: z47.string().describe("Recipe node name the parameter belongs to."),
4974
+ param: z47.string().describe("TD parameter name on that node."),
4975
+ value: z47.unknown().optional(),
4976
+ label: z47.string().optional(),
4977
+ min: z47.number().optional(),
4978
+ max: z47.number().optional(),
4979
+ description: z47.string().optional()
3761
4980
  });
3762
- var RecipeGlslUniformSchema = z41.object({
3763
- node: z41.string().describe("Recipe node name of the GLSL TOP that declares the uniform."),
3764
- name: z41.string().describe("Uniform name as referenced in the shader, e.g. 'uFeed'."),
3765
- kind: z41.enum(["float", "vec", "color"]).default("float").describe(
4981
+ var RecipeGlslUniformSchema = z47.object({
4982
+ node: z47.string().describe("Recipe node name of the GLSL TOP that declares the uniform."),
4983
+ name: z47.string().describe("Uniform name as referenced in the shader, e.g. 'uFeed'."),
4984
+ kind: z47.enum(["float", "vec", "color"]).default("float").describe(
3766
4985
  "Uniform kind: float (uniform float), vec (uniform vec2/3/4), color (rgba). float/vec use the Vectors page; color uses the Colors page."
3767
4986
  ),
3768
- value: z41.union([z41.number(), z41.array(z41.number())]).optional().describe("Initial value: a number for float, or an array of components for vec/color."),
3769
- label: z41.string().optional(),
3770
- min: z41.number().optional(),
3771
- max: z41.number().optional(),
3772
- description: z41.string().optional()
4987
+ value: z47.union([z47.number(), z47.array(z47.number())]).optional().describe("Initial value: a number for float, or an array of components for vec/color."),
4988
+ label: z47.string().optional(),
4989
+ min: z47.number().optional(),
4990
+ max: z47.number().optional(),
4991
+ description: z47.string().optional()
3773
4992
  });
3774
- var RecipeSchema = z41.object({
3775
- id: z41.string(),
3776
- name: z41.string(),
3777
- description: z41.string().default(""),
3778
- tags: z41.array(z41.string()).default([]),
3779
- difficulty: z41.enum(["beginner", "intermediate", "advanced"]).default("intermediate"),
3780
- td_version_min: z41.string().default("2023"),
3781
- nodes: z41.array(RecipeNodeSchema).min(1),
3782
- connections: z41.array(RecipeConnectionSchema).default([]),
3783
- parameters: z41.array(RecipeParameterSchema).default([]),
3784
- glsl_uniforms: z41.array(RecipeGlslUniformSchema).default([]),
3785
- glsl_code: z41.record(z41.string(), z41.string()).optional(),
3786
- python_code: z41.record(z41.string(), z41.string()).optional(),
3787
- preview_description: z41.string().default("")
4993
+ var RecipeSchema = z47.object({
4994
+ id: z47.string(),
4995
+ name: z47.string(),
4996
+ description: z47.string().default(""),
4997
+ tags: z47.array(z47.string()).default([]),
4998
+ difficulty: z47.enum(["beginner", "intermediate", "advanced"]).default("intermediate"),
4999
+ td_version_min: z47.string().default("2023"),
5000
+ nodes: z47.array(RecipeNodeSchema).min(1),
5001
+ connections: z47.array(RecipeConnectionSchema).default([]),
5002
+ parameters: z47.array(RecipeParameterSchema).default([]),
5003
+ glsl_uniforms: z47.array(RecipeGlslUniformSchema).default([]),
5004
+ glsl_code: z47.record(z47.string(), z47.string()).optional(),
5005
+ python_code: z47.record(z47.string(), z47.string()).optional(),
5006
+ /**
5007
+ * Live controls to auto-expose on the system container: custom parameters (knobs/
5008
+ * sliders/toggles) bound to node parameters so the built system is immediately
5009
+ * playable. Each control's `bind_to` uses recipe node *names* ("nodeName.parName");
5010
+ * buildFromRecipe rewrites them to the real created paths.
5011
+ */
5012
+ controls: z47.array(controlSchema).default([]),
5013
+ preview_description: z47.string().default("")
3788
5014
  });
3789
5015
 
3790
5016
  // src/recipes/loader.ts
@@ -4007,40 +5233,46 @@ var TouchDesignerClient = class {
4007
5233
  recursive ? { recursive: true } : void 0
4008
5234
  );
4009
5235
  }
4010
- getNetworkPerformance(path) {
4011
- return this.request("GET", `/api/network/${segment(path)}/performance`, PerformanceSchema);
5236
+ getNetworkPerformance(path, recursive = false) {
5237
+ return this.request(
5238
+ "GET",
5239
+ `/api/network/${segment(path)}/performance`,
5240
+ PerformanceSchema,
5241
+ void 0,
5242
+ recursive ? { recursive: true } : void 0
5243
+ );
4012
5244
  }
4013
5245
  };
4014
5246
 
4015
5247
  // src/utils/config.ts
4016
- import { z as z42 } from "zod";
4017
- var ConfigSchema = z42.object({
5248
+ import { z as z48 } from "zod";
5249
+ var ConfigSchema = z48.object({
4018
5250
  /** TouchDesigner bridge host. */
4019
- tdHost: z42.string().min(1).default("127.0.0.1"),
5251
+ tdHost: z48.string().min(1).default("127.0.0.1"),
4020
5252
  /** TouchDesigner bridge port (WebServer DAT). */
4021
- tdPort: z42.coerce.number().int().positive().max(65535).default(9980),
5253
+ tdPort: z48.coerce.number().int().positive().max(65535).default(9980),
4022
5254
  /** MCP transport: `stdio` (default, for local clients) or `http` (Streamable HTTP, loopback-only). */
4023
- transport: z42.enum(["stdio", "http"]).default("stdio"),
5255
+ transport: z48.enum(["stdio", "http"]).default("stdio"),
4024
5256
  /** Log verbosity (written to stderr). */
4025
- logLevel: z42.enum(["debug", "info", "warn", "error", "silent"]).default("info"),
5257
+ logLevel: z48.enum(["debug", "info", "warn", "error", "silent"]).default("info"),
4026
5258
  /** Per-request timeout against the TD bridge, in milliseconds. */
4027
- requestTimeoutMs: z42.coerce.number().int().positive().default(1e4),
5259
+ requestTimeoutMs: z48.coerce.number().int().positive().default(1e4),
4028
5260
  /** HTTP transport port (only used when transport=http). */
4029
- httpPort: z42.coerce.number().int().positive().max(65535).default(3939),
5261
+ httpPort: z48.coerce.number().int().positive().max(65535).default(3939),
4030
5262
  /** Subscribe to TD WebSocket events and forward them as MCP logging notifications. */
4031
- events: z42.enum(["on", "off"]).default("on"),
5263
+ events: z48.enum(["on", "off"]).default("on"),
4032
5264
  /**
4033
5265
  * Raw Python escape-hatch tools (`execute_python_script`, `exec_node_method`).
4034
5266
  * Set to "off" to lock them out for restricted setups; on by default.
4035
5267
  */
4036
- rawPython: z42.enum(["on", "off"]).default("on"),
5268
+ rawPython: z48.enum(["on", "off"]).default("on"),
4037
5269
  /**
4038
5270
  * Optional shared bearer token for the TD bridge. When set, the server sends it
4039
5271
  * as `Authorization: Bearer <token>` and the bridge requires a match. Leave unset
4040
5272
  * (default) for the zero-config local flow. Set the SAME value in TouchDesigner's
4041
5273
  * environment (`TDMCP_BRIDGE_TOKEN`) to turn enforcement on.
4042
5274
  */
4043
- bridgeToken: z42.string().min(1).optional()
5275
+ bridgeToken: z48.string().min(1).optional()
4044
5276
  });
4045
5277
  function loadConfig(env = process.env) {
4046
5278
  return ConfigSchema.parse({