@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/README.md +150 -143
- package/dist/cli/agent.d.ts +24 -1
- package/dist/cli/agent.js +423 -365
- package/dist/cli/agent.js.map +1 -1
- package/dist/index.js +1723 -491
- package/dist/index.js.map +1 -1
- package/dist/knowledge/data/meta.json +1 -1
- package/dist/recipes/feedback_tunnel.json +4 -4
- package/dist/recipes/noise_landscape.json +102 -1
- package/dist/recipes/performable_feedback_tunnel.json +92 -0
- package/dist/recipes/webcam_glitch.json +5 -1
- package/package.json +1 -1
- package/recipes/feedback_tunnel.json +4 -4
- package/recipes/noise_landscape.json +102 -1
- package/recipes/performable_feedback_tunnel.json +92 -0
- package/recipes/webcam_glitch.json +5 -1
- package/td/modules/mcp/controllers/api_controller.py +29 -1
- package/td/modules/mcp/services/analysis_service.py +7 -2
- package/td/modules/mcp/services/api_service.py +25 -3
- package/td/modules/mcp/services/batch_service.py +36 -2
- package/td/modules/mcp/services/preview_service.py +84 -11
- package/td/tests/test_api_controller.py +34 -0
- package/td/tests/test_services.py +209 -0
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
982
|
-
source_path:
|
|
983
|
-
effects:
|
|
984
|
-
parent_path:
|
|
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
|
|
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
|
-
|
|
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
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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:
|
|
1064
|
-
|
|
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
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 =
|
|
1169
|
-
data_source:
|
|
1170
|
-
chart_style:
|
|
1171
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
1288
|
-
seed_type:
|
|
1289
|
-
transformations:
|
|
1290
|
-
|
|
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:
|
|
1303
|
-
colors:
|
|
1304
|
-
|
|
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, {
|
|
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
|
|
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 =
|
|
1383
|
-
technique:
|
|
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:
|
|
1395
|
-
evolution_speed:
|
|
1396
|
-
custom_glsl_code:
|
|
1397
|
-
|
|
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(
|
|
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
|
|
1446
|
-
if (
|
|
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
|
-
|
|
1957
|
+
inlineShader,
|
|
1958
|
+
args.evolution_speed
|
|
1452
1959
|
);
|
|
1453
1960
|
return finalize(ctx, {
|
|
1454
|
-
summary: `Created a "${args.technique}" system
|
|
1961
|
+
summary: `Created a "${args.technique}" generative system (GLSL).`,
|
|
1455
1962
|
builder: builder2,
|
|
1456
1963
|
outputPath,
|
|
1457
|
-
|
|
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
|
|
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
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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:
|
|
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
|
-
|
|
1560
|
-
|
|
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
|
|
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 =
|
|
1615
|
-
description:
|
|
1616
|
-
parent_path:
|
|
1617
|
-
resolution:
|
|
1618
|
-
|
|
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(
|
|
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
|
|
1779
|
-
var describeProjectSchema =
|
|
1780
|
-
description:
|
|
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 = "
|
|
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
|
|
1843
|
-
var getPreviewSchema =
|
|
1844
|
-
node_path:
|
|
1845
|
-
width:
|
|
1846
|
-
height:
|
|
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
|
|
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 =
|
|
1887
|
-
source_path:
|
|
1888
|
-
output_type:
|
|
1889
|
-
resolution:
|
|
1890
|
-
record_format:
|
|
1891
|
-
parent_path:
|
|
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
|
|
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
|
|
1966
|
-
var connectNodesSchema =
|
|
1967
|
-
source_path:
|
|
1968
|
-
target_path:
|
|
1969
|
-
source_output:
|
|
1970
|
-
target_input:
|
|
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
|
|
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 =
|
|
2010
|
-
parent_path:
|
|
2011
|
-
name:
|
|
2012
|
-
comp_type:
|
|
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
|
-
() =>
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
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
|
|
2039
|
-
var UniformSchema =
|
|
2040
|
-
name:
|
|
2041
|
-
type:
|
|
2042
|
-
default_value:
|
|
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 =
|
|
2045
|
-
parent_path:
|
|
2046
|
-
name:
|
|
2047
|
-
fragment_shader:
|
|
2048
|
-
vertex_shader:
|
|
2049
|
-
uniforms:
|
|
2050
|
-
resolution:
|
|
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
|
|
2094
|
-
|
|
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
|
-
|
|
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
|
|
2141
|
-
var ChainNodeSchema =
|
|
2142
|
-
type:
|
|
2143
|
-
name:
|
|
2144
|
-
parameters:
|
|
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 =
|
|
2147
|
-
parent_path:
|
|
2148
|
-
nodes:
|
|
2149
|
-
connect_sequentially:
|
|
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
|
|
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 =
|
|
2218
|
-
parent_path:
|
|
2219
|
-
name:
|
|
2220
|
-
code:
|
|
2221
|
-
dat_type:
|
|
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
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
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
|
|
3223
|
+
import { z as z25 } from "zod";
|
|
2255
3224
|
var q10 = (value) => JSON.stringify(value);
|
|
2256
|
-
var duplicateNetworkSchema =
|
|
2257
|
-
source_path:
|
|
2258
|
-
name:
|
|
2259
|
-
parent_path:
|
|
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
|
|
2293
|
-
var UpdateSchema =
|
|
2294
|
-
path:
|
|
2295
|
-
parameters:
|
|
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 =
|
|
2298
|
-
updates:
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
2340
|
-
var compareTdNodesSchema =
|
|
2341
|
-
path_a:
|
|
2342
|
-
path_b:
|
|
2343
|
-
only_diff:
|
|
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 =
|
|
2346
|
-
a:
|
|
2347
|
-
b:
|
|
2348
|
-
type_a:
|
|
2349
|
-
type_b:
|
|
2350
|
-
type_match:
|
|
2351
|
-
differing_count:
|
|
2352
|
-
same_count:
|
|
2353
|
-
differing:
|
|
2354
|
-
identical:
|
|
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
|
|
2409
|
-
var createTdNodeSchema =
|
|
2410
|
-
parent_path:
|
|
2411
|
-
type:
|
|
2412
|
-
name:
|
|
2413
|
-
parameters:
|
|
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) =>
|
|
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
|
|
2448
|
-
var deleteTdNodeSchema =
|
|
2449
|
-
path:
|
|
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
|
|
2472
|
-
var execNodeMethodSchema =
|
|
2473
|
-
path:
|
|
2474
|
-
method:
|
|
2475
|
-
args:
|
|
2476
|
-
kwargs:
|
|
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
|
|
2500
|
-
var executePythonScriptSchema =
|
|
2501
|
-
script:
|
|
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:
|
|
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
|
|
3740
|
+
import { z as z35 } from "zod";
|
|
2528
3741
|
|
|
2529
3742
|
// src/td-client/validators.ts
|
|
2530
|
-
import { z as
|
|
2531
|
-
var ApiEnvelopeSchema =
|
|
2532
|
-
ok:
|
|
2533
|
-
data:
|
|
2534
|
-
error:
|
|
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 =
|
|
2537
|
-
path:
|
|
2538
|
-
type:
|
|
2539
|
-
name:
|
|
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:
|
|
2543
|
-
inputs:
|
|
2544
|
-
outputs:
|
|
2545
|
-
family:
|
|
2546
|
-
errors:
|
|
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 =
|
|
2549
|
-
var InfoSchema =
|
|
2550
|
-
td_version:
|
|
2551
|
-
python_version:
|
|
2552
|
-
build:
|
|
2553
|
-
bridge_version:
|
|
2554
|
-
project:
|
|
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 =
|
|
2557
|
-
path:
|
|
2558
|
-
message:
|
|
2559
|
-
type:
|
|
3771
|
+
var NodeErrorSchema = z34.object({
|
|
3772
|
+
path: z34.string(),
|
|
3773
|
+
message: z34.string(),
|
|
3774
|
+
type: z34.string().optional()
|
|
2560
3775
|
});
|
|
2561
|
-
var NodeErrorsSchema =
|
|
2562
|
-
var PreviewSchema =
|
|
2563
|
-
path:
|
|
2564
|
-
width:
|
|
2565
|
-
height:
|
|
2566
|
-
format:
|
|
2567
|
-
base64:
|
|
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 =
|
|
2570
|
-
result:
|
|
2571
|
-
stdout:
|
|
2572
|
-
printed:
|
|
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 =
|
|
2575
|
-
var DeleteResultSchema =
|
|
2576
|
-
var ConnectionSchema =
|
|
2577
|
-
source_path:
|
|
2578
|
-
source_output:
|
|
2579
|
-
target_path:
|
|
2580
|
-
target_input:
|
|
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 =
|
|
2583
|
-
nodes:
|
|
2584
|
-
connections:
|
|
3797
|
+
var TopologySchema = z34.object({
|
|
3798
|
+
nodes: z34.array(NodeRefSchema).default([]),
|
|
3799
|
+
connections: z34.array(ConnectionSchema).default([])
|
|
2585
3800
|
});
|
|
2586
|
-
var PerformanceSchema =
|
|
2587
|
-
nodes:
|
|
2588
|
-
|
|
2589
|
-
path:
|
|
2590
|
-
cook_time_ms:
|
|
2591
|
-
cook_count:
|
|
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:
|
|
2595
|
-
gpu_memory_mb:
|
|
3809
|
+
total_cook_time_ms: z34.number().optional(),
|
|
3810
|
+
gpu_memory_mb: z34.number().optional()
|
|
2596
3811
|
});
|
|
2597
|
-
var BatchOpResultSchema =
|
|
2598
|
-
action:
|
|
2599
|
-
ok:
|
|
2600
|
-
path:
|
|
2601
|
-
data:
|
|
2602
|
-
error:
|
|
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 =
|
|
2605
|
-
var CreateNodeInputSchema =
|
|
2606
|
-
parent_path:
|
|
2607
|
-
type:
|
|
2608
|
-
name:
|
|
2609
|
-
parameters:
|
|
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 =
|
|
2612
|
-
|
|
2613
|
-
action:
|
|
2614
|
-
parent_path:
|
|
2615
|
-
type:
|
|
2616
|
-
name:
|
|
2617
|
-
parameters:
|
|
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
|
-
|
|
2620
|
-
action:
|
|
2621
|
-
path:
|
|
2622
|
-
parameters:
|
|
3834
|
+
z34.object({
|
|
3835
|
+
action: z34.literal("update"),
|
|
3836
|
+
path: z34.string(),
|
|
3837
|
+
parameters: z34.record(z34.string(), z34.unknown())
|
|
2623
3838
|
}),
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
action:
|
|
2627
|
-
source_path:
|
|
2628
|
-
target_path:
|
|
2629
|
-
source_output:
|
|
2630
|
-
target_input:
|
|
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
|
|
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 =
|
|
2646
|
-
parent_path:
|
|
2647
|
-
pattern:
|
|
2648
|
-
type:
|
|
2649
|
-
recursive:
|
|
2650
|
-
path_only:
|
|
2651
|
-
limit:
|
|
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 =
|
|
2654
|
-
parent_path:
|
|
2655
|
-
recursive:
|
|
2656
|
-
count:
|
|
2657
|
-
truncated:
|
|
2658
|
-
paths:
|
|
2659
|
-
matches:
|
|
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
|
|
2697
|
-
var getModuleHelpSchema =
|
|
2698
|
-
name:
|
|
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
|
|
2740
|
-
var getTdClassDetailsSchema =
|
|
2741
|
-
class_name:
|
|
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
|
|
2772
|
-
var getTdClassesSchema =
|
|
2773
|
-
filter:
|
|
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
|
|
2835
|
-
var getTdNodeErrorsSchema =
|
|
2836
|
-
path:
|
|
2837
|
-
recursive:
|
|
2838
|
-
summary:
|
|
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 =
|
|
2841
|
-
path:
|
|
2842
|
-
total:
|
|
2843
|
-
errors:
|
|
2844
|
-
by_type:
|
|
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
|
|
2887
|
-
var getTdNodeParametersSchema =
|
|
2888
|
-
path:
|
|
2889
|
-
keys:
|
|
2890
|
-
omit_io:
|
|
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
|
|
2934
|
-
var getTdNodesSchema =
|
|
2935
|
-
parent_path:
|
|
2936
|
-
pattern:
|
|
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:
|
|
2940
|
-
limit:
|
|
2941
|
-
detail_level:
|
|
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 =
|
|
2947
|
-
parent_path:
|
|
2948
|
-
count:
|
|
2949
|
-
detail_level:
|
|
2950
|
-
truncated:
|
|
2951
|
-
by_type:
|
|
2952
|
-
sample:
|
|
2953
|
-
paths:
|
|
2954
|
-
nodes:
|
|
2955
|
-
hint:
|
|
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
|
|
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
|
-
|
|
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 =
|
|
3048
|
-
root_path:
|
|
3049
|
-
target_fps:
|
|
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 =
|
|
3052
|
-
path:
|
|
3053
|
-
targetFps:
|
|
3054
|
-
frameBudgetMs:
|
|
3055
|
-
totalCookMs:
|
|
3056
|
-
nodes:
|
|
3057
|
-
|
|
3058
|
-
path:
|
|
3059
|
-
cook_time_ms:
|
|
3060
|
-
cook_count:
|
|
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:
|
|
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
|
|
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 =
|
|
3111
|
-
root_path:
|
|
4329
|
+
var getTdTopologySchema = z43.object({
|
|
4330
|
+
root_path: z43.string().default("/project1").describe("Network root to map.")
|
|
3112
4331
|
});
|
|
3113
|
-
var getTdTopologyOutputSchema =
|
|
3114
|
-
path:
|
|
3115
|
-
nodeCount:
|
|
3116
|
-
connectionCount:
|
|
3117
|
-
issues:
|
|
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
|
|
4363
|
+
import { z as z44 } from "zod";
|
|
3145
4364
|
var MAX_PARAM_NODES = 60;
|
|
3146
|
-
var snapshotTdGraphSchema =
|
|
3147
|
-
path:
|
|
3148
|
-
include_params:
|
|
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 =
|
|
3151
|
-
path:
|
|
3152
|
-
nodeCount:
|
|
3153
|
-
connectionCount:
|
|
3154
|
-
issues:
|
|
3155
|
-
params_truncated:
|
|
3156
|
-
nodes:
|
|
3157
|
-
|
|
3158
|
-
path:
|
|
3159
|
-
type:
|
|
3160
|
-
name:
|
|
3161
|
-
parameters:
|
|
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:
|
|
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
|
|
3219
|
-
var summarizeTdErrorsSchema =
|
|
3220
|
-
path:
|
|
3221
|
-
group_by:
|
|
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 =
|
|
3226
|
-
path:
|
|
3227
|
-
total:
|
|
3228
|
-
group_by:
|
|
3229
|
-
groups:
|
|
3230
|
-
|
|
3231
|
-
key:
|
|
3232
|
-
count:
|
|
3233
|
-
sample:
|
|
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:
|
|
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" :
|
|
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
|
|
3297
|
-
var updateTdNodeParametersSchema =
|
|
3298
|
-
path:
|
|
3299
|
-
parameters:
|
|
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"
|
|
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
|
|
3734
|
-
var RecipeNodeSchema =
|
|
3735
|
-
name:
|
|
3736
|
-
type:
|
|
3737
|
-
parameters:
|
|
3738
|
-
parent:
|
|
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:
|
|
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:
|
|
4963
|
+
comment: z47.string().optional()
|
|
3745
4964
|
});
|
|
3746
|
-
var RecipeConnectionSchema =
|
|
3747
|
-
from:
|
|
3748
|
-
to:
|
|
3749
|
-
from_output:
|
|
3750
|
-
to_input:
|
|
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 =
|
|
3753
|
-
name:
|
|
3754
|
-
node:
|
|
3755
|
-
param:
|
|
3756
|
-
value:
|
|
3757
|
-
label:
|
|
3758
|
-
min:
|
|
3759
|
-
max:
|
|
3760
|
-
description:
|
|
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 =
|
|
3763
|
-
node:
|
|
3764
|
-
name:
|
|
3765
|
-
kind:
|
|
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:
|
|
3769
|
-
label:
|
|
3770
|
-
min:
|
|
3771
|
-
max:
|
|
3772
|
-
description:
|
|
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 =
|
|
3775
|
-
id:
|
|
3776
|
-
name:
|
|
3777
|
-
description:
|
|
3778
|
-
tags:
|
|
3779
|
-
difficulty:
|
|
3780
|
-
td_version_min:
|
|
3781
|
-
nodes:
|
|
3782
|
-
connections:
|
|
3783
|
-
parameters:
|
|
3784
|
-
glsl_uniforms:
|
|
3785
|
-
glsl_code:
|
|
3786
|
-
python_code:
|
|
3787
|
-
|
|
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(
|
|
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
|
|
4017
|
-
var ConfigSchema =
|
|
5248
|
+
import { z as z48 } from "zod";
|
|
5249
|
+
var ConfigSchema = z48.object({
|
|
4018
5250
|
/** TouchDesigner bridge host. */
|
|
4019
|
-
tdHost:
|
|
5251
|
+
tdHost: z48.string().min(1).default("127.0.0.1"),
|
|
4020
5252
|
/** TouchDesigner bridge port (WebServer DAT). */
|
|
4021
|
-
tdPort:
|
|
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:
|
|
5255
|
+
transport: z48.enum(["stdio", "http"]).default("stdio"),
|
|
4024
5256
|
/** Log verbosity (written to stderr). */
|
|
4025
|
-
logLevel:
|
|
5257
|
+
logLevel: z48.enum(["debug", "info", "warn", "error", "silent"]).default("info"),
|
|
4026
5258
|
/** Per-request timeout against the TD bridge, in milliseconds. */
|
|
4027
|
-
requestTimeoutMs:
|
|
5259
|
+
requestTimeoutMs: z48.coerce.number().int().positive().default(1e4),
|
|
4028
5260
|
/** HTTP transport port (only used when transport=http). */
|
|
4029
|
-
httpPort:
|
|
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:
|
|
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:
|
|
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:
|
|
5275
|
+
bridgeToken: z48.string().min(1).optional()
|
|
4044
5276
|
});
|
|
4045
5277
|
function loadConfig(env = process.env) {
|
|
4046
5278
|
return ConfigSchema.parse({
|